Skip to content

Commit e8430cc

Browse files
simeng-liCopilot
andauthored
refactor(console): unblock private region prod tenant creation and deletion (#8119)
* refactor(console): unblock private region prod tenant creation and deletion unblock private region prod tenant creation and deletion * fix(console): fix region selection fix region selection * fix(console): fix typo fix typo Co-authored-by: Copilot <[email protected]> * fix(console): fix typo fix typo --------- Co-authored-by: Copilot <[email protected]>
1 parent 8dd57e4 commit e8430cc

File tree

3 files changed

+74
-33
lines changed

3 files changed

+74
-33
lines changed

packages/console/src/components/CreateTenantModal/index.tsx

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Theme, TenantTag } from '@logto/schemas';
22
import { condArray } from '@silverhand/essentials';
33
import { useCallback, useMemo, useState } from 'react';
4-
import { Controller, FormProvider, useForm } from 'react-hook-form';
4+
import { Controller, type ControllerRenderProps, FormProvider, useForm } from 'react-hook-form';
55
import { toast } from 'react-hot-toast';
66
import { useTranslation } from 'react-i18next';
77
import Modal from 'react-modal';
@@ -12,9 +12,10 @@ import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
1212
import { type TenantResponse, type RegionResponse as RegionType } from '@/cloud/types/router';
1313
import Region, {
1414
defaultRegionName,
15-
logtoDropdownItem,
15+
publicInstancesDropdownItem,
1616
type InstanceDropdownItemProps,
1717
} from '@/components/Region';
18+
import { isDevFeaturesEnabled } from '@/consts/env';
1819
import Button from '@/ds-components/Button';
1920
import DangerousRaw from '@/ds-components/DangerousRaw';
2021
import FormField from '@/ds-components/FormField';
@@ -48,26 +49,28 @@ const getInstanceDropdownItems = (regions: RegionType[]): InstanceDropdownItemPr
4849
.filter(({ isPrivate }) => isPrivate)
4950
.map(({ id, name, country, tags, displayName }) => ({ id, name, country, tags, displayName }));
5051

51-
return condArray(hasPublicRegions && logtoDropdownItem, ...privateInstances);
52+
return condArray(hasPublicRegions && publicInstancesDropdownItem, ...privateInstances);
5253
};
5354

