0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

fix(core,console): disable quota guard and unblock resource creation for pro tenants (#6487)

This commit is contained in:
Darcy Ye 2024-08-21 16:58:10 +08:00 committed by GitHub
parent c3bec6803d
commit a999c51919
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 92 additions and 44 deletions

View file

@ -51,7 +51,7 @@ function TabNavItem<Paths extends string>({
)}
</div>
{errorCount > 0 && (
<div className={styles.errors}>{t('general.tab_errors', { count: errorCount })}</div>
<div className={styles.errors}>{t('general.tab_error', { count: errorCount })}</div>
)}
</div>
);

View file

@ -62,7 +62,8 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
const isSsoEnabled =
!isCloud ||
currentSubscriptionQuota.enterpriseSsoLimit === null ||
currentSubscriptionQuota.enterpriseSsoLimit > 0;
currentSubscriptionQuota.enterpriseSsoLimit > 0 ||
planId === ReservedPlanId.Pro;
const { data, error } = useSWR<SsoConnectorProvidersResponse, RequestError>(
'api/sso-connector-providers'

View file

@ -37,7 +37,7 @@ function EnterpriseSso() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { isDevTenant } = useContext(TenantsContext);
const {
currentSubscription: { isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable },
currentSubscriptionQuota,
} = useContext(SubscriptionDataContext);
@ -45,7 +45,8 @@ function EnterpriseSso() {
page: 1,
});
const isSsoEnabled = !isCloud || currentSubscriptionQuota.enterpriseSsoLimit !== 0;
const isSsoEnabled =
!isCloud || currentSubscriptionQuota.enterpriseSsoLimit !== 0 || planId === ReservedPlanId.Pro;
const url = buildUrl('api/sso-connectors', {
page: String(page),

View file

@ -1,4 +1,4 @@
import { MfaFactor, MfaPolicy, type SignInExperience } from '@logto/schemas';
import { MfaFactor, MfaPolicy, ReservedPlanId, type SignInExperience } from '@logto/schemas';
import { useContext, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
@ -33,9 +33,13 @@ type Props = {
};
function MfaForm({ data, onMfaUpdated }: Props) {
const { currentSubscriptionQuota, mutateSubscriptionQuotaAndUsages } =
useContext(SubscriptionDataContext);
const isMfaDisabled = isCloud && !currentSubscriptionQuota.mfaEnabled;
const {
currentSubscription: { planId },
currentSubscriptionQuota,
mutateSubscriptionQuotaAndUsages,
} = useContext(SubscriptionDataContext);
const isMfaDisabled =
isCloud && !currentSubscriptionQuota.mfaEnabled && planId !== ReservedPlanId.Pro;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();

View file

@ -17,10 +17,10 @@ type Props = {
function PageWrapper({ children }: Props) {
const { isDevTenant } = useContext(TenantsContext);
const {
currentSubscription: { isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable },
currentSubscriptionQuota: { mfaEnabled },
} = useContext(SubscriptionDataContext);
const isMfaEnabled = !isCloud || mfaEnabled;
const isMfaEnabled = !isCloud || mfaEnabled || planId === ReservedPlanId.Pro;
return (
<div className={styles.container}>

View file

@ -32,9 +32,13 @@ const basePathname = '/organization-template';
function OrganizationTemplate() {
const { getDocumentationUrl } = useDocumentationUrl();
const [isGuideDrawerOpen, setIsGuideDrawerOpen] = useState(false);
const { currentSubscriptionQuota } = useContext(SubscriptionDataContext);
const {
currentSubscription: { planId },
currentSubscriptionQuota,
} = useContext(SubscriptionDataContext);
const { isDevTenant } = useContext(TenantsContext);
const isOrganizationsDisabled = isCloud && !currentSubscriptionQuota.organizationsEnabled;
const isOrganizationsDisabled =
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
const { navigate } = useTenantPathname();
const handleUpgradePlan = useCallback(() => {

View file

@ -40,7 +40,8 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
data: { organizationUpsellNoticeAcknowledged },
update,
} = useUserPreferences();
const isOrganizationsDisabled = isCloud && !currentSubscriptionQuota.organizationsEnabled;
const isOrganizationsDisabled =
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
const {
reset,

View file

@ -26,7 +26,7 @@ const organizationsPathname = '/organizations';
function Organizations() {
const { getDocumentationUrl } = useDocumentationUrl();
const {
currentSubscription: { isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable },
currentSubscriptionQuota,
} = useContext(SubscriptionDataContext);
const { isDevTenant } = useContext(TenantsContext);
@ -34,7 +34,8 @@ function Organizations() {
const { navigate } = useTenantPathname();
const [isCreating, setIsCreating] = useState(false);
const isOrganizationsDisabled = isCloud && !currentSubscriptionQuota.organizationsEnabled;
const isOrganizationsDisabled =
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
const upgradePlan = useCallback(() => {
navigate(subscriptionPage);

View file

@ -14,11 +14,14 @@ import { type CloudConnectionLibrary } from './cloud-connection.js';
export type QuotaLibrary = ReturnType<typeof createQuotaLibrary>;
const shouldReportSubscriptionUpdates = (
planId: string,
key: keyof SubscriptionQuota,
isAddOnAvailable?: boolean
) => planId === ReservedPlanId.Pro && isAddOnAvailable && isReportSubscriptionUpdatesUsageKey(key);
/**
* @remarks
* Should report usage changes to the Cloud only when the following conditions are met:
* 1. The tenant is on the Pro plan.
* 2. The usage key is add-on related usage key.
*/
const shouldReportSubscriptionUpdates = (planId: string, key: keyof SubscriptionQuota) =>
planId === ReservedPlanId.Pro && isReportSubscriptionUpdatesUsageKey(key);
export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => {
const guardTenantUsageByKey = async (key: keyof SubscriptionQuota) => {
@ -36,13 +39,12 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => {
const {
planId,
isAddOnAvailable,
quota: fullQuota,
usage: fullUsage,
} = await getTenantSubscriptionData(cloudConnection);
// Do not block Pro plan from adding add-on resources.
if (shouldReportSubscriptionUpdates(planId, key, isAddOnAvailable)) {
if (shouldReportSubscriptionUpdates(planId, key)) {
return;
}
@ -159,7 +161,7 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => {
const { planId, isAddOnAvailable } = await getTenantSubscriptionData(cloudConnection);
if (shouldReportSubscriptionUpdates(planId, key, isAddOnAvailable)) {
if (shouldReportSubscriptionUpdates(planId, key) && isAddOnAvailable) {
await reportSubscriptionUpdates(cloudConnection, key);
}
};

View file

@ -57,6 +57,10 @@ export const reportSubscriptionUpdates = async (
);
};
/**
* @remarks
* Check whether the provided usage key is add-on related usage key.
*/
export const isReportSubscriptionUpdatesUsageKey = (
value: string
): value is ReportSubscriptionUpdatesUsageKey => {

View file

@ -73,7 +73,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
await expectErrorsOnNavTab(page, {
tab: 'Sign-up and sign-in',
error: '1 errors',
error: '1 error',
});
});
@ -129,7 +129,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
await expectErrorsOnNavTab(page, {
tab: 'Sign-up and sign-in',
error: '1 errors',
error: '1 error',
});
});
@ -220,7 +220,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
await expectSignInMethodError(page, 'Phone number');
await expectErrorsOnNavTab(page, {
tab: 'Sign-up and sign-in',
error: '1 errors',
error: '1 error',
});
// Disable password option for sign-in method
@ -234,7 +234,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
await expectSignInMethodError(page, 'Phone number');
await expectErrorsOnNavTab(page, {
tab: 'Sign-up and sign-in',
error: '1 errors',
error: '1 error',
});
});
});
@ -285,7 +285,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
await expectSignInMethodError(page, 'Email address');
await expectErrorsOnNavTab(page, {
tab: 'Sign-up and sign-in',
error: '1 errors',
error: '1 error',
});
// Disable password option for sign-in method
@ -299,7 +299,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
await expectSignInMethodError(page, 'Email address');
await expectErrorsOnNavTab(page, {
tab: 'Sign-up and sign-in',
error: '1 errors',
error: '1 error',
});
});
});

View file

@ -47,7 +47,9 @@ const general = {
continue: 'Fortsetzen',
page_info: '{{min, number}}-{{max, number}} von {{total, number}}',
learn_more: 'Mehr erfahren',
tab_errors: '{{count, number}} Fehler',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} Fehler',
skip_for_now: 'Jetzt überspringen',
remove: 'Entfernen',
visit: 'Besuchen',

View file

@ -46,7 +46,9 @@ const general = {
continue: 'Continue',
page_info: '{{min, number}}-{{max, number}} of {{total, number}}',
learn_more: 'Learn more',
tab_errors: '{{count, number}} errors',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} errors',
skip_for_now: 'Skip for now',
remove: 'Remove',
visit: 'Visit',

View file

@ -47,7 +47,9 @@ const general = {
continue: 'Continuar',
page_info: '{{min, number}}-{{max, number}} de {{total, number}}',
learn_more: 'Saber más',
tab_errors: '{{count, number}} errores',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} errores',
skip_for_now: 'Omitir por ahora',
remove: 'Eliminar',
visit: 'Visitar',

View file

@ -47,7 +47,9 @@ const general = {
continue: 'Continuez',
page_info: '{{min, number}}-{{max, number}} de {{total, number}}',
learn_more: 'En savoir plus',
tab_errors: '{{count, number}} erreurs',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} erreurs',
skip_for_now: 'Passer pour l`instant',
remove: 'Supprimer',
visit: 'Visiter',

View file

@ -47,7 +47,9 @@ const general = {
continue: 'Continua',
page_info: '{{min, number}}-{{max, number}} di {{total, number}}',
learn_more: 'Scopri di più',
tab_errors: '{{count, number}} errori',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} errori',
skip_for_now: 'Salta per ora',
remove: 'Rimuovi',
visit: 'Visita',

View file

@ -46,7 +46,9 @@ const general = {
continue: '続ける',
page_info: '{{total}}件中{{min}}件〜{{max}}件を表示',
learn_more: '詳しく見る',
tab_errors: '{{count}}件のエラーがあります',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count}}件のエラーがあります',
skip_for_now: '今回はスキップする',
remove: '削除する',
visit: '訪問する',

View file

@ -46,7 +46,9 @@ const general = {
continue: '계속하기',
page_info: '{{min, number}}-{{max, number}} / {{total, number}}',
learn_more: '더 알아보기',
tab_errors: '{{count, number}} 오류',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} 오류',
skip_for_now: '지금은 건너뛰기',
remove: '삭제',
visit: '방문하기',

View file

@ -46,7 +46,9 @@ const general = {
continue: 'Kontynuuj',
page_info: '{{min, number}}-{{max, number}} z {{total, number}}',
learn_more: 'Dowiedz się więcej',
tab_errors: '{{count, number}} błędów',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} błędów',
skip_for_now: 'Pomiń na teraz',
remove: 'Usuń',
visit: 'Odwiedź',

View file

@ -47,7 +47,9 @@ const general = {
continue: 'Continuar',
page_info: '{{min, number}}-{{max, number}} de {{total, number}}',
learn_more: 'Saiba mais',
tab_errors: '{{count, number}} erros',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} erros',
skip_for_now: 'Pular por agora',
remove: 'Remover',
visit: 'Visitar',

View file

@ -46,7 +46,9 @@ const general = {
continue: 'Continuar',
page_info: '{{min, number}}-{{max, number}} de {{total, number}}',
learn_more: 'Saber mais',
tab_errors: '{{count, number}} erros',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} erros',
skip_for_now: 'Saltar por agora',
remove: 'Remover',
visit: 'Visitar',

View file

@ -46,7 +46,9 @@ const general = {
continue: 'Продолжить',
page_info: '{{min, number}}-{{max, number}} из {{total, number}}',
learn_more: 'Узнать больше',
tab_errors: '{{count, number}} ошибок',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} ошибок',
skip_for_now: 'Пропустить',
remove: 'Удалить',
visit: 'Посетить',

View file

@ -47,7 +47,9 @@ const general = {
continue: 'Devam et',
page_info: '{{min, number}}-{{max, number}} / {{total, number}}',
learn_more: 'Daha fazla bilgi edinin',
tab_errors: '{{count, number}} hata',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} hata',
skip_for_now: 'Şimdilik atla',
remove: 'Kaldır',
visit: 'Ziyaret et',

View file

@ -46,7 +46,9 @@ const general = {
continue: '继续',
page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 条',
learn_more: '了解更多',
tab_errors: '{{count, number}} 个错误',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} 个错误',
skip_for_now: '先跳过',
remove: '移除',
visit: '访问',

View file

@ -46,7 +46,9 @@ const general = {
continue: '繼續',
page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 條',
learn_more: '了解更多',
tab_errors: '{{count, number}} 個錯誤',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} 個錯誤',
skip_for_now: '先跳過',
remove: '移除',
visit: '訪問',

View file

@ -46,7 +46,9 @@ const general = {
continue: '繼續',
page_info: '{{min, number}}-{{max, number}} 共 {{total, number}} 條',
learn_more: '了解更多',
tab_errors: '{{count, number}} 個錯誤',
/** UNTRANSLATED */
tab_error_one: '{{count, number}} error',
tab_error_other: '{{count, number}} 個錯誤',
skip_for_now: '先跳過',
remove: '移除',
visit: '訪問',