From db87743ca1ee1296b68ceb3333c5a9506c4e29d2 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 19 Jul 2023 18:18:47 +0800 Subject: [PATCH] feat(console): apply quota limit for webhooks (#4186) --- .../components/ContactUsPhraseLink/index.tsx | 18 +++++++ .../QuotaGuardFooter/index.module.scss | 16 +++++++ .../src/components/QuotaGuardFooter/index.tsx | 29 ++++++++++++ .../ModalLayout/index.module.scss | 1 + .../src/hooks/use-current-subscription.ts | 7 ++- .../src/hooks/use-subscription-plans.ts | 7 ++- .../components/CreateFormModal/CreateForm.tsx | 47 +++++++++++++++---- .../components/CreateFormModal/index.tsx | 5 +- packages/console/src/pages/Webhooks/index.tsx | 27 ++++++----- packages/console/src/utils/quota.ts | 35 ++++++++++++++ 10 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 packages/console/src/components/ContactUsPhraseLink/index.tsx create mode 100644 packages/console/src/components/QuotaGuardFooter/index.module.scss create mode 100644 packages/console/src/components/QuotaGuardFooter/index.tsx create mode 100644 packages/console/src/utils/quota.ts diff --git a/packages/console/src/components/ContactUsPhraseLink/index.tsx b/packages/console/src/components/ContactUsPhraseLink/index.tsx new file mode 100644 index 000000000..e87bf38a2 --- /dev/null +++ b/packages/console/src/components/ContactUsPhraseLink/index.tsx @@ -0,0 +1,18 @@ +import { type ReactNode } from 'react'; + +import { contactEmailLink } from '@/consts'; +import TextLink from '@/ds-components/TextLink'; + +type Props = { + children?: ReactNode; +}; + +function ContactUsPhraseLink({ children }: Props) { + return ( + + {children} + + ); +} + +export default ContactUsPhraseLink; diff --git a/packages/console/src/components/QuotaGuardFooter/index.module.scss b/packages/console/src/components/QuotaGuardFooter/index.module.scss new file mode 100644 index 000000000..c085bcdad --- /dev/null +++ b/packages/console/src/components/QuotaGuardFooter/index.module.scss @@ -0,0 +1,16 @@ +@use '@/scss/underscore' as _; + +.container { + display: flex; + align-items: center; + gap: _.unit(6); + padding: _.unit(6); + background-color: var(--color-info-container); + margin: 0 _.unit(-6) _.unit(-6); + + .description { + flex: 1; + flex-shrink: 0; + font: var(--font-body-2); + } +} diff --git a/packages/console/src/components/QuotaGuardFooter/index.tsx b/packages/console/src/components/QuotaGuardFooter/index.tsx new file mode 100644 index 000000000..36a0f9c5e --- /dev/null +++ b/packages/console/src/components/QuotaGuardFooter/index.tsx @@ -0,0 +1,29 @@ +import { type ReactNode } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import Button from '@/ds-components/Button'; + +import * as styles from './index.module.scss'; + +type Props = { + children: ReactNode; +}; + +function QuotaGuardFooter({ children }: Props) { + const navigate = useNavigate(); + return ( +
+
{children}
+
+ ); +} + +export default QuotaGuardFooter; diff --git a/packages/console/src/ds-components/ModalLayout/index.module.scss b/packages/console/src/ds-components/ModalLayout/index.module.scss index 0f255bb68..b79e1a5c6 100644 --- a/packages/console/src/ds-components/ModalLayout/index.module.scss +++ b/packages/console/src/ds-components/ModalLayout/index.module.scss @@ -9,6 +9,7 @@ padding: _.unit(6); margin: 0 _.unit(6); box-shadow: var(--shadow-3); + overflow: hidden; .header { display: flex; diff --git a/packages/console/src/hooks/use-current-subscription.ts b/packages/console/src/hooks/use-current-subscription.ts index 233824fe5..ffb6e5da0 100644 --- a/packages/console/src/hooks/use-current-subscription.ts +++ b/packages/console/src/hooks/use-current-subscription.ts @@ -3,14 +3,17 @@ import useSWR from 'swr'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { type Subscription } from '@/cloud/types/router'; -import { isCloud } from '@/consts/env'; +import { isCloud, isProduction } from '@/consts/env'; import { TenantsContext } from '@/contexts/TenantsProvider'; const useCurrentSubscription = () => { const { currentTenantId } = useContext(TenantsContext); const cloudApi = useCloudApi(); return useSWR( - isCloud && `/api/tenants/${currentTenantId}/subscription`, + /** + * Todo: @xiaoyijun remove this condition on subscription features ready. + */ + !isProduction && isCloud && `/api/tenants/${currentTenantId}/subscription`, async () => cloudApi.get('/api/tenants/:tenantId/subscription', { params: { tenantId: currentTenantId }, diff --git a/packages/console/src/hooks/use-subscription-plans.ts b/packages/console/src/hooks/use-subscription-plans.ts index d259585fd..9ef168865 100644 --- a/packages/console/src/hooks/use-subscription-plans.ts +++ b/packages/console/src/hooks/use-subscription-plans.ts @@ -4,7 +4,7 @@ import useSWRImmutable from 'swr/immutable'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { type SubscriptionPlanResponse } from '@/cloud/types/router'; -import { isCloud } from '@/consts/env'; +import { isCloud, isProduction } from '@/consts/env'; import { reservedPlanIdOrder } from '@/consts/subscriptions'; import { type SubscriptionPlan } from '@/types/subscriptions'; import { addSupportQuotaToPlan } from '@/utils/subscription'; @@ -12,7 +12,10 @@ import { addSupportQuotaToPlan } from '@/utils/subscription'; const useSubscriptionPlans = () => { const cloudApi = useCloudApi(); const useSwrResponse = useSWRImmutable( - isCloud && '/api/subscription-plans', + /** + * Todo: @xiaoyijun remove this condition on subscription features ready. + */ + !isProduction && isCloud && '/api/subscription-plans', async () => cloudApi.get('/api/subscription-plans') ); diff --git a/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx b/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx index 72df14490..da30db3eb 100644 --- a/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx +++ b/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx @@ -1,15 +1,22 @@ import { type Hook, type CreateHook, type HookEvent, type HookConfig } from '@logto/schemas'; import { FormProvider, useForm } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; +import ContactUsPhraseLink from '@/components/ContactUsPhraseLink'; +import PlanName from '@/components/PlanName'; +import QuotaGuardFooter from '@/components/QuotaGuardFooter'; import Button from '@/ds-components/Button'; import ModalLayout from '@/ds-components/ModalLayout'; import useApi from '@/hooks/use-api'; +import useCurrentSubscriptionPlan from '@/hooks/use-current-subscription-plan'; import { trySubmitSafe } from '@/utils/form'; +import { isOverQuota } from '@/utils/quota'; import { type BasicWebhookFormType } from '../../types'; import BasicWebhookForm from '../BasicWebhookForm'; type Props = { + totalWebhookCount: number; onClose: (createdHook?: Hook) => void; }; @@ -20,7 +27,16 @@ type CreateHookPayload = Pick & { }; }; -function CreateForm({ onClose }: Props) { +function CreateForm({ totalWebhookCount, onClose }: Props) { + const { data: currentPlan } = useCurrentSubscriptionPlan(); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const shouldBlockCreation = isOverQuota({ + quotaKey: 'hooksLimit', + usage: totalWebhookCount, + plan: currentPlan, + }); + const formMethods = useForm(); const { handleSubmit, @@ -50,14 +66,27 @@ function CreateForm({ onClose }: Props) { title="webhooks.create_form.title" subtitle="webhooks.create_form.subtitle" footer={ -