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:
parent
a6178f45e2
commit
343602027d
17 changed files with 112 additions and 54 deletions
|
@ -27,7 +27,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/roboto-mono": "^5.0.0",
|
"@fontsource/roboto-mono": "^5.0.0",
|
||||||
"@jest/types": "^29.5.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/connector-kit": "workspace:^4.0.0",
|
||||||
"@logto/core-kit": "workspace:^2.5.0",
|
"@logto/core-kit": "workspace:^2.5.0",
|
||||||
"@logto/elements": "workspace:^0.0.0",
|
"@logto/elements": "workspace:^0.0.0",
|
||||||
|
|
|
@ -20,11 +20,23 @@ export type NewSubscriptionUsageResponse = GuardedResponse<
|
||||||
GetRoutes['/api/tenants/:tenantId/subscription-usage']
|
GetRoutes['/api/tenants/:tenantId/subscription-usage']
|
||||||
>;
|
>;
|
||||||
/** The response of `GET /api/tenants/my/subscription/quota` has the same response type. */
|
/** 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. */
|
/** 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 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<
|
export type NewSubscriptionPeriodicUsage = GuardedResponse<
|
||||||
GetRoutes['/api/tenants/:tenantId/subscription/periodic-usage']
|
GetRoutes['/api/tenants/:tenantId/subscription/periodic-usage']
|
||||||
|
|
|
@ -4,7 +4,10 @@ import classNames from 'classnames';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useContext, useMemo } from 'react';
|
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 { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
import DynamicT from '@/ds-components/DynamicT';
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
|
@ -12,12 +15,41 @@ import { formatPeriod } from '@/utils/subscription';
|
||||||
|
|
||||||
import PlanUsageCard, { type Props as PlanUsageCardProps } from './PlanUsageCard';
|
import PlanUsageCard, { type Props as PlanUsageCardProps } from './PlanUsageCard';
|
||||||
import styles from './index.module.scss';
|
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 = {
|
type Props = {
|
||||||
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
|
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) {
|
function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
|
||||||
const {
|
const {
|
||||||
currentSubscriptionQuota,
|
currentSubscriptionQuota,
|
||||||
|
@ -59,10 +91,7 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
|
||||||
isAddOnAvailable || (onlyShowPeriodicUsage && (key === 'mauLimit' || key === 'tokenLimit'))
|
isAddOnAvailable || (onlyShowPeriodicUsage && (key === 'mauLimit' || key === 'tokenLimit'))
|
||||||
)
|
)
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
usage:
|
usage: getUsageByKey(key, { periodicUsage, countBasedUsage: currentSubscriptionUsage }),
|
||||||
key === 'mauLimit' || key === 'tokenLimit'
|
|
||||||
? periodicUsage[key]
|
|
||||||
: currentSubscriptionUsage[key],
|
|
||||||
usageKey: `subscription.usage.${usageKeyMap[key]}`,
|
usageKey: `subscription.usage.${usageKeyMap[key]}`,
|
||||||
titleKey: `subscription.usage.${titleKeyMap[key]}`,
|
titleKey: `subscription.usage.${titleKeyMap[key]}`,
|
||||||
unitPrice: usageKeyPriceMap[key],
|
unitPrice: usageKeyPriceMap[key],
|
||||||
|
|
|
@ -12,10 +12,10 @@ import {
|
||||||
hooksAddOnUnitPrice,
|
hooksAddOnUnitPrice,
|
||||||
} from '@/consts/subscriptions';
|
} from '@/consts/subscriptions';
|
||||||
|
|
||||||
type UsageKey = Pick<
|
export type UsageKey = Pick<
|
||||||
NewSubscriptionQuota,
|
NewSubscriptionQuota,
|
||||||
| 'mauLimit'
|
| 'mauLimit'
|
||||||
| 'organizationsEnabled'
|
| 'organizationsLimit'
|
||||||
| 'mfaEnabled'
|
| 'mfaEnabled'
|
||||||
| 'enterpriseSsoLimit'
|
| 'enterpriseSsoLimit'
|
||||||
| 'resourcesLimit'
|
| 'resourcesLimit'
|
||||||
|
@ -28,7 +28,7 @@ type UsageKey = Pick<
|
||||||
// We decide not to show `hooksLimit` usage in console for now.
|
// We decide not to show `hooksLimit` usage in console for now.
|
||||||
export const usageKeys: Array<keyof UsageKey> = [
|
export const usageKeys: Array<keyof UsageKey> = [
|
||||||
'mauLimit',
|
'mauLimit',
|
||||||
'organizationsEnabled',
|
'organizationsLimit',
|
||||||
'mfaEnabled',
|
'mfaEnabled',
|
||||||
'enterpriseSsoLimit',
|
'enterpriseSsoLimit',
|
||||||
'resourcesLimit',
|
'resourcesLimit',
|
||||||
|
@ -39,7 +39,7 @@ export const usageKeys: Array<keyof UsageKey> = [
|
||||||
|
|
||||||
export const usageKeyPriceMap: Record<keyof UsageKey, number> = {
|
export const usageKeyPriceMap: Record<keyof UsageKey, number> = {
|
||||||
mauLimit: 0,
|
mauLimit: 0,
|
||||||
organizationsEnabled: organizationAddOnUnitPrice,
|
organizationsLimit: organizationAddOnUnitPrice,
|
||||||
mfaEnabled: mfaAddOnUnitPrice,
|
mfaEnabled: mfaAddOnUnitPrice,
|
||||||
enterpriseSsoLimit: enterpriseSsoAddOnUnitPrice,
|
enterpriseSsoLimit: enterpriseSsoAddOnUnitPrice,
|
||||||
resourcesLimit: resourceAddOnUnitPrice,
|
resourcesLimit: resourceAddOnUnitPrice,
|
||||||
|
@ -54,7 +54,7 @@ export const usageKeyMap: Record<
|
||||||
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
||||||
> = {
|
> = {
|
||||||
mauLimit: 'mau.description',
|
mauLimit: 'mau.description',
|
||||||
organizationsEnabled: 'organizations.description',
|
organizationsLimit: 'organizations.description',
|
||||||
mfaEnabled: 'mfa.description',
|
mfaEnabled: 'mfa.description',
|
||||||
enterpriseSsoLimit: 'enterprise_sso.description',
|
enterpriseSsoLimit: 'enterprise_sso.description',
|
||||||
resourcesLimit: 'api_resources.description',
|
resourcesLimit: 'api_resources.description',
|
||||||
|
@ -69,7 +69,7 @@ export const titleKeyMap: Record<
|
||||||
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
||||||
> = {
|
> = {
|
||||||
mauLimit: 'mau.title',
|
mauLimit: 'mau.title',
|
||||||
organizationsEnabled: 'organizations.title',
|
organizationsLimit: 'organizations.title',
|
||||||
mfaEnabled: 'mfa.title',
|
mfaEnabled: 'mfa.title',
|
||||||
enterpriseSsoLimit: 'enterprise_sso.title',
|
enterpriseSsoLimit: 'enterprise_sso.title',
|
||||||
resourcesLimit: 'api_resources.title',
|
resourcesLimit: 'api_resources.title',
|
||||||
|
@ -84,7 +84,7 @@ export const tooltipKeyMap: Record<
|
||||||
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
||||||
> = {
|
> = {
|
||||||
mauLimit: 'mau.tooltip',
|
mauLimit: 'mau.tooltip',
|
||||||
organizationsEnabled: 'organizations.tooltip',
|
organizationsLimit: 'organizations.tooltip',
|
||||||
mfaEnabled: 'mfa.tooltip',
|
mfaEnabled: 'mfa.tooltip',
|
||||||
enterpriseSsoLimit: 'enterprise_sso.tooltip',
|
enterpriseSsoLimit: 'enterprise_sso.tooltip',
|
||||||
resourcesLimit: 'api_resources.tooltip',
|
resourcesLimit: 'api_resources.tooltip',
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const skuQuotaItemOrder: Array<keyof LogtoSkuQuota> = [
|
||||||
'userRolesLimit',
|
'userRolesLimit',
|
||||||
'machineToMachineRolesLimit',
|
'machineToMachineRolesLimit',
|
||||||
'scopesPerRoleLimit',
|
'scopesPerRoleLimit',
|
||||||
'organizationsEnabled',
|
'organizationsLimit',
|
||||||
'auditLogsRetentionDays',
|
'auditLogsRetentionDays',
|
||||||
'hooksLimit',
|
'hooksLimit',
|
||||||
'customJwtEnabled',
|
'customJwtEnabled',
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const skuQuotaItemPhrasesMap: Record<
|
||||||
auditLogsRetentionDays: 'audit_logs_retention_days.name',
|
auditLogsRetentionDays: 'audit_logs_retention_days.name',
|
||||||
ticketSupportResponseTime: 'email_ticket_support.name',
|
ticketSupportResponseTime: 'email_ticket_support.name',
|
||||||
mfaEnabled: 'mfa_enabled.name',
|
mfaEnabled: 'mfa_enabled.name',
|
||||||
organizationsEnabled: 'organizations_enabled.name',
|
organizationsLimit: 'organizations_enabled.name',
|
||||||
enterpriseSsoLimit: 'sso_enabled.name',
|
enterpriseSsoLimit: 'sso_enabled.name',
|
||||||
tenantMembersLimit: 'tenant_members_limit.name',
|
tenantMembersLimit: 'tenant_members_limit.name',
|
||||||
customJwtEnabled: 'custom_jwt_enabled.name',
|
customJwtEnabled: 'custom_jwt_enabled.name',
|
||||||
|
@ -49,7 +49,7 @@ export const skuQuotaItemUnlimitedPhrasesMap: Record<
|
||||||
auditLogsRetentionDays: 'audit_logs_retention_days.unlimited',
|
auditLogsRetentionDays: 'audit_logs_retention_days.unlimited',
|
||||||
ticketSupportResponseTime: 'email_ticket_support.unlimited',
|
ticketSupportResponseTime: 'email_ticket_support.unlimited',
|
||||||
mfaEnabled: 'mfa_enabled.unlimited',
|
mfaEnabled: 'mfa_enabled.unlimited',
|
||||||
organizationsEnabled: 'organizations_enabled.unlimited',
|
organizationsLimit: 'organizations_enabled.unlimited',
|
||||||
enterpriseSsoLimit: 'sso_enabled.unlimited',
|
enterpriseSsoLimit: 'sso_enabled.unlimited',
|
||||||
tenantMembersLimit: 'tenant_members_limit.unlimited',
|
tenantMembersLimit: 'tenant_members_limit.unlimited',
|
||||||
customJwtEnabled: 'custom_jwt_enabled.unlimited',
|
customJwtEnabled: 'custom_jwt_enabled.unlimited',
|
||||||
|
@ -76,7 +76,7 @@ export const skuQuotaItemLimitedPhrasesMap: Record<
|
||||||
auditLogsRetentionDays: 'audit_logs_retention_days.limited',
|
auditLogsRetentionDays: 'audit_logs_retention_days.limited',
|
||||||
ticketSupportResponseTime: 'email_ticket_support.limited',
|
ticketSupportResponseTime: 'email_ticket_support.limited',
|
||||||
mfaEnabled: 'mfa_enabled.limited',
|
mfaEnabled: 'mfa_enabled.limited',
|
||||||
organizationsEnabled: 'organizations_enabled.limited',
|
organizationsLimit: 'organizations_enabled.limited',
|
||||||
enterpriseSsoLimit: 'sso_enabled.limited',
|
enterpriseSsoLimit: 'sso_enabled.limited',
|
||||||
tenantMembersLimit: 'tenant_members_limit.limited',
|
tenantMembersLimit: 'tenant_members_limit.limited',
|
||||||
customJwtEnabled: 'custom_jwt_enabled.limited',
|
customJwtEnabled: 'custom_jwt_enabled.limited',
|
||||||
|
@ -103,7 +103,7 @@ export const skuQuotaItemNotEligiblePhrasesMap: Record<
|
||||||
auditLogsRetentionDays: 'audit_logs_retention_days.not_eligible',
|
auditLogsRetentionDays: 'audit_logs_retention_days.not_eligible',
|
||||||
ticketSupportResponseTime: 'email_ticket_support.not_eligible',
|
ticketSupportResponseTime: 'email_ticket_support.not_eligible',
|
||||||
mfaEnabled: 'mfa_enabled.not_eligible',
|
mfaEnabled: 'mfa_enabled.not_eligible',
|
||||||
organizationsEnabled: 'organizations_enabled.not_eligible',
|
organizationsLimit: 'organizations_enabled.not_eligible',
|
||||||
enterpriseSsoLimit: 'sso_enabled.not_eligible',
|
enterpriseSsoLimit: 'sso_enabled.not_eligible',
|
||||||
tenantMembersLimit: 'tenant_members_limit.not_eligible',
|
tenantMembersLimit: 'tenant_members_limit.not_eligible',
|
||||||
customJwtEnabled: 'custom_jwt_enabled.not_eligible',
|
customJwtEnabled: 'custom_jwt_enabled.not_eligible',
|
||||||
|
|
|
@ -112,7 +112,7 @@ export const defaultLogtoSku: LogtoSkuResponse = {
|
||||||
hooksLimit: null,
|
hooksLimit: null,
|
||||||
auditLogsRetentionDays: 14,
|
auditLogsRetentionDays: 14,
|
||||||
mfaEnabled: true,
|
mfaEnabled: true,
|
||||||
organizationsEnabled: true,
|
organizationsLimit: null,
|
||||||
enterpriseSsoLimit: null,
|
enterpriseSsoLimit: null,
|
||||||
thirdPartyApplicationsLimit: null,
|
thirdPartyApplicationsLimit: null,
|
||||||
tenantMembersLimit: 20,
|
tenantMembersLimit: 20,
|
||||||
|
@ -137,7 +137,7 @@ export const defaultSubscriptionQuota: NewSubscriptionQuota = {
|
||||||
hooksLimit: 1,
|
hooksLimit: 1,
|
||||||
auditLogsRetentionDays: 3,
|
auditLogsRetentionDays: 3,
|
||||||
mfaEnabled: false,
|
mfaEnabled: false,
|
||||||
organizationsEnabled: false,
|
organizationsLimit: 0,
|
||||||
enterpriseSsoLimit: 0,
|
enterpriseSsoLimit: 0,
|
||||||
thirdPartyApplicationsLimit: 0,
|
thirdPartyApplicationsLimit: 0,
|
||||||
tenantMembersLimit: 1,
|
tenantMembersLimit: 1,
|
||||||
|
@ -157,7 +157,7 @@ export const defaultSubscriptionUsage: NewSubscriptionCountBasedUsage = {
|
||||||
scopesPerRoleLimit: 0,
|
scopesPerRoleLimit: 0,
|
||||||
hooksLimit: 0,
|
hooksLimit: 0,
|
||||||
mfaEnabled: false,
|
mfaEnabled: false,
|
||||||
organizationsEnabled: false,
|
organizationsLimit: 0,
|
||||||
enterpriseSsoLimit: 0,
|
enterpriseSsoLimit: 0,
|
||||||
thirdPartyApplicationsLimit: 0,
|
thirdPartyApplicationsLimit: 0,
|
||||||
tenantMembersLimit: 0,
|
tenantMembersLimit: 0,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import TablePlaceholder from '@/ds-components/Table/TablePlaceholder';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||||
import pageLayout from '@/scss/page-layout.module.scss';
|
import pageLayout from '@/scss/page-layout.module.scss';
|
||||||
|
import { isFeatureEnabled } from '@/utils/subscription';
|
||||||
|
|
||||||
import Introduction from '../Organizations/Introduction';
|
import Introduction from '../Organizations/Introduction';
|
||||||
|
|
||||||
|
@ -38,7 +39,9 @@ function OrganizationTemplate() {
|
||||||
} = useContext(SubscriptionDataContext);
|
} = useContext(SubscriptionDataContext);
|
||||||
const { isDevTenant } = useContext(TenantsContext);
|
const { isDevTenant } = useContext(TenantsContext);
|
||||||
const isOrganizationsDisabled =
|
const isOrganizationsDisabled =
|
||||||
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
|
isCloud &&
|
||||||
|
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
|
||||||
|
planId !== ReservedPlanId.Pro;
|
||||||
const { navigate } = useTenantPathname();
|
const { navigate } = useTenantPathname();
|
||||||
|
|
||||||
const handleUpgradePlan = useCallback(() => {
|
const handleUpgradePlan = useCallback(() => {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import useApi from '@/hooks/use-api';
|
||||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||||
import modalStyles from '@/scss/modal.module.scss';
|
import modalStyles from '@/scss/modal.module.scss';
|
||||||
import { trySubmitSafe } from '@/utils/form';
|
import { trySubmitSafe } from '@/utils/form';
|
||||||
import { isPaidPlan } from '@/utils/subscription';
|
import { isPaidPlan, isFeatureEnabled } from '@/utils/subscription';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
@ -42,7 +42,9 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
||||||
update,
|
update,
|
||||||
} = useUserPreferences();
|
} = useUserPreferences();
|
||||||
const isOrganizationsDisabled =
|
const isOrganizationsDisabled =
|
||||||
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
|
isCloud &&
|
||||||
|
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
|
||||||
|
planId !== ReservedPlanId.Pro;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reset,
|
reset,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import CardTitle from '@/ds-components/CardTitle';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||||
import pageLayout from '@/scss/page-layout.module.scss';
|
import pageLayout from '@/scss/page-layout.module.scss';
|
||||||
|
import { isFeatureEnabled } from '@/utils/subscription';
|
||||||
|
|
||||||
import CreateOrganizationModal from './CreateOrganizationModal';
|
import CreateOrganizationModal from './CreateOrganizationModal';
|
||||||
import OrganizationsTable from './OrganizationsTable';
|
import OrganizationsTable from './OrganizationsTable';
|
||||||
|
@ -35,7 +36,9 @@ function Organizations() {
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
|
||||||
const isOrganizationsDisabled =
|
const isOrganizationsDisabled =
|
||||||
isCloud && !currentSubscriptionQuota.organizationsEnabled && planId !== ReservedPlanId.Pro;
|
isCloud &&
|
||||||
|
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
|
||||||
|
planId !== ReservedPlanId.Pro;
|
||||||
|
|
||||||
const upgradePlan = useCallback(() => {
|
const upgradePlan = useCallback(() => {
|
||||||
navigate(subscriptionPage);
|
navigate(subscriptionPage);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ReservedPlanId } from '@logto/schemas';
|
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 { ResponseError } from '@withtyped/client';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
@ -98,3 +98,7 @@ export const pickupFeaturedLogtoSkus = (logtoSkus: LogtoSkuResponse[]): LogtoSku
|
||||||
|
|
||||||
export const isPaidPlan = (planId: string, isEnterprisePlan: boolean) =>
|
export const isPaidPlan = (planId: string, isEnterprisePlan: boolean) =>
|
||||||
planId === ReservedPlanId.Pro || isEnterprisePlan;
|
planId === ReservedPlanId.Pro || isEnterprisePlan;
|
||||||
|
|
||||||
|
export const isFeatureEnabled = (quota: Nullable<number>): boolean => {
|
||||||
|
return quota === null || quota > 0;
|
||||||
|
};
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@logto/cloud": "0.2.5-20fd0a2",
|
"@logto/cloud": "0.2.5-91ab76c",
|
||||||
"@silverhand/eslint-config": "6.0.1",
|
"@silverhand/eslint-config": "6.0.1",
|
||||||
"@silverhand/ts-config": "6.0.0",
|
"@silverhand/ts-config": "6.0.0",
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
|
|
|
@ -46,9 +46,9 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
|
||||||
ManagementApiRouterContext
|
ManagementApiRouterContext
|
||||||
>(OrganizationRoles, roles, {
|
>(OrganizationRoles, roles, {
|
||||||
middlewares: condArray(
|
middlewares: condArray(
|
||||||
koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
koaQuotaGuard({ key: 'organizationsLimit', quota, methods: ['POST', 'PUT'] }),
|
||||||
koaReportSubscriptionUpdates({
|
koaReportSubscriptionUpdates({
|
||||||
key: 'organizationsEnabled',
|
key: 'organizationsLimit',
|
||||||
quota,
|
quota,
|
||||||
methods: ['POST', 'PUT', 'DELETE'],
|
methods: ['POST', 'PUT', 'DELETE'],
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,9 +20,9 @@ export default function organizationScopeRoutes<T extends ManagementApiRouter>(
|
||||||
) {
|
) {
|
||||||
const router = new SchemaRouter(OrganizationScopes, scopes, {
|
const router = new SchemaRouter(OrganizationScopes, scopes, {
|
||||||
middlewares: condArray(
|
middlewares: condArray(
|
||||||
koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
koaQuotaGuard({ key: 'organizationsLimit', quota, methods: ['POST', 'PUT'] }),
|
||||||
koaReportSubscriptionUpdates({
|
koaReportSubscriptionUpdates({
|
||||||
key: 'organizationsEnabled',
|
key: 'organizationsLimit',
|
||||||
quota,
|
quota,
|
||||||
methods: ['POST', 'PUT', 'DELETE'],
|
methods: ['POST', 'PUT', 'DELETE'],
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,9 +31,9 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
const router = new SchemaRouter(Organizations, organizations, {
|
const router = new SchemaRouter(Organizations, organizations, {
|
||||||
middlewares: condArray(
|
middlewares: condArray(
|
||||||
koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
koaQuotaGuard({ key: 'organizationsLimit', quota, methods: ['POST', 'PUT'] }),
|
||||||
koaReportSubscriptionUpdates({
|
koaReportSubscriptionUpdates({
|
||||||
key: 'organizationsEnabled',
|
key: 'organizationsLimit',
|
||||||
quota,
|
quota,
|
||||||
methods: ['POST', 'PUT', 'DELETE'],
|
methods: ['POST', 'PUT', 'DELETE'],
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,27 +21,32 @@ export type Subscription = RouteResponseType<GetRoutes['/api/tenants/:tenantId/s
|
||||||
*/
|
*/
|
||||||
export type SubscriptionQuota = Omit<
|
export type SubscriptionQuota = Omit<
|
||||||
RouteResponseType<GetRoutes['/api/tenants/:tenantId/subscription/quota']>,
|
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.
|
* 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.
|
* It is the same as the response type of `GET /api/tenants/my/subscription/usage` endpoint.
|
||||||
*/
|
*/
|
||||||
export type SubscriptionUsage = RouteResponseType<
|
export type SubscriptionUsage = Omit<
|
||||||
GetRoutes['/api/tenants/:tenantId/subscription/usage']
|
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<
|
export type ReportSubscriptionUpdatesUsageKey = Exclude<
|
||||||
PostRoutes['/api/tenants/my/subscription/item-updates']
|
RouteRequestBodyType<PostRoutes['/api/tenants/my/subscription/item-updates']>['usageKey'],
|
||||||
>['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.
|
// 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([
|
export const allReportSubscriptionUpdatesUsageKeys = Object.freeze([
|
||||||
'machineToMachineLimit',
|
'machineToMachineLimit',
|
||||||
'resourcesLimit',
|
'resourcesLimit',
|
||||||
'mfaEnabled',
|
'mfaEnabled',
|
||||||
'organizationsEnabled',
|
'organizationsLimit',
|
||||||
'tenantMembersLimit',
|
'tenantMembersLimit',
|
||||||
'enterpriseSsoLimit',
|
'enterpriseSsoLimit',
|
||||||
'hooksLimit',
|
'hooksLimit',
|
||||||
|
|
|
@ -2568,8 +2568,8 @@ importers:
|
||||||
specifier: ^29.5.0
|
specifier: ^29.5.0
|
||||||
version: 29.5.0
|
version: 29.5.0
|
||||||
'@logto/cloud':
|
'@logto/cloud':
|
||||||
specifier: 0.2.5-20fd0a2
|
specifier: 0.2.5-91ab76c
|
||||||
version: 0.2.5-20fd0a2(zod@3.23.8)
|
version: 0.2.5-91ab76c(zod@3.23.8)
|
||||||
'@logto/connector-kit':
|
'@logto/connector-kit':
|
||||||
specifier: workspace:^4.0.0
|
specifier: workspace:^4.0.0
|
||||||
version: link:../toolkit/connector-kit
|
version: link:../toolkit/connector-kit
|
||||||
|
@ -3064,8 +3064,8 @@ importers:
|
||||||
version: 3.23.8
|
version: 3.23.8
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@logto/cloud':
|
'@logto/cloud':
|
||||||
specifier: 0.2.5-20fd0a2
|
specifier: 0.2.5-91ab76c
|
||||||
version: 0.2.5-20fd0a2(zod@3.23.8)
|
version: 0.2.5-91ab76c(zod@3.23.8)
|
||||||
'@silverhand/eslint-config':
|
'@silverhand/eslint-config':
|
||||||
specifier: 6.0.1
|
specifier: 6.0.1
|
||||||
version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3)
|
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':
|
'@logto/client@2.7.2':
|
||||||
resolution: {integrity: sha512-jsmuDl9QpXfR3uLEMPE67tvYoL5XcjJi+4yGqucYPjd4GH6SUHp3N9skk8C/OyygnKDPLY+ttwD0LaIbpGvn+Q==}
|
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':
|
'@logto/cloud@0.2.5-582d792':
|
||||||
resolution: {integrity: sha512-0fIZzqwyjQguTS0a5+XbgVZlGEB/MXIf6pbuBDkHh6JHlMTJ/XH041rWX+e+nMk5N7/Xk2XXS+d2RJUWumnmpw==}
|
resolution: {integrity: sha512-0fIZzqwyjQguTS0a5+XbgVZlGEB/MXIf6pbuBDkHh6JHlMTJ/XH041rWX+e+nMk5N7/Xk2XXS+d2RJUWumnmpw==}
|
||||||
engines: {node: ^20.9.0}
|
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':
|
'@logto/js@4.1.4':
|
||||||
resolution: {integrity: sha512-6twud1nFBQmj89/aflzej6yD1QwXfPiYmRtyYuN4a7O9OaaW3X/kJBVwjKUn5NC9IUt+rd+jXsI3QJXENfaLAw==}
|
resolution: {integrity: sha512-6twud1nFBQmj89/aflzej6yD1QwXfPiYmRtyYuN4a7O9OaaW3X/kJBVwjKUn5NC9IUt+rd+jXsI3QJXENfaLAw==}
|
||||||
|
|
||||||
|
@ -15244,14 +15244,14 @@ snapshots:
|
||||||
camelcase-keys: 7.0.2
|
camelcase-keys: 7.0.2
|
||||||
jose: 5.6.3
|
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:
|
dependencies:
|
||||||
'@silverhand/essentials': 2.9.1
|
'@silverhand/essentials': 2.9.1
|
||||||
'@withtyped/server': 0.14.0(zod@3.23.8)
|
'@withtyped/server': 0.14.0(zod@3.23.8)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@logto/cloud@0.2.5-582d792(zod@3.23.8)':
|
'@logto/cloud@0.2.5-91ab76c(zod@3.23.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@silverhand/essentials': 2.9.1
|
'@silverhand/essentials': 2.9.1
|
||||||
'@withtyped/server': 0.14.0(zod@3.23.8)
|
'@withtyped/server': 0.14.0(zod@3.23.8)
|
||||||
|
|
Loading…
Reference in a new issue