Skip to content

Commit dfefc8d

Browse files
authored
feat(console): support dynamic regions when creating a new tenant (#7736)
* feat(console): support dynamic regions when creating a new tenant * refactor(console): support loading and error display * refactor(console): read available tags from api
1 parent 07ba4d4 commit dfefc8d

File tree

9 files changed

+111
-67
lines changed

9 files changed

+111
-67
lines changed

packages/connectors/connector-logto-email/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"access": "public"
5353
},
5454
"devDependencies": {
55-
"@logto/cloud": "0.2.5-6279e2e",
55+
"@logto/cloud": "0.2.5-590fbce",
5656
"@silverhand/eslint-config": "6.0.1",
5757
"@silverhand/ts-config": "6.0.0",
5858
"@types/node": "^22.14.0",

packages/console/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@fontsource/roboto-mono": "^5.0.0",
2929
"@inkeep/cxkit-react": "^0.5.66",
3030
"@jest/types": "^29.5.0",
31-
"@logto/cloud": "0.2.5-6279e2e",
31+
"@logto/cloud": "0.2.5-590fbce",
3232
"@logto/connector-kit": "workspace:^",
3333
"@logto/core-kit": "workspace:^",
3434
"@logto/language-kit": "workspace:^",

packages/console/src/components/CreateTenantModal/index.module.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@
1111
grid-template-columns: repeat(2, 1fr);
1212
gap: _.unit(4);
1313
}
14+
15+
.error {
16+
font: var(--font-body-2);
17+
color: var(--color-error);
18+
}

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

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1+
import { type Region as RegionType } from '@logto/cloud/routes';
12
import { Theme, TenantTag } from '@logto/schemas';
2-
import { useState } from 'react';
3+
import { useMemo, useState } from 'react';
34
import { Controller, FormProvider, useForm } from 'react-hook-form';
45
import { toast } from 'react-hot-toast';
56
import { useTranslation } from 'react-i18next';
67
import Modal from 'react-modal';
8+
import useSWRImmutable from 'swr/immutable';
79

810
import CreateTenantHeaderIconDark from '@/assets/icons/create-tenant-header-dark.svg?react';
911
import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg?react';
1012
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
1113
import { type TenantResponse } from '@/cloud/types/router';
1214
import Region, { defaultRegionName } from '@/components/Region';
13-
import { availableRegions } from '@/consts';
1415
import Button from '@/ds-components/Button';
1516
import DangerousRaw from '@/ds-components/DangerousRaw';
1617
import FormField from '@/ds-components/FormField';
1718
import ModalLayout from '@/ds-components/ModalLayout';
1819
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
20+
import { Ring } from '@/ds-components/Spinner';
1921
import TextInput from '@/ds-components/TextInput';
2022
import useTheme from '@/hooks/use-theme';
2123
import modalStyles from '@/scss/modal.module.scss';
@@ -31,11 +33,19 @@ type Props = {
3133
readonly onClose: (tenant?: TenantResponse) => void;
3234
};
3335

34-
const availableTags = [TenantTag.Development, TenantTag.Production];
36+
const allTags = Object.freeze([TenantTag.Development, TenantTag.Production]);
3537

