0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor: use orgsLimit instead of orgsEnabled as org quota key (#6570)

* refactor: use orgsLimit instead of orgsEnabled as org quota key

* refactor: implement getUsageByKey method

* chore: undo logto email connector dependency update
This commit is contained in:
Darcy Ye 2024-09-12 11:17:43 +08:00 committed by GitHub
parent a6178f45e2
commit 343602027d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 112 additions and 54 deletions

View file

@ -27,7 +27,7 @@
"devDependencies": {
"@fontsource/roboto-mono": "^5.0.0",
"@jest/types": "^29.5.0",
"@logto/cloud": "0.2.5-20fd0a2",
"@logto/cloud": "0.2.5-91ab76c",
"@logto/connector-kit": "workspace:^4.0.0",
"@logto/core-kit": "workspace:^2.5.0",
"@logto/elements": "workspace:^0.0.0",

View file

@ -20,11 +20,23 @@ export type NewSubscriptionUsageResponse = GuardedResponse<
GetRoutes['/api/tenants/:tenantId/subscription-usage']
>;
/** The response of `GET /api/tenants/my/subscription/quota` has the same response type. */
export type NewSubscriptionQuota = NewSubscriptionUsageResponse['quota'];
export type NewSubscriptionQuota = Omit<
NewSubscriptionUsageResponse['quota'],
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the quota keys for now to avoid confusion.
'organizationsEnabled'
>;
/** The response of `GET /api/tenants/my/subscription/usage` has the same response type. */
export type NewSubscriptionCountBasedUsage = NewSubscriptionUsageResponse['usage'];
export type NewSubscriptionCountBasedUsage = Omit<
NewSubscriptionUsageResponse['usage'],
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the usage keys for now to avoid confusion.
'organizationsEnabled'
>;
export type NewSubscriptionResourceScopeUsage = NewSubscriptionUsageResponse['resources'];
export type NewSubscriptionRoleScopeUsage = NewSubscriptionUsageResponse['roles'];
export type NewSubscriptionRoleScopeUsage = Omit<
NewSubscriptionUsageResponse['roles'],
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the quota keys for now to avoid confusion.
'organizationsEnabled'
>;
export type NewSubscriptionPeriodicUsage = GuardedResponse<
GetRoutes['/api/tenants/:tenantId/subscription/periodic-usage']

View file

@ -4,7 +4,10 @@ import classNames from 'classnames';
import dayjs from 'dayjs';
import { useContext, useMemo } from 'react';
import { type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
import {
type NewSubscriptionPeriodicUsage,
type NewSubscriptionCountBasedUsage,
} from '@/cloud/types/router';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import DynamicT from '@/ds-components/DynamicT';
@ -12,12 +15,41 @@ import { formatPeriod } from '@/utils/subscription';
import PlanUsageCard, { type Props as PlanUsageCardProps } from './PlanUsageCard';
import styles from './index.module.scss';
import { usageKeys, usageKeyPriceMap, usageKeyMap, titleKeyMap, tooltipKeyMap } from './utils';
import {
type UsageKey,
usageKeys,
usageKeyPriceMap,
usageKeyMap,
titleKeyMap,
tooltipKeyMap,
} from './utils';
type Props = {
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
};
const getUsageByKey = (
key: keyof UsageKey,
{
periodicUsage,
countBasedUsage,
}: {
periodicUsage: NewSubscriptionPeriodicUsage;
countBasedUsage: NewSubscriptionCountBasedUsage;
}
) => {
if (key === 'mauLimit' || key === 'tokenLimit') {
return periodicUsage[key];
}
// Show enabled status for organization feature.
if (key === 'organizationsLimit') {
return countBasedUsage[key] > 0;
}
return countBasedUsage[key];
};
function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
const {
currentSubscriptionQuota,
@ -59,10 +91,7 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
isAddOnAvailable || (onlyShowPeriodicUsage && (key === 'mauLimit' || key === 'tokenLimit'))
)
.map((key) => ({
usage:
key === 'mauLimit' || key === 'tokenLimit'
? periodicUsage[key]
: currentSubscriptionUsage[key],
usage: getUsageByKey(key, { periodicUsage, countBasedUsage: currentSubscriptionUsage }),
usageKey: `subscription.usage.${usageKeyMap[key]}`,
titleKey: `subscription.usage.${titleKeyMap[key]}`,
unitPrice: usageKeyPriceMap[key],

View file

@ -12,10 +12,10 @@ import {
hooksAddOnUnitPrice,
} from '@/consts/subscriptions';
type UsageKey = Pick<
export type UsageKey = Pick<
NewSubscriptionQuota,
| 'mauLimit'
| 'organizationsEnabled'
| 'organizationsLimit'
| 'mfaEnabled'
| 'enterpriseSsoLimit'
| 'resourcesLimit'
@ -28,7 +28,7 @@ type UsageKey = Pick<
// We decide not to show `hooksLimit` usage in console for now.
export const usageKeys: Array<keyof UsageKey> = [
'mauLimit',
'organizationsEnabled',
'organizationsLimit',
'mfaEnabled',
'enterpriseSsoLimit',
'resourcesLimit',
@ -39,7 +39,7 @@ export const usageKeys: Array<keyof UsageKey> = [
export const usageKeyPriceMap: Record<keyof UsageKey, number> = {
mauLimit: 0,
organizationsEnabled: organizationAddOnUnitPrice,
organizationsLimit: organizationAddOnUnitPrice,
mfaEnabled: mfaAddOnUnitPrice,
enterpriseSsoLimit: enterpriseSsoAddOnUnitPrice,
resourcesLimit: resourceAddOnUnitPrice,
@ -54,7 +54,7 @@ export const usageKeyMap: Record<
TFuncKey<'translation', 'admin_console.subscription.usage'>
> = {
mauLimit: 'mau.description',
organizationsEnabled: 'organizations.description',
organizationsLimit: 'organizations.description',
mfaEnabled: 'mfa.description',
enterpriseSsoLimit: 'enterprise_sso.description',
resourcesLimit: 'api_resources.description',
@ -69,7 +69,7 @@ export const titleKeyMap: Record<
TFuncKey<'translation', 'admin_console.subscription.usage'>
> = {
mauLimit: 'mau.title',
organizationsEnabled: 'organizations.title',
organizationsLimit: 'organizations.title',
mfaEnabled: 'mfa.title',
enterpriseSsoLimit: 'enterprise_sso.title',
resourcesLimit: 'api_resources.title',
@ -84,7 +84,7 @@ export const tooltipKeyMap: Record<
TFuncKey<'translation', 'admin_console.subscription.usage'>
> = {
mauLimit: 'mau.tooltip',
organizationsEnabled: 'organizations.tooltip',
organizationsLimit: 'organizations.tooltip',
mfaEnabled: 'mfa.tooltip',
enterpriseSsoLimit: 'enterprise_sso.tooltip',
resourcesLimit: 'api_resources.tooltip',

View file

@ -27,7 +27,7 @@ export const skuQuotaItemOrder: Array<keyof LogtoSkuQuota> = [
'userRolesLimit',
'machineToMachineRolesLimit',
'scopesPerRoleLimit',
'organizationsEnabled',
'organizationsLimit',
'auditLogsRetentionDays',
'hooksLimit',
'customJwtEnabled',

View file

@ -22,7 +22,7 @@ export const skuQuotaItemPhrasesMap: Record<
auditLogsRetentionDays: 'audit_logs_retention_days.name',
ticketSupportResponseTime: 'email_ticket_support.name',
mfaEnabled: 'mfa_enabled.name',
organizationsEnabled: 'organizations_enabled.name',
organizationsLimit: 'organizations_enabled.name',
enterpriseSsoLimit: 'sso_enabled.name',
tenantMembersLimit: 'tenant_members_limit.name',
customJwtEnabled: 'custom_jwt_enabled.name',
@ -49,7 +49,7 @@ export const skuQuotaItemUnlimitedPhrasesMap: Record<
auditLogsRetentionDays: 'audit_logs_retention_days.unlimited',
ticketSupportResponseTime: 'email_ticket_support.unlimited',
mfaEnabled: 'mfa_enabled.unlimited',
organizationsEnabled: 'organizations_enabled.unlimited',
organizationsLimit: 'organizations_enabled.unlimited',
enterpriseSsoLimit: 'sso_enabled.unlimited',
tenantMembersLimit: 'tenant_members_limit.unlimited',
customJwtEnabled: 'custom_jwt_enabled.unlimited',
@ -76,7 +76,7 @@ export const skuQuotaItemLimitedPhrasesMap: Record<
auditLogsRetentionDays: 'audit_logs_retention_days.limited',
ticketSupportResponseTime: 'email_ticket_support.limited',
mfaEnabled: 'mfa_enabled.limited',
organizationsEnabled: 'organizations_enabled.limited',
organizationsLimit: 'organizations_enabled.limited',
enterpriseSsoLimit: 'sso_enabled.limited',
tenantMembersLimit: 'tenant_members_limit.limited',
customJwtEnabled: 'custom_jwt_enabled.limited',
@ -103,7 +103,7 @@ export const skuQuotaItemNotEligiblePhrasesMap: Record<
auditLogsRetentionDays: 'audit_logs_retention_days.not_eligible',
ticketSupportResponseTime: 'email_ticket_support.not_eligible',
mfaEnabled: 'mfa_enabled.not_eligible',
organizationsEnabled: 'organizations_enabled.not_eligible',
organizationsLimit: 'organizations_enabled.not_eligible',
enterpriseSsoLimit: 'sso_enabled.not_eligible',
tenantMembersLimit: 'tenant_members_limit.not_eligible',
customJwtEnabled: 'custom_jwt_enabled.not_eligible',

View file

@ -112,7 +112,7 @@ export const defaultLogtoSku: LogtoSkuResponse = {
hooksLimit: null,
auditLogsRetentionDays: 14,
mfaEnabled: true,
organizationsEnabled: true,
organizationsLimit: null,
enterpriseSsoLimit: null,
thirdPartyApplicationsLimit: null,
tenantMembersLimit: 20,
@ -137,7 +137,7 @@ export const defaultSubscriptionQuota: NewSubscriptionQuota = {
hooksLimit: 1,
auditLogsRetentionDays: 3,
mfaEnabled: false,
organizationsEnabled: false,
organizationsLimit: 0,
enterpriseSsoLimit: 0,
thirdPartyApplicationsLimit: 0,
tenantMembersLimit: 1,
@ -157,7 +157,7 @@ export const defaultSubscriptionUsage: NewSubscriptionCountBasedUsage = {
scopesPerRoleLimit: 0,
hooksLimit: 0,
mfaEnabled: false,
organizationsEnabled: false,
organizationsLimit: 0,
enterpriseSsoLimit: 0,
thirdPartyApplicationsLimit: 0,
tenantMembersLimit: 0,

View file

@ -22,6 +22,7 @@ import TablePlaceholder from '@/ds-components/Table/TablePlaceholder';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import pageLayout from '@/scss/page-layout.module.scss';
import { isFeatureEnabled } from '@/utils/subscription';
import Introduction from '../Organizations/Introduction';
@ -38,7 +39,9 @@ function OrganizationTemplate() {
} = useContext(SubscriptionDataContext);
const { isDevTenant } = useContext(TenantsContext);
const isOrganizationsDisabled =
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
isCloud &&
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
planId !== ReservedPlanId.Pro;
const { navigate } = useTenantPathname();
const handleUpgradePlan = useCallback(() => {

View file

@ -21,7 +21,7 @@ import useApi from '@/hooks/use-api';
import useUserPreferences from '@/hooks/use-user-preferences';
import modalStyles from '@/scss/modal.module.scss';
import { trySubmitSafe } from '@/utils/form';
import { isPaidPlan } from '@/utils/subscription';
import { isPaidPlan, isFeatureEnabled } from '@/utils/subscription';
import styles from './index.module.scss';
@ -42,7 +42,9 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
update,
} = useUserPreferences();
const isOrganizationsDisabled =
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
isCloud &&
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
planId !== ReservedPlanId.Pro;
const {
reset,

View file

@ -15,6 +15,7 @@ import CardTitle from '@/ds-components/CardTitle';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import pageLayout from '@/scss/page-layout.module.scss';
import { isFeatureEnabled } from '@/utils/subscription';
import CreateOrganizationModal from './CreateOrganizationModal';
import OrganizationsTable from './OrganizationsTable';
@ -35,7 +36,9 @@ function Organizations() {
const [isCreating, setIsCreating] = useState(false);
const isOrganizationsDisabled =
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
isCloud &&
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
planId !== ReservedPlanId.Pro;
const upgradePlan = useCallback(() => {
navigate(subscriptionPage);

View file

@ -1,5 +1,5 @@
import { ReservedPlanId } from '@logto/schemas';
import { conditional, trySafe } from '@silverhand/essentials';
import { conditional, trySafe, type Nullable } from '@silverhand/essentials';
import { ResponseError } from '@withtyped/client';
import dayjs from 'dayjs';
@ -98,3 +98,7 @@ export const pickupFeaturedLogtoSkus = (logtoSkus: LogtoSkuResponse[]): LogtoSku
export const isPaidPlan = (planId: string, isEnterprisePlan: boolean) =>
planId === ReservedPlanId.Pro || isEnterprisePlan;
export const isFeatureEnabled = (quota: Nullable<number>): boolean => {
return quota === null || quota > 0;
};

View file

@ -97,7 +97,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@logto/cloud": "0.2.5-20fd0a2",
"@logto/cloud": "0.2.5-91ab76c",
"@silverhand/eslint-config": "6.0.1",
"@silverhand/ts-config": "6.0.0",
"@types/adm-zip": "^0.5.5",

View file

@ -46,9 +46,9 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
ManagementApiRouterContext
>(OrganizationRoles, roles, {
middlewares: condArray(
koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
koaQuotaGuard({ key: 'organizationsLimit', quota, methods: ['POST', 'PUT'] }),
koaReportSubscriptionUpdates({
key: 'organizationsEnabled',
key: 'organizationsLimit',
quota,
methods: ['POST', 'PUT', 'DELETE'],
})

View file

@ -20,9 +20,9 @@ export default function organizationScopeRoutes<T extends ManagementApiRouter>(
) {
const router = new SchemaRouter(OrganizationScopes, scopes, {
middlewares: condArray(
koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
koaQuotaGuard({ key: 'organizationsLimit', quota, methods: ['POST', 'PUT'] }),
koaReportSubscriptionUpdates({
key: 'organizationsEnabled',
key: 'organizationsLimit',
quota,
methods: ['POST', 'PUT', 'DELETE'],
})

View file

@ -31,9 +31,9 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
const router = new SchemaRouter(Organizations, organizations, {
middlewares: condArray(
koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
koaQuotaGuard({ key: 'organizationsLimit', quota, methods: ['POST', 'PUT'] }),
koaReportSubscriptionUpdates({
key: 'organizationsEnabled',
key: 'organizationsLimit',
quota,
methods: ['POST', 'PUT', 'DELETE'],
})

View file

@ -21,27 +21,32 @@ export type Subscription = RouteResponseType<GetRoutes['/api/tenants/:tenantId/s
*/
export type SubscriptionQuota = Omit<
RouteResponseType<GetRoutes['/api/tenants/:tenantId/subscription/quota']>,
'auditLogsRetentionDays'
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the usage keys for now to avoid confusion.
'auditLogsRetentionDays' | 'organizationsEnabled'
>;
/**
* The type of the response of the `GET /api/tenants/:tenantId/subscription/usage` endpoint.
* It is the same as the response type of `GET /api/tenants/my/subscription/usage` endpoint.
*/
export type SubscriptionUsage = RouteResponseType<
GetRoutes['/api/tenants/:tenantId/subscription/usage']
export type SubscriptionUsage = Omit<
RouteResponseType<GetRoutes['/api/tenants/:tenantId/subscription/usage']>,
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the usage keys for now to avoid confusion.
'organizationsEnabled'
>;
export type ReportSubscriptionUpdatesUsageKey = RouteRequestBodyType<
PostRoutes['/api/tenants/my/subscription/item-updates']
>['usageKey'];
export type ReportSubscriptionUpdatesUsageKey = Exclude<
RouteRequestBodyType<PostRoutes['/api/tenants/my/subscription/item-updates']>['usageKey'],
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the usage keys for now to avoid confusion.
'organizationsEnabled'
>;
// Have to manually define this variable since we can only get the literal union from the @logto/cloud/routes module.
export const allReportSubscriptionUpdatesUsageKeys = Object.freeze([
'machineToMachineLimit',
'resourcesLimit',
'mfaEnabled',
'organizationsEnabled',
'organizationsLimit',
'tenantMembersLimit',
'enterpriseSsoLimit',
'hooksLimit',

View file

@ -2568,8 +2568,8 @@ importers:
specifier: ^29.5.0
version: 29.5.0
'@logto/cloud':
specifier: 0.2.5-20fd0a2
version: 0.2.5-20fd0a2(zod@3.23.8)
specifier: 0.2.5-91ab76c
version: 0.2.5-91ab76c(zod@3.23.8)
'@logto/connector-kit':
specifier: workspace:^4.0.0
version: link:../toolkit/connector-kit
@ -3064,8 +3064,8 @@ importers:
version: 3.23.8
devDependencies:
'@logto/cloud':
specifier: 0.2.5-20fd0a2
version: 0.2.5-20fd0a2(zod@3.23.8)
specifier: 0.2.5-91ab76c
version: 0.2.5-91ab76c(zod@3.23.8)
'@silverhand/eslint-config':
specifier: 6.0.1
version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3)
@ -5571,14 +5571,14 @@ packages:
'@logto/client@2.7.2':
resolution: {integrity: sha512-jsmuDl9QpXfR3uLEMPE67tvYoL5XcjJi+4yGqucYPjd4GH6SUHp3N9skk8C/OyygnKDPLY+ttwD0LaIbpGvn+Q==}
'@logto/cloud@0.2.5-20fd0a2':
resolution: {integrity: sha512-j0f2RDpi/OEI59WXKnih7QeFSywNFV91PkulZdmcGa8HCRNmht94siw+LILzheg6bzwfvHU/aN4tJYL1/Px1BA==}
engines: {node: ^20.9.0}
'@logto/cloud@0.2.5-582d792':
resolution: {integrity: sha512-0fIZzqwyjQguTS0a5+XbgVZlGEB/MXIf6pbuBDkHh6JHlMTJ/XH041rWX+e+nMk5N7/Xk2XXS+d2RJUWumnmpw==}
engines: {node: ^20.9.0}
'@logto/cloud@0.2.5-91ab76c':
resolution: {integrity: sha512-t/ZVrFICVxtqw6zh6/OJ+0VYt+fl+waNz77CdAJkhxC91KFMfojm4hWNrx1qTNSTUzg+gc/2p8cbKC9cH1ngeA==}
engines: {node: ^20.9.0}
'@logto/js@4.1.4':
resolution: {integrity: sha512-6twud1nFBQmj89/aflzej6yD1QwXfPiYmRtyYuN4a7O9OaaW3X/kJBVwjKUn5NC9IUt+rd+jXsI3QJXENfaLAw==}
@ -15244,14 +15244,14 @@ snapshots:
camelcase-keys: 7.0.2
jose: 5.6.3
'@logto/cloud@0.2.5-20fd0a2(zod@3.23.8)':
'@logto/cloud@0.2.5-582d792(zod@3.23.8)':
dependencies:
'@silverhand/essentials': 2.9.1
'@withtyped/server': 0.14.0(zod@3.23.8)
transitivePeerDependencies:
- zod
'@logto/cloud@0.2.5-582d792(zod@3.23.8)':
'@logto/cloud@0.2.5-91ab76c(zod@3.23.8)':
dependencies:
'@silverhand/essentials': 2.9.1
'@withtyped/server': 0.14.0(zod@3.23.8)