Skip to content

Commit

Permalink
Merge pull request #4994 from kobotoolbox/nlp-usage-limit-block-frontend
Browse files Browse the repository at this point in the history
[TASK-461] Add modal(s) to NLP page when user is over limits
  • Loading branch information
RuthShryock authored Jul 8, 2024
2 parents bac5130 + 368e755 commit a7b1839
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 28 deletions.
7 changes: 7 additions & 0 deletions jsapp/js/account/stripe.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ export enum PlanNames {
'ENTERPRISE' = 'Enterprise',
}

export enum UsageLimitTypes {
'STORAGE' = 'storage',
'SUBMISSION' = 'submission',
'TRANSCRIPTION' = 'automated transcription',
'TRANSLATION' = 'machine translation',
}

export enum Limits {
'unlimited' = 'unlimited',
}
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/account/usage/usage.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import UsageContainer, {
USAGE_CONTAINER_TYPE,
} from 'js/account/usage/usageContainer';
import envStore from 'js/envStore';
import {formatDate} from 'js/utils';
import {convertSecondsToMinutes, formatDate} from 'js/utils';
import styles from './usage.module.scss';
import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
import {ProductsContext} from '../useProducts.hook';
Expand Down Expand Up @@ -104,7 +104,7 @@ export default function Usage() {
nlpCharacterLimit: limits.nlp_character_limit,
nlpMinuteLimit:
typeof limits.nlp_seconds_limit === 'number'
? limits.nlp_seconds_limit / 60
? convertSecondsToMinutes(limits.nlp_seconds_limit)
: limits.nlp_seconds_limit,
submissionLimit: limits.submission_limit,
isLoaded: true,
Expand Down
14 changes: 5 additions & 9 deletions jsapp/js/account/usage/usageProjectBreakdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {USAGE_ASSETS_PER_PAGE} from 'jsapp/js/constants';
import SortableProjectColumnHeader from 'jsapp/js/projects/projectsTable/sortableProjectColumnHeader';
import type {ProjectFieldDefinition} from 'jsapp/js/projects/projectViews/constants';
import type {ProjectsTableOrder} from 'jsapp/js/projects/projectsTable/projectsTable';
import {truncateNumber} from 'jsapp/js/utils';
import {UsageContext, useUsage} from './useUsage.hook';
import {convertSecondsToMinutes} from 'jsapp/js/utils';
import {UsageContext} from './useUsage.hook';
import Button from 'js/components/common/button';
import Icon from 'js/components/common/icon';
import {OrganizationContext} from 'js/account/organizations/useOrganization.hook';
Expand Down Expand Up @@ -132,10 +132,8 @@ const ProjectBreakdown = () => {
`submission_count_current_${usage.trackingPeriod}`
].toLocaleString();

const periodASRSeconds = truncateNumber(
project[`nlp_usage_current_${usage.trackingPeriod}`]
.total_nlp_asr_seconds / 60,
1
const periodASRSeconds = convertSecondsToMinutes(
project[`nlp_usage_current_${usage.trackingPeriod}`].total_nlp_asr_seconds
).toLocaleString();

const periodMTCharacters =
Expand All @@ -154,9 +152,7 @@ const ProjectBreakdown = () => {
</Link>
</td>
<td>{project.submission_count_all_time.toLocaleString()}</td>
<td className={styles.currentMonth}>
{periodSubmissions}
</td>
<td className={styles.currentMonth}>{periodSubmissions}</td>
<td>{prettyBytes(project.storage_bytes)}</td>
<td>{periodASRSeconds}</td>
<td>{periodMTCharacters}</td>
Expand Down
10 changes: 4 additions & 6 deletions jsapp/js/account/usage/useUsage.hook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createContext, useCallback} from 'react';
import type {Organization, RecurringInterval} from 'js/account/stripe.types';
import {getSubscriptionInterval} from 'js/account/stripe.api';
import {formatRelativeTime, truncateNumber} from 'js/utils';
import {convertSecondsToMinutes, formatRelativeTime} from 'js/utils';
import {getUsage} from 'js/account/usage/usage.api';
import {useApiFetcher, withApiFetcher} from 'js/hooks/useApiFetcher.hook';

Expand Down Expand Up @@ -50,11 +50,9 @@ const loadUsage = async (
return {
storage: usage.total_storage_bytes,
submissions: usage.total_submission_count[`current_${trackingPeriod}`],
transcriptionMinutes: Math.floor(
truncateNumber(
usage.total_nlp_usage[`asr_seconds_current_${trackingPeriod}`] / 60
)
), // seconds to minutes
transcriptionMinutes: convertSecondsToMinutes(
usage.total_nlp_usage[`asr_seconds_current_${trackingPeriod}`]
),
translationChars:
usage.total_nlp_usage[`mt_characters_current_${trackingPeriod}`],
currentMonthStart: usage.current_month_start,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import cx from 'classnames';
import React from 'react';
import KoboModal from '../../modals/koboModal';
import KoboModalHeader from 'js/components/modals/koboModalHeader';
import KoboModalFooter from 'js/components/modals/koboModalFooter';
import Button from 'js/components/common/button';
import Icon from 'js/components/common/icon';
import {useNavigate} from 'react-router-dom';
import styles from './nlpUsageLimitBlockModal.module.scss';
import {ACCOUNT_ROUTES} from 'js/account/routes.constants';
import { RecurringInterval, UsageLimitTypes } from 'jsapp/js/account/stripe.types';

interface NlpUsageLimitBlockModalProps {
isModalOpen: boolean;
dismissed: () => void;
usageType: UsageLimitTypes.TRANSLATION | UsageLimitTypes.TRANSCRIPTION;
interval: RecurringInterval;
}

function NlpUsageLimitBlockModal(props: NlpUsageLimitBlockModalProps) {
const navigate = useNavigate();

const handleClose = () => {
props.dismissed();
};

return (
<div>
<KoboModal
isOpen={props.isModalOpen}
onRequestClose={handleClose}
size='medium'
>
<KoboModalHeader onRequestCloseByX={handleClose} headerColor='white'>
{t('Upgrade to continue using this feature')}
</KoboModalHeader>

<section className={styles.modalBody}>
<div>
<div>
{t('You have reached the ##LIMIT## limit for this ##PERIOD##.')
.replace('##LIMIT##', props.usageType)
.replace('##PERIOD##', props.interval)}{' '}
{t(
'Please consider our plans or add-ons to continue using this feature.'
)}
</div>
<div className={styles.note}>
<Icon
name='information'
size='s'
color='blue'
className={styles.noteIcon}
/>
{t('You can monitor your usage')}&nbsp;
<a href={'/#/account/usage'}>{t('here')}</a>.
</div>
</div>
</section>

<KoboModalFooter alignment='end'>
<Button
type='frame'
color='dark-blue'
size='l'
onClick={handleClose}
label={t('Go back')}
/>

<Button
type='full'
color='blue'
size='l'
onClick={() => navigate(ACCOUNT_ROUTES.PLAN)}
label={t('Upgrade now')}
/>
</KoboModalFooter>
</KoboModal>
</div>
);
}

export default NlpUsageLimitBlockModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@use 'scss/colors';
@use 'scss/sizes';


.modalBody {
line-height: sizes.$x24;
padding: 0 sizes.$x30;

p:first-of-type {
margin-top: 0;
}
}

.note {
display: flex;
padding: sizes.$x12;
margin: sizes.$x24 0 0 0;
background-color: colors.$kobo-bg-blue;
border-radius: sizes.$x5;
}

.noteIcon {
margin-right: sizes.$x8;
}
33 changes: 31 additions & 2 deletions jsapp/js/components/processing/transcript/stepConfig.component.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useContext, useMemo, useState} from 'react';
import cx from 'classnames';
import clonedeep from 'lodash.clonedeep';
import Button from 'js/components/common/button';
Expand All @@ -13,8 +13,23 @@ import type {
import TransxAutomaticButton from 'js/components/processing/transxAutomaticButton';
import envStore from 'js/envStore';
import bodyStyles from 'js/components/processing/processingBody.module.scss';
import NlpUsageLimitBlockModal from '../nlpUsageLimitBlockModal/nlpUsageLimitBlockModal.component';
import {UsageLimitTypes} from 'js/account/stripe.types';
import {UsageContext} from 'js/account/usage/useUsage.hook';
import {useExceedingLimits} from 'js/components/usageLimits/useExceedingLimits.hook';

export default function StepConfig() {
const [usage] = useContext(UsageContext);
const limits = useExceedingLimits();
const [isLimitBlockModalOpen, setIsLimitBlockModalOpen] =
useState<boolean>(false);
const isOverLimit = useMemo(() => {
return limits.exceedList.includes(UsageLimitTypes.TRANSCRIPTION);
}, [limits.exceedList]);

function dismissLimitBlockModal() {
setIsLimitBlockModalOpen(false);
}
/** Changes the draft value, preserving the other draft properties. */
function setDraftValue(newVal: string | undefined) {
const newDraft =
Expand Down Expand Up @@ -62,6 +77,14 @@ export default function StepConfig() {
singleProcessingStore.setTranscriptDraft(newDraft);
}

function onAutomaticButtonClick() {
if (isOverLimit) {
setIsLimitBlockModalOpen(true);
} else {
selectModeAuto();
}
}

const draft = singleProcessingStore.getTranscriptDraft();
const typeLabel =
singleProcessingStore.currentQuestionType || t('source file');
Expand Down Expand Up @@ -103,10 +126,16 @@ export default function StepConfig() {
/>

<TransxAutomaticButton
onClick={selectModeAuto}
onClick={onAutomaticButtonClick}
selectedLanguage={draft?.languageCode}
type='transcript'
/>
<NlpUsageLimitBlockModal
isModalOpen={isLimitBlockModalOpen}
usageType={UsageLimitTypes.TRANSCRIPTION}
dismissed={dismissLimitBlockModal}
interval={usage.trackingPeriod}
/>
</div>
</footer>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getRowData,
getMediaAttachment,
} from 'js/components/submissions/submissionUtils';
import { convertSecondsToMinutes } from 'jsapp/js/utils';

/**
* Returns an error string or the attachment. It's basically a wrapper function
Expand Down Expand Up @@ -54,7 +55,7 @@ export function secondsToTranscriptionEstimate(sourceSeconds: number): string {
} else if (durationSeconds >= 75 && durationSeconds < 150) {
return t('about 2 minutes');
} else {
const durationMinutes = Math.round(durationSeconds / 60);
const durationMinutes = convertSecondsToMinutes(durationSeconds);
return t('about ##number## minutes').replace('##number##', String(durationMinutes));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useContext, useMemo, useState} from 'react';
import cx from 'classnames';
import clonedeep from 'lodash.clonedeep';
import Button from 'js/components/common/button';
Expand All @@ -14,8 +14,23 @@ import type {
} from 'js/components/languages/languagesStore';
import envStore from 'js/envStore';
import bodyStyles from 'js/components/processing/processingBody.module.scss';
import NlpUsageLimitBlockModal from '../nlpUsageLimitBlockModal/nlpUsageLimitBlockModal.component';
import {UsageLimitTypes} from 'js/account/stripe.types';
import {UsageContext} from 'js/account/usage/useUsage.hook';
import {useExceedingLimits} from 'js/components/usageLimits/useExceedingLimits.hook';

export default function StepConfig() {
const [usage] = useContext(UsageContext);
const limits = useExceedingLimits();
const [isLimitBlockModalOpen, setIsLimitBlockModalOpen] =
useState<boolean>(false);
const isOverLimit = useMemo(() => {
return limits.exceedList.includes(UsageLimitTypes.TRANSLATION);
}, [limits.exceedList]);

function dismissLimitBlockModal() {
setIsLimitBlockModalOpen(false);
}
/** Changes the draft value, preserving the other draft properties. */
function setDraftValue(newVal: string | undefined) {
const newDraft =
Expand Down Expand Up @@ -74,6 +89,14 @@ export default function StepConfig() {
singleProcessingStore.setTranslationDraft(newDraft);
}

function onAutomaticButtonClick() {
if (isOverLimit) {
setIsLimitBlockModalOpen(true);
} else {
selectModeAuto();
}
}

const draft = singleProcessingStore.getTranslationDraft();
const isAutoEnabled = envStore.data.asr_mt_features_enabled;

Expand Down Expand Up @@ -112,10 +135,16 @@ export default function StepConfig() {
/>

<TransxAutomaticButton
onClick={selectModeAuto}
onClick={onAutomaticButtonClick}
selectedLanguage={draft?.languageCode}
type='translation'
/>
<NlpUsageLimitBlockModal
isModalOpen={isLimitBlockModalOpen}
usageType={UsageLimitTypes.TRANSLATION}
dismissed={dismissLimitBlockModal}
interval={usage.trackingPeriod}
/>
</div>
</footer>
</div>
Expand Down
19 changes: 13 additions & 6 deletions jsapp/js/components/usageLimits/useExceedingLimits.hook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {useState, useReducer, useContext, useEffect} from 'react';
import type {SubscriptionInfo} from 'js/account/stripe.types';
import {SubscriptionInfo, UsageLimitTypes} from 'js/account/stripe.types';
import {getAccountLimits} from 'js/account/stripe.api';
import {USAGE_WARNING_RATIO} from 'js/constants';
import {convertSecondsToMinutes} from 'jsapp/js/utils';
import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
import {when} from 'mobx';
import subscriptionStore from 'js/account/subscriptionStore';
Expand Down Expand Up @@ -47,7 +48,9 @@ export const useExceedingLimits = () => {
getAccountLimits(productsContext.products).then((limits) => {
setSubscribedSubmissionLimit(limits.submission_limit);
setSubscribedStorageLimit(limits.storage_bytes_limit);
setTranscriptionMinutes(Number(limits.nlp_seconds_limit));
setTranscriptionMinutes(
convertSecondsToMinutes(Number(limits.nlp_seconds_limit))
);
setTranslationChars(Number(limits.nlp_character_limit));
setAreLimitsLoaded(true);
});
Expand Down Expand Up @@ -91,17 +94,21 @@ export const useExceedingLimits = () => {
return;
}
setExceedList(() => []);
isOverLimit(subscribedStorageLimit, usage.storage, 'storage');
isOverLimit(subscribedSubmissionLimit, usage.submissions, 'submission');
isOverLimit(subscribedStorageLimit, usage.storage, UsageLimitTypes.STORAGE);
isOverLimit(
subscribedSubmissionLimit,
usage.submissions,
UsageLimitTypes.SUBMISSION
);
isOverLimit(
subscribedTranscriptionMinutes,
usage.transcriptionMinutes,
'automated transcription'
UsageLimitTypes.TRANSCRIPTION
);
isOverLimit(
subscribedTranslationChars,
usage.translationChars,
'machine translation'
UsageLimitTypes.TRANSLATION
);
}, [usageStatus, areLimitsLoaded]);

Expand Down
Loading

0 comments on commit a7b1839

Please sign in to comment.