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} > - +