0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-14 23:11:31 -05:00

feat(console): init users page ()

This commit is contained in:
Wang Sijie 2022-03-14 17:13:24 +08:00 committed by GitHub
parent f2bfc30ef9
commit f0ab8f0c96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 273 additions and 3 deletions
packages
console/src
App.tsx
components/Sidebar
pages/Users
phrases/src/locales

View file

@ -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>

View file

@ -62,7 +62,7 @@ export const sections: SidebarSection[] = [
items: [
{
Icon: UserProfile,
title: 'user_management',
title: 'users',
},
{
Icon: List,

View file

@ -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);
}

View file

@ -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;

View 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;
}

View 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;

View file

@ -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',
},
},
};

View file

@ -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: '姓名',
},
},
};