55+
const defaultFormValues = Object.freeze({
56+
tag: TenantTag.Development,
57+
instanceId: publicInstancesDropdownItem.name,
58+
regionName: defaultRegionName,
59+
});
60+
5461
function CreateTenantModal({ isOpen, onClose }: Props) {
5562
const [tenantData, setTenantData] = useState<CreateTenantData>();
5663
const theme = useTheme();
5764
const cloudApi = useCloudApi();
5865
const { regions, regionsError, getRegionByName } = useAvailableRegions();
5966

60-
const defaultValues = Object.freeze({
61-
tag: TenantTag.Development,
62-
instanceId: logtoDropdownItem.name,
63-
regionName: defaultRegionName,
64-
});
6567
const methods = useForm<CreateTenantData>({
66-
defaultValues,
68+
defaultValues: defaultFormValues,
6769
});
6870

6971
const {
7072
reset,
73+
setValue,
7174
control,
7275
handleSubmit,
7376
formState: { errors, isSubmitting },
@@ -84,53 +87,73 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
8487
[reset, watch]
8588
);
8689

87-
const instanceOptions = useMemo(() => getInstanceDropdownItems(regions ?? []), [regions]);
90+
const instanceDropdownItems = useMemo(() => getInstanceDropdownItems(regions ?? []), [regions]);
8891
const hasPrivateRegionsAccess = useMemo(() => checkPrivateRegionAccess(regions ?? []), [regions]);
8992

9093
const publicRegions = useMemo(
9194
() => regions?.filter((region) => !region.isPrivate) ?? [],
9295
[regions]
9396
);
9497

95-
const isLogtoInstance = useMemo(() => instanceId === logtoDropdownItem.name, [instanceId]);
96-
97-
const currentRegion = useMemo(
98-
() => getRegionByName(isLogtoInstance ? regionName : instanceId),
99-
[isLogtoInstance, regionName, instanceId, getRegionByName]
98+
const isPublicInstanceSelected = useMemo(
99+
() => instanceId === publicInstancesDropdownItem.name,
100+
[instanceId]
100101
);
101102

102-
const getFinalRegionName = useCallback(
103-
(instanceId: string, regionName: string) => {
104-
return isLogtoInstance ? regionName : instanceId;
105-
},
106-
[isLogtoInstance]
107-
);
103+
const currentRegion = useMemo(() => getRegionByName(regionName), [regionName, getRegionByName]);
108104

109-
const createTenant = async ({ name, tag, instanceId, regionName }: CreateTenantData) => {
110-
// For Logto public instance, use the selected region
111-
// For private instances, use the instance ID as the region
112-
const finalRegionName = getFinalRegionName(instanceId, regionName);
105+
const createTenant = async ({ name, tag, regionName }: CreateTenantData) => {
113106
const newTenant = await cloudApi.post('/api/tenants', {
114-
body: { name, tag, regionName: finalRegionName },
107+
body: { name, tag, regionName },
115108
});
116109
onClose(newTenant);
117110
};
118111
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
119112

120113
const onCreateClick = handleSubmit(
121114
trySubmitSafe(async (data: CreateTenantData) => {
122-
const { tag } = data;
115+
const { tag, instanceId } = data;
123116
if (tag === TenantTag.Development) {
124117
await createTenant(data);
125118
toast.success(t('tenants.create_modal.tenant_created'));
126119
return;
127120
}
128121

122+
// TODO: remove the dev feature guard once the enterprise subscription is ready
123+
// Private region production tenant creation
124+
if (isDevFeaturesEnabled && instanceId !== publicInstancesDropdownItem.name) {
125+
// Directly call the create tenant API instead of going through the plan selection modal.
126+
// Based on product design, private region can only have one production tenant plan,
127+
// and should not go through the subscription checkout flow,
128+
// always associated the new tenant with the existing enterprise subscription of the private region.
129+
await createTenant(data);
130+
toast.success(t('tenants.create_modal.tenant_created'));
131+
return;
132+
}
133+
129134
// For production tenants, store creation parameters with the correct regionName for later use after plan selection.
130-
setTenantData({ ...data, regionName: getFinalRegionName(data.instanceId, data.regionName) });
135+
setTenantData(data);
131136
})
132137
);
133138

139+
const handleInstanceIdChange = useCallback(
140+
(
141+
nextId: string,
142+
onChange: ControllerRenderProps<CreateTenantData, 'instanceId'>['onChange']
143+
) => {
144+
onChange(nextId);
145+
146+
if (nextId === publicInstancesDropdownItem.name && publicRegions[0]) {
147+
// Otherwise, reset to the first public region when switching to public instance.
148+
setValue('regionName', publicRegions[0].name, { shouldValidate: true, shouldDirty: true });
149+
} else {
150+
// If switching to a private instance, reset regionName using the instanceId.
151+
setValue('regionName', nextId, { shouldValidate: true, shouldDirty: true });
152+
}
153+
},
154+
[publicRegions, setValue]
155+
);
156+
134157
return (
135158
<Modal
136159
shouldCloseOnOverlayClick
@@ -139,7 +162,7 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
139162
className={modalStyles.content}
140163
overlayClassName={modalStyles.overlay}
141164
onAfterClose={() => {
142-
reset(defaultValues);
165+
reset(defaultFormValues);
143166
}}
144167
onRequestClose={() => {
145168
onClose();
@@ -191,18 +214,20 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
191214
rules={{ required: true }}
192215
render={({ field: { onChange, value } }) => (
193216
<InstanceSelector
194-
instances={instanceOptions}
217+
instances={instanceDropdownItems}
195218
value={value}
196219
isDisabled={isSubmitting}
197220
setTenantTagInForm={setTenantTagInForm}
198-
onChange={onChange}
221+
onChange={(nextId) => {
222+
handleInstanceIdChange(nextId, onChange);
223+
}}
199224
/>
200225
)}
201226
/>
202227
)}
203228
</FormField>
204229
)}
205-
{isLogtoInstance && (
230+
{isPublicInstanceSelected && (
206231
<FormField
207232
title="tenants.settings.tenant_region"
208233
tip={t('tenants.settings.tenant_region_description')}

packages/console/src/components/Region/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,16 @@ export type InstanceDropdownItemProps = Pick<
8686
'name' | 'country' | 'tags' | 'displayName'
8787
>;
8888

89-
export const logtoDropdownItem: InstanceDropdownItemProps = {
89+
/**
90+
* The default public Logto instance dropdown item.
91+
*
92+
* @remarks
93+
* This item is a placeholder for the public Logto instance and is used in the instance selection dropdown.
94+
*
95+
* - When selected, it indicates that the user is choosing the public Logto instance, need to show the public region radio options below.
96+
* - When not selected, it indicates that the user is choosing a private instance, need to hide the public region radio options below.
97+
*/
98+
export const publicInstancesDropdownItem: InstanceDropdownItemProps = {
9099
name: 'logto',
91100
displayName: 'Logto Cloud (Public)',
92101
country: 'LOGTO',

packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
99
import PageMeta from '@/components/PageMeta';
1010
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
1111
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
12+
import { isDevFeaturesEnabled } from '@/consts/env';
1213
import { TenantsContext } from '@/contexts/TenantsProvider';
1314
import { useConfirmModal } from '@/hooks/use-confirm-modal';
1415
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
@@ -77,8 +78,14 @@ function TenantBasicSettings() {
7778
);
7879

7980
const onClickDeletionButton = async () => {
81+
const isSharedEnterpriseSubscription =
82+
// TODO: remove the dev feature guard once the enterprise subscription is ready
83+
isDevFeaturesEnabled && currentTenant?.subscription.quotaScope === 'shared';
84+
8085
if (
8186
!isDevTenant &&
87+
// Should allow production tenant deletion of shared enterprise subscription
88+
!isSharedEnterpriseSubscription &&
8289
(currentTenant?.subscription.planId !== ReservedPlanId.Free ||
8390
currentTenant.openInvoices.length > 0)
8491
) {

0 commit comments

Comments
 (0)