mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
refactor(console): refactor tenant creation form (#4826)
This commit is contained in:
parent
7b19837ffc
commit
a3de4fa6bb
8 changed files with 226 additions and 30 deletions
|
@ -7,6 +7,7 @@ import TenantLandingPageImageDark from '@/assets/images/tenant-landing-page-dark
|
|||
import TenantLandingPageImage from '@/assets/images/tenant-landing-page.svg';
|
||||
import { type TenantResponse } from '@/cloud/types/router';
|
||||
import CreateTenantModal from '@/components/CreateTenantModal';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
@ -52,7 +53,7 @@ function TenantLandingPageContent({ className }: Props) {
|
|||
/>
|
||||
</div>
|
||||
<CreateTenantModal
|
||||
skipPlanSelection
|
||||
skipPlanSelection={!isDevFeaturesEnabled}
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={async (tenant?: TenantResponse) => {
|
||||
if (tenant) {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(3);
|
||||
padding: _.unit(2.5) _.unit(3);
|
||||
}
|
||||
|
||||
.tag {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font: var(--font-body-3);
|
||||
}
|
||||
|
||||
.planNameTag {
|
||||
margin-left: _.unit(1);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { type AdminConsoleKey } from '@logto/phrases';
|
||||
import { TenantTag } from '@logto/schemas/lib/models/tenants.js';
|
||||
|
||||
import TenantEnvTag from '@/components/TenantEnvTag';
|
||||
import Divider from '@/ds-components/Divider';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import { ReservedPlanName } from '@/types/subscriptions';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
tag: TenantTag;
|
||||
};
|
||||
|
||||
const descriptionMap: Record<TenantTag, AdminConsoleKey> = {
|
||||
[TenantTag.Development]: 'tenants.create_modal.development_description',
|
||||
[TenantTag.Production]: 'tenants.create_modal.production_description',
|
||||
// Todo @xiaoyijun Remove deprecated tag
|
||||
[TenantTag.Staging]: 'tenants.create_modal.production_description',
|
||||
};
|
||||
|
||||
const availableProductionPlanNames = [
|
||||
ReservedPlanName.Free,
|
||||
ReservedPlanName.Hobby,
|
||||
ReservedPlanName.Pro,
|
||||
];
|
||||
|
||||
function EnvTagOptionContent({ tag }: Props) {
|
||||
// Todo @xiaoyijun Deprecated tag
|
||||
if (tag === TenantTag.Staging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<TenantEnvTag isAbbreviated={false} tag={tag} size="large" className={styles.tag} />
|
||||
<div className={styles.description}>
|
||||
<DynamicT forKey={descriptionMap[tag]} />
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={styles.hint}>
|
||||
{tag === TenantTag.Development && (
|
||||
<DynamicT forKey="tenants.create_modal.development_hint" />
|
||||
)}
|
||||
{tag === TenantTag.Production && (
|
||||
<>
|
||||
<DynamicT forKey="tenants.create_modal.available_plan" />
|
||||
{availableProductionPlanNames.map((planName) => (
|
||||
<Tag key={planName} variant="cell" size="small" className={styles.planNameTag}>
|
||||
{planName}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EnvTagOptionContent;
|
|
@ -6,3 +6,18 @@
|
|||
margin-top: _.unit(0.5);
|
||||
}
|
||||
|
||||
.regionOptions {
|
||||
font: var(--font-label-2);
|
||||
|
||||
.comingSoon {
|
||||
margin-left: _.unit(1);
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.envTagRadioGroup {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: _.unit(4);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { TenantTag } from '@logto/schemas/models';
|
||||
import { trySafe } from '@silverhand/essentials';
|
||||
import { useState } from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -11,7 +10,9 @@ import CreateTenantHeaderIconDark from '@/assets/icons/create-tenant-header-dark
|
|||
import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg';
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type TenantResponse } from '@/cloud/types/router';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import Button from '@/ds-components/Button';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
|
||||
|
@ -19,6 +20,7 @@ import TextInput from '@/ds-components/TextInput';
|
|||
import useTheme from '@/hooks/use-theme';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import EnvTagOptionContent from './EnvTagOptionContent';
|
||||
import SelectTenantPlanModal from './SelectTenantPlanModal';
|
||||
import * as styles from './index.module.scss';
|
||||
import { type CreateTenantData } from './type';
|
||||
|
@ -26,10 +28,12 @@ import { type CreateTenantData } from './type';
|
|||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: (tenant?: TenantResponse) => void;
|
||||
// Todo @xiaoyijun delete this prop when dev tenant feature is ready
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
skipPlanSelection?: boolean;
|
||||
};
|
||||
|
||||
// Todo @xiaoyijun remove when dev tenant feature is ready
|
||||
const tagOptions: Array<{ title: AdminConsoleKey; value: TenantTag }> = [
|
||||
{
|
||||
title: 'tenants.settings.environment_tag_development',
|
||||
|
@ -45,6 +49,8 @@ const tagOptions: Array<{ title: AdminConsoleKey; value: TenantTag }> = [
|
|||
},
|
||||
];
|
||||
|
||||
const availableTags = [TenantTag.Development, TenantTag.Production];
|
||||
|
||||
function CreateTenantModal({ isOpen, onClose, skipPlanSelection = false }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [tenantData, setTenantData] = useState<CreateTenantData>();
|
||||
|
@ -66,18 +72,37 @@ function CreateTenantModal({ isOpen, onClose, skipPlanSelection = false }: Props
|
|||
const cloudApi = useCloudApi();
|
||||
|
||||
const createTenant = async (data: CreateTenantData) => {
|
||||
void trySafe(async () => {
|
||||
const { name, tag } = data;
|
||||
const newTenant = await cloudApi.post('/api/tenants', { body: { name, tag } });
|
||||
onClose(newTenant);
|
||||
});
|
||||
const { name, tag } = data;
|
||||
const newTenant = await cloudApi.post('/api/tenants', { body: { name, tag } });
|
||||
onClose(newTenant);
|
||||
};
|
||||
|
||||
/**
|
||||
* Note: create tenant directly if it's from landing page,
|
||||
* since we want the user to get into the console as soon as possible
|
||||
*/
|
||||
const onCreateClick = handleSubmit(skipPlanSelection ? createTenant : setTenantData);
|
||||
const onCreateClick = handleSubmit(async (data: CreateTenantData) => {
|
||||
/**
|
||||
* Todo @xiaoyijun remove the original logic when dev tenant feature is ready
|
||||
*/
|
||||
if (!isDevFeaturesEnabled) {
|
||||
/**
|
||||
* Note: create tenant directly if it's from landing page,
|
||||
* since we want the user to get into the console as soon as possible
|
||||
*/
|
||||
if (skipPlanSelection) {
|
||||
await createTenant(data);
|
||||
return;
|
||||
}
|
||||
|
||||
setTenantData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const { tag } = data;
|
||||
if (tag === TenantTag.Development) {
|
||||
await createTenant(data);
|
||||
return;
|
||||
}
|
||||
|
||||
setTenantData(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -109,6 +134,7 @@ function CreateTenantModal({ isOpen, onClose, skipPlanSelection = false }: Props
|
|||
onClick={onCreateClick}
|
||||
/>
|
||||
}
|
||||
size="large"
|
||||
onClose={onClose}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
|
@ -120,23 +146,75 @@ function CreateTenantModal({ isOpen, onClose, skipPlanSelection = false }: Props
|
|||
error={Boolean(errors.name)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="tenants.settings.environment_tag">
|
||||
<Controller
|
||||
control={control}
|
||||
name="tag"
|
||||
rules={{ required: true }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<RadioGroup type="small" value={value} name={name} onChange={onChange}>
|
||||
{tagOptions.map(({ value: optionValue, title }) => (
|
||||
<Radio key={optionValue} title={title} value={optionValue} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
<div className={styles.description}>
|
||||
{t('tenants.settings.environment_tag_description')}
|
||||
</div>
|
||||
</FormField>
|
||||
{isDevFeaturesEnabled && (
|
||||
<FormField title="tenants.settings.tenant_region">
|
||||
<RadioGroup type="small" value="eu" name="region">
|
||||
<Radio
|
||||
title={
|
||||
<DangerousRaw>
|
||||
<span className={styles.regionOptions}>🇪🇺 EU</span>
|
||||
</DangerousRaw>
|
||||
}
|
||||
value="eu"
|
||||
/>
|
||||
<Radio
|
||||
isDisabled
|
||||
title={
|
||||
<DangerousRaw>
|
||||
<span className={styles.regionOptions}>
|
||||
🇺🇸 US
|
||||
<span className={styles.comingSoon}>{`(${t('general.coming_soon')})`}</span>
|
||||
</span>
|
||||
</DangerousRaw>
|
||||
}
|
||||
value="us"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormField>
|
||||
)}
|
||||
{!isDevFeaturesEnabled && (
|
||||
<FormField title="tenants.settings.environment_tag">
|
||||
<Controller
|
||||
control={control}
|
||||
name="tag"
|
||||
rules={{ required: true }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<RadioGroup type="small" value={value} name={name} onChange={onChange}>
|
||||
{tagOptions.map(({ value: optionValue, title }) => (
|
||||
<Radio key={optionValue} title={title} value={optionValue} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
<div className={styles.description}>
|
||||
{t('tenants.settings.environment_tag_description')}
|
||||
</div>
|
||||
</FormField>
|
||||
)}
|
||||
{isDevFeaturesEnabled && (
|
||||
<FormField title="tenants.create_modal.tenant_usage_purpose">
|
||||
<Controller
|
||||
control={control}
|
||||
name="tag"
|
||||
rules={{ required: true }}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<RadioGroup
|
||||
type="card"
|
||||
className={styles.envTagRadioGroup}
|
||||
value={value}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
>
|
||||
{availableTags.map((tag) => (
|
||||
<Radio key={tag} value={tag}>
|
||||
<EnvTagOptionContent tag={tag} />
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
</FormProvider>
|
||||
<SelectTenantPlanModal
|
||||
tenantData={tenantData}
|
||||
|
|
|
@ -275,6 +275,12 @@
|
|||
outline: unset;
|
||||
}
|
||||
|
||||
.small.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-state-disabled);
|
||||
}
|
||||
|
||||
.compact.disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--color-layer-2);
|
||||
|
|
|
@ -78,4 +78,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
font: var(--font-label-3);
|
||||
padding: _.unit(0.25) _.unit(1.5);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ export type Props = Pick<HTMLProps<HTMLDivElement>, 'className' | 'onClick'> & {
|
|||
type?: 'property' | 'state' | 'result';
|
||||
status?: 'info' | 'success' | 'alert' | 'error';
|
||||
variant?: 'plain' | 'outlined' | 'cell';
|
||||
size?: 'medium' | 'small';
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
|
@ -23,6 +24,7 @@ function Tag({
|
|||
type = 'property',
|
||||
status = 'info',
|
||||
variant = 'outlined',
|
||||
size = 'medium',
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
|
@ -30,7 +32,10 @@ function Tag({
|
|||
const ResultIcon = conditional(type === 'result' && ResultIconMap[status]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.tag, styles[status], styles[variant], className)} {...rest}>
|
||||
<div
|
||||
className={classNames(styles.tag, styles[status], styles[variant], styles[size], className)}
|
||||
{...rest}
|
||||
>
|
||||
{type === 'state' && <div className={styles.icon} />}
|
||||
{ResultIcon && <ResultIcon className={classNames(styles.icon, styles.resultIcon)} />}
|
||||
{children}
|
||||
|
|
Loading…
Add table
Reference in a new issue