3638
function CreateTenantModal({ isOpen, onClose }: Props) {
3739
const [tenantData, setTenantData] = useState<CreateTenantData>();
3840
const theme = useTheme();
41+
const cloudApi = useCloudApi();
42+
const { data: regions, error: regionsError } = useSWRImmutable<RegionType[], Error>(
43+
'api/me/regions',
44+
async () => {
45+
const { regions } = await cloudApi.get('/api/me/regions');
46+
return regions;
47+
}
48+
);
3949

4050
const defaultValues = Object.freeze({
4151
tag: TenantTag.Development,
@@ -51,10 +61,14 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
5161
handleSubmit,
5262
formState: { errors, isSubmitting },
5363
register,
64+
watch,
5465
} = methods;
5566

56-
const cloudApi = useCloudApi();
57-
67+
const regionName = watch('regionName');
68+
const currentRegion = useMemo(
69+
() => regions?.find((region) => region.id === regionName),
70+
[regions, regionName]
71+
);
5872
const createTenant = async ({ name, tag, regionName }: CreateTenantData) => {
5973
const newTenant = await cloudApi.post('/api/tenants', { body: { name, tag, regionName } });
6074
onClose(newTenant);
@@ -118,54 +132,61 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
118132
disabled={isSubmitting}
119133
/>
120134
</FormField>
135+
121136
<FormField
122137
title="tenants.settings.tenant_region"
123138
tip={t('tenants.settings.tenant_region_description')}
124139
>
125-
<Controller
126-
control={control}
127-
name="regionName"
128-
rules={{ required: true }}
129-
render={({ field: { onChange, value, name } }) => (
130-
<RadioGroup type="small" name={name} value={value} onChange={onChange}>
131-
{availableRegions.map((region) => (
132-
<Radio
133-
key={region}
134-
title={
135-
<DangerousRaw>
136-
<Region regionName={region} />
137-
</DangerousRaw>
138-
}
139-
value={region}
140-
isDisabled={isSubmitting}
141-
/>
142-
))}
143-
</RadioGroup>
144-
)}
145-
/>
146-
</FormField>
147-
<FormField title="tenants.create_modal.tenant_usage_purpose">
148-
<Controller
149-
control={control}
150-
name="tag"
151-
rules={{ required: true }}
152-
render={({ field: { onChange, value, name } }) => (
153-
<RadioGroup
154-
type="card"
155-
className={styles.envTagRadioGroup}
156-
value={value}
157-
name={name}
158-
onChange={onChange}
159-
>
160-
{availableTags.map((tag) => (
161-
<Radio key={tag} value={tag}>
162-
<EnvTagOptionContent tag={tag} />
163-
</Radio>
164-
))}
165-
</RadioGroup>
166-
)}
167-
/>
140+
{!regions && !regionsError && <Ring />}
141+
{regionsError && <span className={styles.error}>{regionsError.message}</span>}
142+
{regions && !regionsError && (
143+
<Controller
144+
control={control}
145+
name="regionName"
146+
rules={{ required: true }}
147+
render={({ field: { onChange, value, name } }) => (
148+
<RadioGroup type="plain" name={name} value={value} onChange={onChange}>
149+
{regions.map((region) => (
150+
<Radio
151+
key={region.id}
152+
title={
153+
<DangerousRaw>
154+
<Region region={region} />
155+
</DangerousRaw>
156+
}
157+
value={region.id}
158+
isDisabled={isSubmitting}
159+
/>
160+
))}
161+
</RadioGroup>
162+
)}
163+
/>
164+
)}
168165
</FormField>
166+
{currentRegion && (
167+
<FormField title="tenants.create_modal.tenant_usage_purpose">
168+
<Controller
169+
control={control}
170+
name="tag"
171+
rules={{ required: true }}
172+
render={({ field: { onChange, value, name } }) => (
173+
<RadioGroup
174+
type="card"
175+
className={styles.envTagRadioGroup}
176+
value={value}
177+
name={name}
178+
onChange={onChange}
179+
>
180+
{currentRegion.tags.map((tag) => (
181+
<Radio key={tag} value={tag}>
182+
<EnvTagOptionContent tag={tag} />
183+
</Radio>
184+
))}
185+
</RadioGroup>
186+
)}
187+
/>
188+
</FormField>
189+
)}
169190
</FormProvider>
170191
<SelectTenantPlanModal
171192
tenantData={tenantData}

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type PublicRegionName } from '@logto/cloud/routes';
1+
import { type PublicRegionName, type Region as RegionType } from '@logto/cloud/routes';
22
import classNames from 'classnames';
33
import { useMemo, type FunctionComponent } from 'react';
44
import { useTranslation } from 'react-i18next';
@@ -53,13 +53,17 @@ export function RegionFlag({ regionName, width = 16 }: RegionFlagProps) {
5353
return Flag ? <Flag width={width} /> : null;
5454
}
5555

56-
type Props = {
56+
type StaticRegionProps = {
5757
readonly regionName: string;
5858
readonly isComingSoon?: boolean;
5959
readonly className?: string;
6060
};
6161

62-
function Region({ isComingSoon = false, regionName, className }: Props) {
62+
/**
63+
* @deprecated A legacy component that renders a region by name. You should use the new
64+
* {@link Region} component to render region data from the API instead.
65+
*/
66+
export function StaticRegion({ isComingSoon = false, regionName, className }: StaticRegionProps) {
6367
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
6468

6569
return (
@@ -71,4 +75,18 @@ function Region({ isComingSoon = false, regionName, className }: Props) {
7175
);
7276
}
7377

78+
type Props = {
79+
readonly region: RegionType;
80+
readonly className?: string;
81+
};
82+
83+
function Region({ region, className }: Props) {
84+
return (
85+
<span className={classNames(styles.wrapper, className)}>
86+
<RegionFlag regionName={region.id} />
87+
<span>{region.name}</span>
88+
</span>
89+
);
90+
}
91+
7492
export default Region;

packages/console/src/onboarding/pages/CreateTenant/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import ActionBar from '@/components/ActionBar';
1313
import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
1414
import { type CreateTenantData } from '@/components/CreateTenantModal/types';
1515
import PageMeta from '@/components/PageMeta';
16-
import Region, { defaultRegionName } from '@/components/Region';
16+
import { StaticRegion, defaultRegionName } from '@/components/Region';
1717
import { availableRegions } from '@/consts';
1818
import { TenantsContext } from '@/contexts/TenantsProvider';
1919
import Button from '@/ds-components/Button';
@@ -138,7 +138,7 @@ function CreateTenant() {
138138
key={region}
139139
title={
140140
<DangerousRaw>
141-
<Region regionName={region} />
141+
<StaticRegion regionName={region} />
142142
</DangerousRaw>
143143
}
144144
value={region}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useContext } from 'react';
22
import { Trans, useTranslation } from 'react-i18next';
33

4-
import Region, { getRegionDisplayName } from '@/components/Region';
4+
import { getRegionDisplayName, StaticRegion } from '@/components/Region';
55
import { trustAndSecurityLink } from '@/consts';
66
import { TenantsContext } from '@/contexts/TenantsProvider';
77
import TextLink from '@/ds-components/TextLink';
@@ -19,7 +19,7 @@ function TenantRegion() {
1919

2020
return (
2121
<div className={styles.container}>
22-
<Region className={styles.region} regionName={regionName} />
22+
<StaticRegion className={styles.region} regionName={regionName} />
2323
<div className={styles.regionTip}>
2424
<Trans
2525
components={{

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
"zod": "3.24.3"
101101
},
102102
"devDependencies": {
103-
"@logto/cloud": "0.2.5-6279e2e",
103+
"@logto/cloud": "0.2.5-590fbce",
104104
"@silverhand/eslint-config": "6.0.1",
105105
"@silverhand/ts-config": "6.0.0",
106106
"@types/adm-zip": "^0.5.5",

pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)