0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

feat(console): create user (#388)

* feat(console): create user

* fix: handle error
This commit is contained in:
Wang Sijie 2022-03-16 14:45:15 +08:00 committed by GitHub
parent f0ff2b3906
commit a77b94231e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 175 additions and 16 deletions

View file

@ -0,0 +1,16 @@
import React, { SVGProps } from 'react';
const Eye = (props: SVGProps<SVGSVGElement>) => (
<svg width="20" height="14" viewBox="0 0 20 14" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M13 7C13 8.65685 11.6569 10 10 10C8.34315 10 7 8.65685 7 7C7 5.34315 8.34315 4 10 4C11.6569 4 13 5.34315 13 7Z"
fill="#747778"
/>
<path
d="M19.8944 6.55279C17.7362 2.23635 13.9031 0 10 0C6.09687 0 2.26379 2.23635 0.105573 6.55279C-0.0351909 6.83431 -0.0351909 7.16569 0.105573 7.44721C2.26379 11.7637 6.09687 14 10 14C13.9031 14 17.7362 11.7637 19.8944 7.44721C20.0352 7.16569 20.0352 6.83431 19.8944 6.55279ZM10 12C7.03121 12 3.99806 10.3792 2.12966 7C3.99806 3.62078 7.03121 2 10 2C12.9688 2 16.0019 3.62078 17.8703 7C16.0019 10.3792 12.9688 12 10 12Z"
fill="#747778"
/>
</svg>
);
export default Eye;

View file

@ -0,0 +1,51 @@
@use '@/scss/underscore' as _;
.card {
min-width: _.unit(100);
}
.header h1 {
font: var(--font-title-large);
margin-top: 0;
}
.body {
font: var(--font-body-2);
.info {
margin-top: _.unit(6);
background: var(--color-neutral-variant-90);
padding: _.unit(5);
border-radius: _.unit(2);
.infoLine {
display: flex;
align-items: center;
&:not(:last-child) {
margin-bottom: _.unit(2);
}
.infoContent {
font-weight: bold;
padding-left: _.unit(1);
}
.operation {
padding-left: _.unit(1);
}
}
}
}
.footer {
border-top: 1px solid var(--color-neutral-80);
margin-top: _.unit(6);
padding-top: _.unit(6);
display: flex;
justify-content: right;
button:not(:last-child) {
margin-right: _.unit(2);
}
}

View file

@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { useSearchParams } from 'react-router-dom';
import Button from '@/components/Button';
import Card from '@/components/Card';
import Eye from '@/icons/Eye';
import * as modalStyles from '@/scss/modal.module.scss';
import * as styles from './CreateSuccess.module.scss';
type Props = {
username: string;
};
const CreateSuccess = ({ username }: Props) => {
const [searchParameters, setSearchParameters] = useSearchParams();
const [passwordVisible, setPasswordVisible] = useState(false);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const passwordEncoded = searchParameters.get('password');
const password = passwordEncoded && atob(passwordEncoded);
const handleClose = () => {
setSearchParameters({});
};
const handleCopy = async () => {
if (!password) {
return null;
}
await navigator.clipboard.writeText(
`User username: ${username}\nInitial password: ${password}`
);
toast.success(t('copy.copied'));
};
if (!password) {
return null;
}
return (
<ReactModal isOpen className={modalStyles.content} overlayClassName={modalStyles.overlay}>
<Card className={styles.card}>
<div className={styles.header}>
<h1>{t('user_details.created_title')}</h1>
</div>
<div className={styles.body}>
<div>{t('user_details.created_guide')}</div>
<div className={styles.info}>
<div className={styles.infoLine}>
<div>{t('user_details.created_username')}</div>
<div className={styles.infoContent}>{username}</div>
</div>
<div className={styles.infoLine}>
<div>{t('user_details.created_password')}</div>
<div className={styles.infoContent}>
{passwordVisible ? password : password.replace(/./g, '*')}
</div>
<div className={styles.operation}>
{/* TODO: Replaced into IconButton(LOG-1890) */}
<Eye
onClick={() => {
setPasswordVisible((previous) => !previous);
}}
/>
</div>
</div>
</div>
</div>
<div className={styles.footer}>
<Button title="admin_console.user_details.created_button_close" onClick={handleClose} />
<Button
type="primary"
title="admin_console.user_details.created_button_copy"
onClick={handleCopy}
/>
</div>
</Card>
</ReactModal>
);
};
export default CreateSuccess;

View file

@ -10,6 +10,7 @@ import CopyToClipboard from '@/components/CopyToClipboard';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import { RequestError } from '@/swr';
import CreateSuccess from './components/CreateSuccess';
import * as styles from './index.module.scss';
const UserDetails = () => {
@ -42,6 +43,7 @@ const UserDetails = () => {
<Card>TBD</Card>
</>
)}
{data && <CreateSuccess username={data.username ?? '-'} />}
</div>
);
};

View file

@ -19,20 +19,15 @@ type FormData = {
};
type Props = {
onClose?: (createdUser?: User) => void;
onClose?: (createdUser?: User, password?: string) => void;
};
const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, register } = useForm<FormData>();
const onSubmit = handleSubmit(async (data) => {
try {
const createdUser = await api.post('/api/users', { json: data }).json<User>();
onClose?.(createdUser);
} catch (error: unknown) {
console.error(error);
}
const createdUser = await api.post('/api/users', { json: data }).json<User>();
onClose?.(createdUser, btoa(data.password));
});
return (
@ -61,7 +56,7 @@ const CreateForm = ({ onClose }: Props) => {
title="admin_console.users.create_form_password"
className={styles.textField}
>
<TextInput {...register('name', { required: true })} />
<TextInput {...register('password', { required: true })} />
</FormField>
<div className={styles.submit}>
<Button

View file

@ -1,5 +1,4 @@
import { User } from '@logto/schemas';
import { conditional } from '@silverhand/essentials/lib/utilities/conditional.js';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Modal from 'react-modal';
@ -20,7 +19,7 @@ import * as styles from './index.module.scss';
const Users = () => {
const [isCreateFormOpen, setIsCreateFormOpen] = useState(false);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data, error, mutate } = useSWR<User[], RequestError>('/api/users');
const { data, error } = useSWR<User[], RequestError>('/api/users');
const isLoading = !data && !error;
const navigate = useNavigate();
@ -41,13 +40,11 @@ const Users = () => {
overlayClassName={modalStyles.overlay}
>
<CreateForm
onClose={(createdUser) => {
onClose={(createdUser, password) => {
setIsCreateFormOpen(false);
if (createdUser) {
void mutate(conditional(data && [...data, createdUser]));
navigate(`/applications/${createdUser.id}/get-started`);
if (createdUser && password) {
navigate(`/users/${createdUser.id}?password=${password}`);
}
}}
/>

View file

@ -161,6 +161,12 @@ const translation = {
},
user_details: {
back_to_users: 'Back to user management',
created_title: 'Congratulations! This user has been created.',
created_guide: 'Now send this following information.',
created_username: 'User username:',
created_password: 'Initial password:',
created_button_close: 'Close',
created_button_copy: 'Copy',
},
},
};

View file

@ -162,6 +162,12 @@ const translation = {
},
user_details: {
back_to_users: '返回用户管理',
created_title: '恭喜!用户创建成功',
created_guide: '用户信息如下',
created_username: '用户名:',
created_password: '初始密码:',
created_button_close: '关闭',
created_button_copy: '拷贝',
},
},
};