mirror of
https://github.com/logto-io/logto.git
synced 2025-04-14 23:11:31 -05:00
feat(console): init users page (#380)
This commit is contained in:
parent
f2bfc30ef9
commit
f0ab8f0c96
8 changed files with 273 additions and 3 deletions
packages
|
@ -16,6 +16,7 @@ import ApplicationDetails from './pages/ApplicationDetails';
|
|||
import Applications from './pages/Applications';
|
||||
import ConnectorDetails from './pages/ConnectorDetails';
|
||||
import Connectors from './pages/Connectors';
|
||||
import Users from './pages/Users';
|
||||
import { fetcher } from './swr';
|
||||
|
||||
const isBasenameNeeded = process.env.NODE_ENV !== 'development' || process.env.PORT === '5002';
|
||||
|
@ -54,6 +55,9 @@ const Main = () => {
|
|||
<Route path="social" element={<Connectors />} />
|
||||
<Route path=":connectorId" element={<ConnectorDetails />} />
|
||||
</Route>
|
||||
<Route path="users">
|
||||
<Route index element={<Users />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Content>
|
||||
</div>
|
||||
|
|
|
@ -62,7 +62,7 @@ export const sections: SidebarSection[] = [
|
|||
items: [
|
||||
{
|
||||
Icon: UserProfile,
|
||||
title: 'user_management',
|
||||
title: 'users',
|
||||
},
|
||||
{
|
||||
Icon: List,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.card {
|
||||
padding: _.unit(8);
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> *:not(:first-child) {
|
||||
margin-left: _.unit(3);
|
||||
}
|
||||
|
||||
> svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: _.unit(8);
|
||||
}
|
||||
|
||||
.textField {
|
||||
@include _.form-text-field;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: _.unit(8);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.error {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-error);
|
||||
margin-top: _.unit(2);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import { User } from '@logto/schemas';
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import FormField from '@/components/FormField';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import Close from '@/icons/Close';
|
||||
import api from '@/utilities/api';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type FormData = {
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onClose?: (createdUser?: User) => 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);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.headline}>
|
||||
<CardTitle title="users.create" subtitle="users.subtitle" />
|
||||
<Close onClick={() => onClose?.()} />
|
||||
</div>
|
||||
<form className={styles.form} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
isRequired
|
||||
title="admin_console.users.create_form_username"
|
||||
className={styles.textField}
|
||||
>
|
||||
<TextInput {...register('username', { required: true })} />
|
||||
</FormField>
|
||||
<FormField
|
||||
isRequired
|
||||
title="admin_console.users.create_form_name"
|
||||
className={styles.textField}
|
||||
>
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
</FormField>
|
||||
<FormField
|
||||
isRequired
|
||||
title="admin_console.users.create_form_password"
|
||||
className={styles.textField}
|
||||
>
|
||||
<TextInput {...register('name', { required: true })} />
|
||||
</FormField>
|
||||
<div className={styles.submit}>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
title="admin_console.users.create"
|
||||
size="large"
|
||||
type="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateForm;
|
26
packages/console/src/pages/Users/index.module.scss
Normal file
26
packages/console/src/pages/Users/index.module.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: _.unit(4);
|
||||
|
||||
tbody {
|
||||
max-height: calc(100vh - _.unit(64));
|
||||
|
||||
tr.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-table-row-selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userName {
|
||||
width: 360px;
|
||||
}
|
101
packages/console/src/pages/Users/index.tsx
Normal file
101
packages/console/src/pages/Users/index.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
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';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import ImagePlaceholder from '@/components/ImagePlaceholder';
|
||||
import ItemPreview from '@/components/ItemPreview';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { RequestError } from '@/swr';
|
||||
|
||||
import CreateForm from './components/CreateForm';
|
||||
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 isLoading = !data && !error;
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className={styles.headline}>
|
||||
<CardTitle title="users.title" subtitle="users.subtitle" />
|
||||
<Button
|
||||
title="admin_console.users.create"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setIsCreateFormOpen(true);
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={isCreateFormOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
>
|
||||
<CreateForm
|
||||
onClose={(createdUser) => {
|
||||
setIsCreateFormOpen(false);
|
||||
|
||||
if (createdUser) {
|
||||
void mutate(conditional(data && [...data, createdUser]));
|
||||
|
||||
navigate(`/applications/${createdUser.id}/get-started`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('users.user_name')}</th>
|
||||
<th>{t('users.application_name')}</th>
|
||||
<th>{t('users.latest_sign_in')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{error && (
|
||||
<tr>
|
||||
<td colSpan={2}>error occurred: {error.metadata.code}</td>
|
||||
</tr>
|
||||
)}
|
||||
{isLoading && (
|
||||
<tr>
|
||||
<td colSpan={2}>loading</td>
|
||||
</tr>
|
||||
)}
|
||||
{data?.map(({ id, name, username }) => (
|
||||
<tr
|
||||
key={id}
|
||||
className={styles.clickable}
|
||||
onClick={() => {
|
||||
navigate(`/users/${id}`);
|
||||
}}
|
||||
>
|
||||
<td className={styles.userName}>
|
||||
<ItemPreview
|
||||
title={name ?? '-'}
|
||||
subtitle={username ?? '-'}
|
||||
icon={<ImagePlaceholder />}
|
||||
to={`/users/${id}`}
|
||||
/>
|
||||
</td>
|
||||
<td>Application</td>
|
||||
<td>Last sign in</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
|
@ -41,7 +41,7 @@ const translation = {
|
|||
api_resources: 'API Resources',
|
||||
sign_in_experience: 'Sign-in Experience',
|
||||
connectors: 'Connectors',
|
||||
user_management: 'User Management',
|
||||
users: 'User Management',
|
||||
audit_logs: 'Audit Logs',
|
||||
documentation: 'Documentation',
|
||||
community_support: 'Community Support',
|
||||
|
@ -128,6 +128,18 @@ const translation = {
|
|||
test_message_sent: 'Test Message Sent!',
|
||||
test_sender_description: 'Test sender description',
|
||||
},
|
||||
users: {
|
||||
title: 'User Management',
|
||||
subtitle:
|
||||
'The tab for managing users, creating a new user, and editing user profiles. Every registered user can be found here.',
|
||||
create: 'Add user',
|
||||
user_name: 'User',
|
||||
application_name: 'Apps',
|
||||
latest_sign_in: 'Latest sign in',
|
||||
create_form_username: 'Username',
|
||||
create_form_password: 'Password',
|
||||
create_form_name: 'Full name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ const translation = {
|
|||
api_resources: 'API 资源',
|
||||
sign_in_experience: '登录体验',
|
||||
connectors: '连接器',
|
||||
user_management: '用户管理',
|
||||
users: '用户管理',
|
||||
audit_logs: '审计日志',
|
||||
documentation: '文档',
|
||||
community_support: '社区支持',
|
||||
|
@ -130,6 +130,17 @@ const translation = {
|
|||
test_message_sent: 'Test Message Sent!',
|
||||
test_sender_description: 'Test sender description',
|
||||
},
|
||||
users: {
|
||||
title: '用户管理',
|
||||
subtitle: '管理已注册用户, 创建新用户,编辑用户资料。',
|
||||
create: '添加用户',
|
||||
user_name: '用户',
|
||||
application_name: '应用',
|
||||
latest_sign_in: '最后登录',
|
||||
create_form_username: '用户名',
|
||||
create_form_password: '密码',
|
||||
create_form_name: '姓名',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue