mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(console): add quota guard to protected app creation form (#5353)
This commit is contained in:
parent
df92091106
commit
1d20a3a1ff
3 changed files with 53 additions and 25 deletions
|
@ -4,16 +4,22 @@ import { isValidSubdomain } from '@logto/shared/universal';
|
|||
import { condString, conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { HTTPError } from 'ky';
|
||||
import { useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button, { type Props as ButtonProps } from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useApplicationsUsage from '@/hooks/use-applications-usage';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -41,6 +47,9 @@ function ProtectedAppForm({
|
|||
const { data } = useSWRImmutable<ProtectedAppsDomainConfig>(
|
||||
isDevFeaturesEnabled && isCloud && 'api/systems/application'
|
||||
);
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const { hasAppsReachedLimit } = useApplicationsUsage();
|
||||
const { name: planName, quota } = currentPlan;
|
||||
const defaultDomain = data?.protectedApps.defaultDomain ?? '';
|
||||
const { navigate } = useTenantPathname();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
@ -166,17 +175,30 @@ function ProtectedAppForm({
|
|||
</div>
|
||||
</FormField>
|
||||
</div>
|
||||
<Button
|
||||
className={classNames(
|
||||
styles.submitButton,
|
||||
buttonAlignment === 'left' && styles.leftAligned
|
||||
)}
|
||||
size={buttonSize}
|
||||
type="primary"
|
||||
title={buttonText}
|
||||
isLoading={isSubmitting}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
{hasAppsReachedLimit ? (
|
||||
<QuotaGuardFooter>
|
||||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName name={planName} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.applications', { count: quota.applicationsLimit ?? 0 })}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
) : (
|
||||
<Button
|
||||
className={classNames(
|
||||
styles.submitButton,
|
||||
buttonAlignment === 'left' && styles.leftAligned
|
||||
)}
|
||||
size={buttonSize}
|
||||
type="primary"
|
||||
title={buttonText}
|
||||
isLoading={isSubmitting}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
.protectedApps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(8);
|
||||
gap: _.unit(3);
|
||||
background: var(--color-success-container);
|
||||
padding: _.unit(5) _.unit(9);
|
||||
|
||||
|
@ -31,6 +31,7 @@
|
|||
.list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: _.unit(8);
|
||||
}
|
||||
|
||||
.app {
|
||||
|
|
|
@ -6,6 +6,7 @@ import useSWR from 'swr';
|
|||
|
||||
import OpenExternalLink from '@/components/OpenExternalLink';
|
||||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import useApplicationsUsage from '@/hooks/use-applications-usage';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import ProtectedAppCard from '@/pages/Applications/components/ProtectedAppCard';
|
||||
import ProtectedAppForm from '@/pages/Applications/components/ProtectedAppForm';
|
||||
|
@ -16,7 +17,9 @@ function ProtectedAppCreationForm() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getTo } = useTenantPathname();
|
||||
const { data, mutate } = useSWR<Application[]>('api/applications?types=Protected');
|
||||
const { hasAppsReachedLimit } = useApplicationsUsage();
|
||||
const hasProtectedApps = !!data?.length;
|
||||
const showCreationForm = !hasProtectedApps && !hasAppsReachedLimit;
|
||||
|
||||
const mutateApps = useCallback(
|
||||
(app: Application) => {
|
||||
|
@ -27,18 +30,20 @@ function ProtectedAppCreationForm() {
|
|||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ProtectedAppCard hasCreateButton={hasProtectedApps} onCreateSuccess={mutateApps} />
|
||||
<div className={styles.separator} />
|
||||
{!hasProtectedApps && (
|
||||
<div className={styles.form}>
|
||||
<ProtectedAppForm
|
||||
hasDetailedInstructions
|
||||
buttonAlignment="left"
|
||||
buttonText="protected_app.form.create_protected_app"
|
||||
buttonSize="medium"
|
||||
onCreateSuccess={mutateApps}
|
||||
/>
|
||||
</div>
|
||||
<ProtectedAppCard hasCreateButton={!showCreationForm} onCreateSuccess={mutateApps} />
|
||||
{showCreationForm && (
|
||||
<>
|
||||
<div className={styles.separator} />
|
||||
<div className={styles.form}>
|
||||
<ProtectedAppForm
|
||||
hasDetailedInstructions
|
||||
buttonAlignment="left"
|
||||
buttonText="protected_app.form.create_protected_app"
|
||||
buttonSize="medium"
|
||||
onCreateSuccess={mutateApps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasProtectedApps && (
|
||||
<div className={styles.protectedApps}>
|
||||
|
|
Loading…
Add table
Reference in a new issue