diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index 2a7e8c1a6..6f22cce83 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -16,6 +16,7 @@ import Callback from './pages/Callback'; import ConnectorDetails from './pages/ConnectorDetails'; import Connectors from './pages/Connectors'; import GetStarted from './pages/GetStarted'; +import UserDetails from './pages/UserDetails'; import Users from './pages/Users'; import { fetcher } from './swr'; @@ -60,6 +61,7 @@ const Main = () => { } /> + } /> diff --git a/packages/console/src/icons/Eye.tsx b/packages/console/src/icons/Eye.tsx new file mode 100644 index 000000000..441e0f1b7 --- /dev/null +++ b/packages/console/src/icons/Eye.tsx @@ -0,0 +1,16 @@ +import React, { SVGProps } from 'react'; + +const Eye = (props: SVGProps) => ( + + + + +); + +export default Eye; diff --git a/packages/console/src/pages/UserDetails/components/CreateSuccess.module.scss b/packages/console/src/pages/UserDetails/components/CreateSuccess.module.scss new file mode 100644 index 000000000..3462f6f8c --- /dev/null +++ b/packages/console/src/pages/UserDetails/components/CreateSuccess.module.scss @@ -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); + } +} diff --git a/packages/console/src/pages/UserDetails/components/CreateSuccess.tsx b/packages/console/src/pages/UserDetails/components/CreateSuccess.tsx new file mode 100644 index 000000000..f317f1251 --- /dev/null +++ b/packages/console/src/pages/UserDetails/components/CreateSuccess.tsx @@ -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 ( + + + + {t('user_details.created_title')} + + + {t('user_details.created_guide')} + + + {t('user_details.created_username')} + {username} + + + {t('user_details.created_password')} + + {passwordVisible ? password : password.replace(/./g, '*')} + + + {/* TODO: Replaced into IconButton(LOG-1890) */} + { + setPasswordVisible((previous) => !previous); + }} + /> + + + + + + + + + + + ); +}; + +export default CreateSuccess; diff --git a/packages/console/src/pages/UserDetails/index.module.scss b/packages/console/src/pages/UserDetails/index.module.scss new file mode 100644 index 000000000..d8cb1dffb --- /dev/null +++ b/packages/console/src/pages/UserDetails/index.module.scss @@ -0,0 +1,70 @@ +@use '@/scss/underscore' as _; + +.container { + > *:not(:first-child) { + margin-top: _.unit(4); + } +} + +.container .backButton { + display: flex; + align-items: center; + + > *:not(:first-child) { + margin-left: _.unit(1); + } +} + +.container .header { + padding: _.unit(8); + display: flex; + align-items: center; + justify-content: space-between; + + > *:not(:first-child) { + margin-left: _.unit(6); + } + + .metadata { + flex: 1; + + > div { + display: flex; + align-items: center; + + &:not(:first-child) { + margin-top: _.unit(2); + } + + > *:not(:first-child) { + margin-left: _.unit(2); + } + } + + .name { + font: var(--font-title-large); + color: var(--color-component-text); + } + + .username { + background-color: var(--color-neutral-90); + padding: _.unit(0.5) _.unit(2); + border-radius: 10px; + font: var(--font-label-medium); + } + + .text { + font: var(--font-subhead-2); + color: var(--color-component-caption); + } + + .verticalBar { + @include _.vertical-bar; + height: 12px; + } + + .copy { + padding: _.unit(1) _.unit(4) _.unit(1) _.unit(2); + } + } +} diff --git a/packages/console/src/pages/UserDetails/index.tsx b/packages/console/src/pages/UserDetails/index.tsx new file mode 100644 index 000000000..5725dcaed --- /dev/null +++ b/packages/console/src/pages/UserDetails/index.tsx @@ -0,0 +1,51 @@ +import { User } from '@logto/schemas'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import useSWR from 'swr'; + +import BackLink from '@/components/BackLink'; +import Card from '@/components/Card'; +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 = () => { + const { id } = useParams(); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const { data, error } = useSWR(id && `/api/users/${id}`); + const isLoading = !data && !error; + + return ( + + {t('user_details.back_to_users')} + + {isLoading && loading} + {error && {`error occurred: ${error.metadata.code}`}} + {data && ( + <> + + + + {data.name ?? '-'} + + {data.username} + + User ID + + + + + TBD + > + )} + {data && } + + ); +}; + +export default UserDetails; diff --git a/packages/console/src/pages/Users/components/CreateForm/index.tsx b/packages/console/src/pages/Users/components/CreateForm/index.tsx index cf2a88a7b..0727986ef 100644 --- a/packages/console/src/pages/Users/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Users/components/CreateForm/index.tsx @@ -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(); const onSubmit = handleSubmit(async (data) => { - try { - const createdUser = await api.post('/api/users', { json: data }).json(); - - onClose?.(createdUser); - } catch (error: unknown) { - console.error(error); - } + const createdUser = await api.post('/api/users', { json: data }).json(); + onClose?.(createdUser, btoa(data.password)); }); return ( @@ -61,7 +56,7 @@ const CreateForm = ({ onClose }: Props) => { title="admin_console.users.create_form_password" className={styles.textField} > - + { const [isCreateFormOpen, setIsCreateFormOpen] = useState(false); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { data, error, mutate } = useSWR('/api/users'); + const { data, error } = useSWR('/api/users'); const isLoading = !data && !error; const navigate = useNavigate(); @@ -41,13 +40,11 @@ const Users = () => { overlayClassName={modalStyles.overlay} > { + 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}`); } }} /> diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 38df07870..fb68e9a1d 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -169,6 +169,15 @@ const translation = { create_form_password: 'Password', create_form_name: 'Full name', }, + 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', + }, }, }; diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 02b59c86d..eabf5f983 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -170,6 +170,15 @@ const translation = { create_form_password: '密码', create_form_name: '姓名', }, + user_details: { + back_to_users: '返回用户管理', + created_title: '恭喜!用户创建成功', + created_guide: '用户信息如下', + created_username: '用户名:', + created_password: '初始密码:', + created_button_close: '关闭', + created_button_copy: '拷贝', + }, }, };