mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(core): merge subscription usage API request (#6427)
* refactor(core): merge subscription usage API request merge subscription usage API request * fix(core): fix type issue fix type issue
This commit is contained in:
parent
ea70e09ad8
commit
758d270f7c
14 changed files with 77 additions and 193 deletions
|
@ -52,7 +52,7 @@
|
|||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/cloud": "0.2.5-3b703da",
|
||||
"@logto/cloud": "0.2.5-3452c56",
|
||||
"@silverhand/eslint-config": "6.0.1",
|
||||
"@silverhand/ts-config": "6.0.0",
|
||||
"@types/node": "^20.11.20",
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"devDependencies": {
|
||||
"@fontsource/roboto-mono": "^5.0.0",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@logto/cloud": "0.2.5-923c26f",
|
||||
"@logto/cloud": "0.2.5-3452c56",
|
||||
"@logto/connector-kit": "workspace:^4.0.0",
|
||||
"@logto/core-kit": "workspace:^2.5.0",
|
||||
"@logto/elements": "workspace:^0.0.0",
|
||||
|
|
|
@ -19,20 +19,16 @@ export type Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/sub
|
|||
export type SubscriptionUsage = GuardedResponse<GetRoutes['/api/tenants/:tenantId/usage']>;
|
||||
|
||||
/* ===== Use `New` in the naming to avoid confusion with legacy types ===== */
|
||||
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 = GuardedResponse<
|
||||
GetRoutes['/api/tenants/:tenantId/subscription/quota']
|
||||
>;
|
||||
|
||||
export type NewSubscriptionQuota = NewSubscriptionUsageResponse['quota'];
|
||||
/** The response of `GET /api/tenants/my/subscription/usage` has the same response type. */
|
||||
export type NewSubscriptionUsage = GuardedResponse<
|
||||
GetRoutes['/api/tenants/:tenantId/subscription/usage']
|
||||
>;
|
||||
export type NewSubscriptionUsage = NewSubscriptionUsageResponse['usage'];
|
||||
export type NewSubscriptionResourceScopeUsage = NewSubscriptionUsageResponse['resources'];
|
||||
export type NewSubscriptionRoleScopeUsage = NewSubscriptionUsageResponse['roles'];
|
||||
|
||||
/** The response of `GET /api/tenants/my/subscription/usage/:entityName/scopes` has the same response type. */
|
||||
export type NewSubscriptionScopeUsage = GuardedResponse<
|
||||
GetRoutes['/api/tenants/:tenantId/subscription/usage/:entityName/scopes']
|
||||
>;
|
||||
/* ===== Use `New` in the naming to avoid confusion with legacy types ===== */
|
||||
|
||||
export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>;
|
||||
|
|
|
@ -30,8 +30,8 @@ export const SubscriptionDataContext = createContext<FullContext>({
|
|||
currentSku: defaultLogtoSku,
|
||||
currentSubscriptionQuota: defaultSubscriptionQuota,
|
||||
currentSubscriptionUsage: defaultSubscriptionUsage,
|
||||
currentSubscriptionScopeResourceUsage: {},
|
||||
currentSubscriptionScopeRoleUsage: {},
|
||||
currentSubscriptionResourceScopeUsage: {},
|
||||
currentSubscriptionRoleScopeUsage: {},
|
||||
mutateSubscriptionQuotaAndUsages: noop,
|
||||
/* ==== For new pricing model ==== */
|
||||
});
|
||||
|
|
|
@ -3,7 +3,8 @@ import {
|
|||
type Subscription,
|
||||
type NewSubscriptionQuota,
|
||||
type NewSubscriptionUsage,
|
||||
type NewSubscriptionScopeUsage,
|
||||
type NewSubscriptionResourceScopeUsage,
|
||||
type NewSubscriptionRoleScopeUsage,
|
||||
} from '@/cloud/types/router';
|
||||
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||
|
||||
|
@ -21,8 +22,8 @@ type NewSubscriptionSupplementContext = {
|
|||
currentSku: LogtoSkuResponse;
|
||||
currentSubscriptionQuota: NewSubscriptionQuota;
|
||||
currentSubscriptionUsage: NewSubscriptionUsage;
|
||||
currentSubscriptionScopeResourceUsage: NewSubscriptionScopeUsage;
|
||||
currentSubscriptionScopeRoleUsage: NewSubscriptionScopeUsage;
|
||||
currentSubscriptionResourceScopeUsage: NewSubscriptionResourceScopeUsage;
|
||||
currentSubscriptionRoleScopeUsage: NewSubscriptionRoleScopeUsage;
|
||||
mutateSubscriptionQuotaAndUsages: () => void;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { cond, condString } from '@silverhand/essentials';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type NewSubscriptionUsageResponse } from '@/cloud/types/router';
|
||||
import {
|
||||
defaultLogtoSku,
|
||||
defaultTenantResponse,
|
||||
|
@ -10,63 +13,35 @@ import {
|
|||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import useLogtoSkus from '@/hooks/use-logto-skus';
|
||||
import useNewSubscriptionQuota from '@/hooks/use-new-subscription-quota';
|
||||
import useNewSubscriptionScopeUsage from '@/hooks/use-new-subscription-scopes-usage';
|
||||
import useNewSubscriptionUsage from '@/hooks/use-new-subscription-usage';
|
||||
|
||||
import useSubscription from '../../hooks/use-subscription';
|
||||
|
||||
import { type NewSubscriptionContext } from './types';
|
||||
|
||||
const useNewSubscriptionData: () => NewSubscriptionContext & { isLoading: boolean } = () => {
|
||||
const cloudApi = useCloudApi();
|
||||
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
const { isLoading: isLogtoSkusLoading, data: fetchedLogtoSkus } = useLogtoSkus();
|
||||
const tenantId = condString(currentTenant?.id);
|
||||
|
||||
const {
|
||||
data: currentSubscription,
|
||||
isLoading: isSubscriptionLoading,
|
||||
mutate: mutateSubscription,
|
||||
} = useSubscription(condString(currentTenant?.id));
|
||||
} = useSubscription(tenantId);
|
||||
|
||||
const {
|
||||
data: currentSubscriptionQuota,
|
||||
isLoading: isSubscriptionQuotaLoading,
|
||||
mutate: mutateSubscriptionQuota,
|
||||
} = useNewSubscriptionQuota(condString(currentTenant?.id));
|
||||
|
||||
const {
|
||||
data: currentSubscriptionUsage,
|
||||
isLoading: isSubscriptionUsageLoading,
|
||||
mutate: mutateSubscriptionUsage,
|
||||
} = useNewSubscriptionUsage(condString(currentTenant?.id));
|
||||
|
||||
const {
|
||||
scopeResourceUsage: {
|
||||
data: scopeResourceUsage,
|
||||
isLoading: isScopePerResourceUsageLoading,
|
||||
mutate: mutateScopeResourceUsage,
|
||||
},
|
||||
scopeRoleUsage: {
|
||||
data: scopeRoleUsage,
|
||||
isLoading: isScopePerRoleUsageLoading,
|
||||
mutate: mutateScopeRoleUsage,
|
||||
},
|
||||
} = useNewSubscriptionScopeUsage(condString(currentTenant?.id));
|
||||
|
||||
const mutateSubscriptionQuotaAndUsages = useCallback(() => {
|
||||
if (!isDevFeaturesEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
void mutateSubscriptionQuota();
|
||||
void mutateSubscriptionUsage();
|
||||
void mutateScopeResourceUsage();
|
||||
void mutateScopeRoleUsage();
|
||||
}, [
|
||||
mutateScopeResourceUsage,
|
||||
mutateScopeRoleUsage,
|
||||
mutateSubscriptionQuota,
|
||||
mutateSubscriptionUsage,
|
||||
]);
|
||||
data: subscriptionUsageData,
|
||||
isLoading: isSubscriptionUsageDataLoading,
|
||||
mutate: mutateSubscriptionQuotaAndUsages,
|
||||
} = useSWR<NewSubscriptionUsageResponse, Error>(
|
||||
isCloud && isDevFeaturesEnabled && tenantId && `/api/tenants/${tenantId}/subscription-usage`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/subscription-usage', {
|
||||
params: { tenantId },
|
||||
})
|
||||
);
|
||||
|
||||
const logtoSkus = useMemo(() => cond(isCloud && fetchedLogtoSkus) ?? [], [fetchedLogtoSkus]);
|
||||
|
||||
|
@ -75,24 +50,34 @@ const useNewSubscriptionData: () => NewSubscriptionContext & { isLoading: boolea
|
|||
[currentTenant?.planId, logtoSkus]
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading:
|
||||
isSubscriptionLoading ||
|
||||
isLogtoSkusLoading ||
|
||||
isSubscriptionQuotaLoading ||
|
||||
isSubscriptionUsageLoading ||
|
||||
isScopePerResourceUsageLoading ||
|
||||
isScopePerRoleUsageLoading,
|
||||
logtoSkus,
|
||||
currentSku,
|
||||
currentSubscription: currentSubscription ?? defaultTenantResponse.subscription,
|
||||
onCurrentSubscriptionUpdated: mutateSubscription,
|
||||
mutateSubscriptionQuotaAndUsages,
|
||||
currentSubscriptionQuota: currentSubscriptionQuota ?? defaultSubscriptionQuota,
|
||||
currentSubscriptionUsage: currentSubscriptionUsage ?? defaultSubscriptionUsage,
|
||||
currentSubscriptionScopeResourceUsage: scopeResourceUsage ?? {},
|
||||
currentSubscriptionScopeRoleUsage: scopeRoleUsage ?? {},
|
||||
};
|
||||
return useMemo(
|
||||
() => ({
|
||||
isLoading: isSubscriptionLoading || isLogtoSkusLoading || isSubscriptionUsageDataLoading,
|
||||
logtoSkus,
|
||||
currentSku,
|
||||
currentSubscription: currentSubscription ?? defaultTenantResponse.subscription,
|
||||
onCurrentSubscriptionUpdated: mutateSubscription,
|
||||
mutateSubscriptionQuotaAndUsages,
|
||||
currentSubscriptionQuota: subscriptionUsageData?.quota ?? defaultSubscriptionQuota,
|
||||
currentSubscriptionUsage: subscriptionUsageData?.usage ?? defaultSubscriptionUsage,
|
||||
currentSubscriptionResourceScopeUsage: subscriptionUsageData?.resources ?? {},
|
||||
currentSubscriptionRoleScopeUsage: subscriptionUsageData?.roles ?? {},
|
||||
}),
|
||||
[
|
||||
currentSku,
|
||||
currentSubscription,
|
||||
isLogtoSkusLoading,
|
||||
isSubscriptionLoading,
|
||||
isSubscriptionUsageDataLoading,
|
||||
logtoSkus,
|
||||
mutateSubscription,
|
||||
mutateSubscriptionQuotaAndUsages,
|
||||
subscriptionUsageData?.quota,
|
||||
subscriptionUsageData?.resources,
|
||||
subscriptionUsageData?.roles,
|
||||
subscriptionUsageData?.usage,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
export default useNewSubscriptionData;
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import useSWR from 'swr';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type NewSubscriptionQuota } from '@/cloud/types/router';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
|
||||
const useNewSubscriptionQuota = (tenantId: string) => {
|
||||
const cloudApi = useCloudApi();
|
||||
|
||||
return useSWR<NewSubscriptionQuota, Error>(
|
||||
isCloud && isDevFeaturesEnabled && tenantId && `/api/tenants/${tenantId}/subscription/quota`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/subscription/quota', {
|
||||
params: { tenantId },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export default useNewSubscriptionQuota;
|
|
@ -1,39 +0,0 @@
|
|||
import useSWR from 'swr';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type NewSubscriptionScopeUsage } from '@/cloud/types/router';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
|
||||
const useNewSubscriptionScopeUsage = (tenantId: string) => {
|
||||
const cloudApi = useCloudApi();
|
||||
|
||||
const resourceEntityName = 'resources';
|
||||
const roleEntityName = 'roles';
|
||||
|
||||
return {
|
||||
scopeResourceUsage: useSWR<NewSubscriptionScopeUsage, Error>(
|
||||
isCloud &&
|
||||
isDevFeaturesEnabled &&
|
||||
tenantId &&
|
||||
`/api/tenants/${tenantId}/subscription/usage/${resourceEntityName}/scopes`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/subscription/usage/:entityName/scopes', {
|
||||
params: { tenantId, entityName: resourceEntityName },
|
||||
search: {},
|
||||
})
|
||||
),
|
||||
scopeRoleUsage: useSWR<NewSubscriptionScopeUsage, Error>(
|
||||
isCloud &&
|
||||
isDevFeaturesEnabled &&
|
||||
tenantId &&
|
||||
`/api/tenants/${tenantId}/subscription/usage/${roleEntityName}/scopes`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/subscription/usage/:entityName/scopes', {
|
||||
params: { tenantId, entityName: roleEntityName },
|
||||
search: {},
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export default useNewSubscriptionScopeUsage;
|
|
@ -1,19 +0,0 @@
|
|||
import useSWR from 'swr';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type NewSubscriptionUsage } from '@/cloud/types/router';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
|
||||
const useNewSubscriptionUsage = (tenantId: string) => {
|
||||
const cloudApi = useCloudApi();
|
||||
|
||||
return useSWR<NewSubscriptionUsage, Error>(
|
||||
isCloud && isDevFeaturesEnabled && tenantId && `/api/tenants/${tenantId}/subscription/usage`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/subscription/usage', {
|
||||
params: { tenantId },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export default useNewSubscriptionUsage;
|
|
@ -33,7 +33,7 @@ function CreatePermissionModal({ resourceId, totalResourceCount, onClose }: Prop
|
|||
currentPlan,
|
||||
currentSku,
|
||||
currentSubscriptionQuota,
|
||||
currentSubscriptionScopeResourceUsage,
|
||||
currentSubscriptionResourceScopeUsage,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
|
@ -62,7 +62,7 @@ function CreatePermissionModal({ resourceId, totalResourceCount, onClose }: Prop
|
|||
const isScopesPerResourceReachLimit = isDevFeaturesEnabled
|
||||
? hasReachedSubscriptionQuotaLimit({
|
||||
quotaKey: 'scopesPerResourceLimit',
|
||||
usage: currentSubscriptionScopeResourceUsage[resourceId] ?? 0,
|
||||
usage: currentSubscriptionResourceScopeUsage[resourceId] ?? 0,
|
||||
quota: currentSubscriptionQuota,
|
||||
})
|
||||
: hasReachedQuotaLimit({
|
||||
|
|
|
@ -27,7 +27,7 @@ type Props = {
|
|||
|
||||
function AssignPermissionsModal({ roleId, roleType, totalRoleScopeCount, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentPlan, currentSku, currentSubscriptionScopeRoleUsage, currentSubscriptionQuota } =
|
||||
const { currentPlan, currentSku, currentSubscriptionRoleScopeUsage, currentSubscriptionQuota } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [scopes, setScopes] = useState<ScopeResponse[]>([]);
|
||||
|
@ -55,7 +55,7 @@ function AssignPermissionsModal({ roleId, roleType, totalRoleScopeCount, onClose
|
|||
const shouldBlockScopeAssignment = isDevFeaturesEnabled
|
||||
? hasSurpassedSubscriptionQuotaLimit({
|
||||
quotaKey: 'scopesPerRoleLimit',
|
||||
usage: (currentSubscriptionScopeRoleUsage[roleId] ?? 0) + scopes.length,
|
||||
usage: (currentSubscriptionRoleScopeUsage[roleId] ?? 0) + scopes.length,
|
||||
quota: currentSubscriptionQuota,
|
||||
})
|
||||
: hasSurpassedQuotaLimit({
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@logto/cloud": "0.2.5-50ff8fe",
|
||||
"@logto/cloud": "0.2.5-3452c56",
|
||||
"@silverhand/eslint-config": "6.0.1",
|
||||
"@silverhand/ts-config": "6.0.0",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
|
|
|
@ -54,4 +54,5 @@ export const allReportSubscriptionUpdatesUsageKeys = Object.freeze([
|
|||
'organizationsEnabled',
|
||||
'tenantMembersLimit',
|
||||
'enterpriseSsoLimit',
|
||||
'hooksLimit',
|
||||
]) satisfies readonly ReportSubscriptionUpdatesUsageKey[];
|
||||
|
|
|
@ -1217,8 +1217,8 @@ importers:
|
|||
version: 3.23.8
|
||||
devDependencies:
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-3b703da
|
||||
version: 0.2.5-3b703da(zod@3.23.8)
|
||||
specifier: 0.2.5-3452c56
|
||||
version: 0.2.5-3452c56(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)
|
||||
|
@ -2458,8 +2458,8 @@ importers:
|
|||
specifier: ^29.5.0
|
||||
version: 29.5.0
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-923c26f
|
||||
version: 0.2.5-923c26f(zod@3.23.8)
|
||||
specifier: 0.2.5-3452c56
|
||||
version: 0.2.5-3452c56(zod@3.23.8)
|
||||
'@logto/connector-kit':
|
||||
specifier: workspace:^4.0.0
|
||||
version: link:../toolkit/connector-kit
|
||||
|
@ -2948,8 +2948,8 @@ importers:
|
|||
version: 3.23.8
|
||||
devDependencies:
|
||||
'@logto/cloud':
|
||||
specifier: 0.2.5-50ff8fe
|
||||
version: 0.2.5-50ff8fe(zod@3.23.8)
|
||||
specifier: 0.2.5-3452c56
|
||||
version: 0.2.5-3452c56(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)
|
||||
|
@ -5076,16 +5076,8 @@ packages:
|
|||
'@logto/client@2.7.2':
|
||||
resolution: {integrity: sha512-jsmuDl9QpXfR3uLEMPE67tvYoL5XcjJi+4yGqucYPjd4GH6SUHp3N9skk8C/OyygnKDPLY+ttwD0LaIbpGvn+Q==}
|
||||
|
||||
'@logto/cloud@0.2.5-3b703da':
|
||||
resolution: {integrity: sha512-VCevQnxP5910s/cDYAxoJRim9iH1yN/La0HAlOP6FhVGtZofYwTTfT9AQXC+dZScgydpcFWo4k/6MYOFRtZCLg==}
|
||||
engines: {node: ^20.9.0}
|
||||
|
||||
'@logto/cloud@0.2.5-50ff8fe':
|
||||
resolution: {integrity: sha512-EMIGnx3swILEcSvYsAlPg9E1srtPcZxHxVH+D/dTrg8ctHbRAJkFbeuQFhwHGvs1dfgULd9MKtaAkL2qckExMw==}
|
||||
engines: {node: ^20.9.0}
|
||||
|
||||
'@logto/cloud@0.2.5-923c26f':
|
||||
resolution: {integrity: sha512-NAK9/T7HxEfE2djO6VTekMziOXH6NtbAzwumZcZo0bqIUDGiKlUvted/KY6iqpCdfFOF4aIyKp+pvlQIjj1T6Q==}
|
||||
'@logto/cloud@0.2.5-3452c56':
|
||||
resolution: {integrity: sha512-19MGifwYGxjQMPrm6monfoQyOp9UTL/chtZE0JugppNwvvLyqr3Nx0maCHuwrydLt0ImBSgVmPW1cJVvu2tVPg==}
|
||||
engines: {node: ^20.9.0}
|
||||
|
||||
'@logto/js@4.1.4':
|
||||
|
@ -14719,21 +14711,7 @@ snapshots:
|
|||
camelcase-keys: 7.0.2
|
||||
jose: 5.6.3
|
||||
|
||||
'@logto/cloud@0.2.5-3b703da(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.9.1
|
||||
'@withtyped/server': 0.13.6(zod@3.23.8)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@logto/cloud@0.2.5-50ff8fe(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.9.1
|
||||
'@withtyped/server': 0.13.6(zod@3.23.8)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@logto/cloud@0.2.5-923c26f(zod@3.23.8)':
|
||||
'@logto/cloud@0.2.5-3452c56(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@silverhand/essentials': 2.9.1
|
||||
'@withtyped/server': 0.13.6(zod@3.23.8)
|
||||
|
|
Loading…
Reference in a new issue