mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
feat(console): create tenant modal (#4019)
This commit is contained in:
parent
0e2664aea5
commit
8e92cb1fe3
8 changed files with 241 additions and 5 deletions
|
@ -0,0 +1,36 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="5" cy="15" r="1.5" stroke="#78767F"/>
|
||||
<rect x="37.7071" y="7.82812" width="3" height="3" rx="0.5" transform="rotate(-45 37.7071 7.82812)" stroke="#78767F"/>
|
||||
<circle cx="42" cy="32" r="1.5" stroke="#78767F"/>
|
||||
<path d="M11 24.6735L21.9956 14.7998C23.1358 13.776 24.8642 13.776 26.0044 14.7998L36.0044 23.7794C36.638 24.3484 37 25.1599 37 26.0116V36C37 37.6569 35.6569 39 34 39H14C12.3431 39 11 37.6569 11 36V24.6735Z" fill="#F7ACCF"/>
|
||||
<path d="M11 24.6735L21.9956 14.7998C23.1358 13.776 24.8642 13.776 26.0044 14.7998L36.0044 23.7794C36.638 24.3484 37 25.1599 37 26.0116V36C37 37.6569 35.6569 39 34 39H14C12.3431 39 11 37.6569 11 36V24.6735Z" fill="url(#paint0_linear_8217_107638)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.174 9.67882C25.0518 8.71295 23.3928 8.71021 22.2675 9.67237L6.6517 23.0235C5.7625 23.7837 5.71282 25.1415 6.54409 25.9647L7.29145 26.7048C8.03068 27.4368 9.20768 27.4799 9.99842 26.8038L22.8609 15.8068C23.6092 15.167 24.7119 15.167 25.4602 15.8068L38.0016 26.5293C38.7923 27.2054 39.9693 27.1623 40.7085 26.4303L41.461 25.6851C42.2904 24.8637 42.2431 23.5096 41.3584 22.7481L26.174 9.67882Z" fill="url(#paint1_linear_8217_107638)"/>
|
||||
<g filter="url(#filter0_i_8217_107638)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 25C24.5523 25 25 24.5523 25 24C25 23.4477 24.5523 23 24 23C23.4477 23 23 23.4477 23 24C23 24.5523 23.4477 25 24 25ZM28 24C28 25.6787 26.9659 27.1159 25.5 27.7092V30H26C26.5523 30 27 30.4477 27 31V32C27 32.5523 26.5523 33 26 33H25.5V34.5C25.5 35.3284 24.8284 36 24 36C23.1716 36 22.5 35.3284 22.5 34.5V27.7092C21.0341 27.1159 20 25.6787 20 24C20 21.7909 21.7909 20 24 20C26.2091 20 28 21.7909 28 24Z" fill="#5E35F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 25C24.5523 25 25 24.5523 25 24C25 23.4477 24.5523 23 24 23C23.4477 23 23 23.4477 23 24C23 24.5523 23.4477 25 24 25ZM28 24C28 25.6787 26.9659 27.1159 25.5 27.7092V30H26C26.5523 30 27 30.4477 27 31V32C27 32.5523 26.5523 33 26 33H25.5V34.5C25.5 35.3284 24.8284 36 24 36C23.1716 36 22.5 35.3284 22.5 34.5V27.7092C21.0341 27.1159 20 25.6787 20 24C20 21.7909 21.7909 20 24 20C26.2091 20 28 21.7909 28 24Z" fill="url(#paint2_linear_8217_107638)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_8217_107638" x="20" y="20" width="8" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_8217_107638"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_8217_107638" x1="21.396" y1="12.1999" x2="16.1518" y2="38.8053" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF06A"/>
|
||||
<stop offset="1" stop-color="#FDA8BF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_8217_107638" x1="12.3575" y1="24.5111" x2="23.5934" y2="2.78065" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_8217_107638" x1="20" y1="33.1072" x2="33.2323" y2="32.3068" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
36
packages/console/src/assets/images/create-tenant-header.svg
Normal file
36
packages/console/src/assets/images/create-tenant-header.svg
Normal file
|
@ -0,0 +1,36 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="5" cy="15" r="1.5" stroke="#FFD5FF"/>
|
||||
<rect x="37.7071" y="7.82812" width="3" height="3" rx="0.5" transform="rotate(-45 37.7071 7.82812)" stroke="#E6DEFF"/>
|
||||
<circle cx="42" cy="32" r="1.5" stroke="#FFD5FF"/>
|
||||
<path d="M11 24.6735L21.9956 14.7998C23.1358 13.776 24.8642 13.776 26.0044 14.7998L36.0044 23.7794C36.638 24.3484 37 25.1599 37 26.0116V36C37 37.6569 35.6569 39 34 39H14C12.3431 39 11 37.6569 11 36V24.6735Z" fill="#F7ACCF"/>
|
||||
<path d="M11 24.6735L21.9956 14.7998C23.1358 13.776 24.8642 13.776 26.0044 14.7998L36.0044 23.7794C36.638 24.3484 37 25.1599 37 26.0116V36C37 37.6569 35.6569 39 34 39H14C12.3431 39 11 37.6569 11 36V24.6735Z" fill="url(#paint0_linear_480_32283)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.174 9.67882C25.0518 8.71295 23.3928 8.71021 22.2675 9.67237L6.6517 23.0235C5.7625 23.7837 5.71282 25.1415 6.54409 25.9647L7.29145 26.7048C8.03068 27.4368 9.20768 27.4799 9.99842 26.8038L22.8609 15.8068C23.6092 15.167 24.7119 15.167 25.4602 15.8068L38.0016 26.5293C38.7923 27.2054 39.9693 27.1623 40.7085 26.4303L41.461 25.6851C42.2904 24.8637 42.2431 23.5096 41.3584 22.7481L26.174 9.67882Z" fill="url(#paint1_linear_480_32283)"/>
|
||||
<g filter="url(#filter0_i_480_32283)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 25C24.5523 25 25 24.5523 25 24C25 23.4477 24.5523 23 24 23C23.4477 23 23 23.4477 23 24C23 24.5523 23.4477 25 24 25ZM28 24C28 25.6787 26.9659 27.1159 25.5 27.7092V30H26C26.5523 30 27 30.4477 27 31V32C27 32.5523 26.5523 33 26 33H25.5V34.5C25.5 35.3284 24.8284 36 24 36C23.1716 36 22.5 35.3284 22.5 34.5V27.7092C21.0341 27.1159 20 25.6787 20 24C20 21.7909 21.7909 20 24 20C26.2091 20 28 21.7909 28 24Z" fill="#5E35F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 25C24.5523 25 25 24.5523 25 24C25 23.4477 24.5523 23 24 23C23.4477 23 23 23.4477 23 24C23 24.5523 23.4477 25 24 25ZM28 24C28 25.6787 26.9659 27.1159 25.5 27.7092V30H26C26.5523 30 27 30.4477 27 31V32C27 32.5523 26.5523 33 26 33H25.5V34.5C25.5 35.3284 24.8284 36 24 36C23.1716 36 22.5 35.3284 22.5 34.5V27.7092C21.0341 27.1159 20 25.6787 20 24C20 21.7909 21.7909 20 24 20C26.2091 20 28 21.7909 28 24Z" fill="url(#paint2_linear_480_32283)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_480_32283" x="20" y="20" width="8" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_480_32283"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_480_32283" x1="21.396" y1="12.1999" x2="16.1518" y2="38.8053" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF06A"/>
|
||||
<stop offset="1" stop-color="#FDA8BF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_480_32283" x1="12.3575" y1="24.5111" x2="23.5934" y2="2.78065" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_480_32283" x1="20" y1="33.1072" x2="33.2323" y2="32.3068" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D34F2"/>
|
||||
<stop offset="1" stop-color="#FAABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,8 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.description {
|
||||
color: var(--color-text-secondary);
|
||||
font: var(--font-body-2);
|
||||
margin-top: _.unit(0.5);
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import { Theme } from '@logto/schemas';
|
||||
import { TenantTag, type TenantInfo } from '@logto/schemas/models';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import CreateTenantHeaderIconDark from '@/assets/images/create-tenant-header-dark.svg';
|
||||
import CreateTenantHeaderIcon from '@/assets/images/create-tenant-header.svg';
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: (tenant?: TenantInfo) => void;
|
||||
};
|
||||
|
||||
const tagOptions: Array<{ title: AdminConsoleKey; value: TenantTag }> = [
|
||||
{
|
||||
title: 'tenants.create_modal.environment_tag_development',
|
||||
value: TenantTag.Development,
|
||||
},
|
||||
{
|
||||
title: 'tenants.create_modal.environment_tag_staging',
|
||||
value: TenantTag.Staging,
|
||||
},
|
||||
{
|
||||
title: 'tenants.create_modal.environment_tag_production',
|
||||
value: TenantTag.Production,
|
||||
},
|
||||
];
|
||||
|
||||
function CreateTenantModal({ isOpen, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const theme = useTheme();
|
||||
const methods = useForm<Pick<TenantInfo, 'name' | 'tag'>>({
|
||||
defaultValues: { tag: TenantTag.Development },
|
||||
});
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
register,
|
||||
} = methods;
|
||||
|
||||
const cloudApi = useCloudApi();
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
const { name, tag } = data;
|
||||
const newTenant = await cloudApi
|
||||
.post('/api/tenants', { json: { name, tag } })
|
||||
.json<TenantInfo>();
|
||||
onClose(newTenant);
|
||||
} catch (error: unknown) {
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
shouldCloseOnOverlayClick
|
||||
shouldCloseOnEsc
|
||||
isOpen={isOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
onRequestClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ModalLayout
|
||||
title="tenants.create_modal.title"
|
||||
subtitle="tenants.create_modal.subtitle"
|
||||
headerIcon={
|
||||
theme === Theme.Light ? <CreateTenantHeaderIcon /> : <CreateTenantHeaderIconDark />
|
||||
}
|
||||
footer={
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
htmlType="submit"
|
||||
title="tenants.create_modal.create_button"
|
||||
size="large"
|
||||
type="primary"
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<FormField isRequired title="tenants.create_modal.tenant_name">
|
||||
<TextInput {...register('name', { required: true })} error={Boolean(errors.name)} />
|
||||
</FormField>
|
||||
<FormField title="tenants.create_modal.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.create_modal.environment_tag_description')}
|
||||
</div>
|
||||
</FormField>
|
||||
</FormProvider>
|
||||
</ModalLayout>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateTenantModal;
|
|
@ -131,6 +131,7 @@
|
|||
border: 1px solid var(--color-border);
|
||||
flex: 1;
|
||||
font: var(--font-body-2);
|
||||
height: 36px;
|
||||
|
||||
&:first-child {
|
||||
border-radius: 6px 0 0 6px;
|
||||
|
@ -149,7 +150,8 @@
|
|||
}
|
||||
|
||||
.content {
|
||||
padding: _.unit(2) 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,35 @@
|
|||
import { TenantTag } from '@logto/schemas/models';
|
||||
import { type TenantInfo, TenantTag } from '@logto/schemas/models';
|
||||
import classNames from 'classnames';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg';
|
||||
import PlusSign from '@/assets/images/plus.svg';
|
||||
import Tick from '@/assets/images/tick.svg';
|
||||
import CreateTenantModal from '@/cloud/pages/Main/CreateTenantModal';
|
||||
import AppError from '@/components/AppError';
|
||||
import Divider from '@/components/Divider';
|
||||
import Dropdown, { DropdownItem } from '@/components/Dropdown';
|
||||
import useTenants from '@/hooks/use-tenants';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import TenantEnvTag from './components/TenantEnvTag';
|
||||
import TenantEnvTag from './TenantEnvTag';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
function TenantSelector() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { tenants, currentTenant: currentTenantInfo, currentTenantId, error } = useTenants();
|
||||
const {
|
||||
tenants,
|
||||
currentTenant: currentTenantInfo,
|
||||
currentTenantId,
|
||||
error,
|
||||
mutate,
|
||||
} = useTenants();
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [showCreateTenantModal, setShowCreateTenantModal] = useState(false);
|
||||
|
||||
if (error) {
|
||||
return <AppError errorMessage={error.message} callStack={error.stack} />;
|
||||
|
@ -77,11 +86,31 @@ function TenantSelector() {
|
|||
</DropdownItem>
|
||||
))}
|
||||
<Divider />
|
||||
<div className={styles.createTenantButton}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.createTenantButton}
|
||||
onClick={() => {
|
||||
setShowCreateTenantModal(true);
|
||||
}}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
setShowCreateTenantModal(true);
|
||||
})}
|
||||
>
|
||||
<div>{t('cloud.tenant.create_tenant')}</div>
|
||||
<PlusSign className={styles.icon} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
<CreateTenantModal
|
||||
isOpen={showCreateTenantModal}
|
||||
onClose={async (tenant?: TenantInfo) => {
|
||||
if (tenant) {
|
||||
toast.success(t('tenants.tenant_created', { name: tenant.name }));
|
||||
void mutate();
|
||||
}
|
||||
setShowCreateTenantModal(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue