0
Fork 0
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:
simeng-li 2024-08-12 09:57:45 +08:00 committed by GitHub
parent ea70e09ad8
commit 758d270f7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 77 additions and 193 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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']>;

View file

@ -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 ==== */
});

View file

@ -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;
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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({

View file

@ -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({

View file

@ -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",

View file

@ -54,4 +54,5 @@ export const allReportSubscriptionUpdatesUsageKeys = Object.freeze([
'organizationsEnabled',
'tenantMembersLimit',
'enterpriseSsoLimit',
'hooksLimit',
]) satisfies readonly ReportSubscriptionUpdatesUsageKey[];

View file

@ -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)