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

feat(console): user roles page (#2860)

This commit is contained in:
Xiao Yijun 2023-01-09 15:34:22 +08:00 committed by GitHub
parent 385966625e
commit ac01933c0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 240 additions and 3 deletions

View file

@ -45,6 +45,7 @@ import ApiResourceSettings from './pages/ApiResourceDetails/ApiResourceSettings'
import RolePermissions from './pages/RoleDetails/RolePermissions';
import RoleSettings from './pages/RoleDetails/RoleSettings';
import UserLogs from './pages/UserDetails/UserLogs';
import UserRoles from './pages/UserDetails/UserRoles';
import UserSettings from './pages/UserDetails/UserSettings';
import { getBasename } from './utilities/router';
@ -101,6 +102,7 @@ const Main = () => {
<Route path=":id" element={<UserDetails />}>
<Route index element={<Navigate replace to={UserDetailsTabs.Settings} />} />
<Route path={UserDetailsTabs.Settings} element={<UserSettings />} />
<Route path={UserDetailsTabs.Roles} element={<UserRoles />} />
<Route path={UserDetailsTabs.Logs} element={<UserLogs />}>
<Route path=":logId" element={<AuditLogDetails />} />
</Route>

View file

@ -16,6 +16,7 @@ export enum SignInExperiencePage {
export enum UserDetailsTabs {
Settings = 'settings',
Roles = 'roles',
Logs = 'logs',
}

View file

@ -0,0 +1,20 @@
@use '@/scss/underscore' as _;
.rolesTable {
margin-bottom: _.unit(6);
color: var(--color-text);
.filter {
display: flex;
justify-content: space-between;
align-items: center;
}
tbody {
td {
max-width: 100%;
@include _.text-ellipsis;
font: var(--font-body-medium);
}
}
}

View file

@ -0,0 +1,138 @@
import type { Role } from '@logto/schemas';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import useSWR from 'swr';
import Delete from '@/assets/images/delete.svg';
import Plus from '@/assets/images/plus.svg';
import Button from '@/components/Button';
import ConfirmModal from '@/components/ConfirmModal';
import IconButton from '@/components/IconButton';
import Search from '@/components/Search';
import Table from '@/components/Table';
import TextLink from '@/components/TextLink';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import type { UserDetailsOutletContext } from '../types';
import * as styles from './index.module.scss';
const UserRoles = () => {
const {
user: { id: userId },
} = useOutletContext<UserDetailsOutletContext>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: roles, error, mutate } = useSWR<Role[], RequestError>(`/api/users/${userId}/roles`);
const isLoading = !roles && !error;
const [roleToBeDeleted, setRoleToBeDeleted] = useState<Role>();
const [isDeleting, setIsDeleting] = useState(false);
const api = useApi();
const handleDelete = async () => {
if (!roleToBeDeleted || isDeleting) {
return;
}
setIsDeleting(true);
try {
await api.delete(`/api/users/${userId}/roles/${roleToBeDeleted.id}`);
toast.success(t('user_details.roles.deleted', { name: roleToBeDeleted.name }));
await mutate();
setRoleToBeDeleted(undefined);
} finally {
setIsDeleting(false);
}
};
return (
<>
<Table
className={styles.rolesTable}
isLoading={isLoading}
rowGroups={[{ key: 'roles', data: roles }]}
rowIndexKey="id"
columns={[
{
title: t('user_details.roles.name_column'),
dataIndex: 'name',
colSpan: 6,
render: ({ id, name }) => (
<TextLink to={`/roles/${id}`} target="_blank">
{name}
</TextLink>
),
},
{
title: t('user_details.roles.name_column'),
dataIndex: 'description',
colSpan: 9,
render: ({ description }) => description,
},
{
title: null,
dataIndex: 'delete',
colSpan: 1,
render: (role) => (
<IconButton
onClick={() => {
setRoleToBeDeleted(role);
}}
>
<Delete />
</IconButton>
),
},
]}
filter={
<div className={styles.filter}>
<Search />
<Button
title="user_details.roles.assign_button"
type="primary"
size="large"
icon={<Plus />}
onClick={() => {
// TODO @xiaoyijun assign roles to user
}}
/>
</div>
}
placeholder={{
content: (
<Button
title="user_details.roles.assign_button"
type="outline"
onClick={() => {
// TODO @xiaoyijun assign roles to user
}}
/>
),
}}
errorMessage={error?.body?.message ?? error?.message}
onRetry={async () => mutate(undefined, true)}
/>
{roleToBeDeleted && (
<ConfirmModal
isOpen
isLoading={isDeleting}
confirmButtonText="general.delete"
onCancel={() => {
setRoleToBeDeleted(undefined);
}}
onConfirm={handleDelete}
>
{t('user_details.roles.delete_description')}
</ConfirmModal>
)}
</>
);
};
export default UserRoles;

View file

@ -32,7 +32,8 @@ import { UserDetailsOutletContext } from './types';
const UserDetails = () => {
const { pathname } = useLocation();
const isOnLogsPage = pathname.endsWith(UserDetailsTabs.Logs);
const isPageHasTable =
pathname.endsWith(UserDetailsTabs.Roles) || pathname.endsWith(UserDetailsTabs.Logs);
const { id } = useParams();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);
@ -67,7 +68,7 @@ const UserDetails = () => {
};
return (
<div className={classNames(detailsStyles.container, isOnLogsPage && styles.resourceLayout)}>
<div className={classNames(detailsStyles.container, isPageHasTable && styles.resourceLayout)}>
<TextLink to="/users" icon={<Back />} className={styles.backLink}>
{t('user_details.back_to_users')}
</TextLink>
@ -151,7 +152,10 @@ const UserDetails = () => {
</Card>
<TabNav>
<TabNavItem href={`/users/${data.id}/${UserDetailsTabs.Settings}`}>
{t('general.settings_nav')}
{t('user_details.tab_settings')}
</TabNavItem>
<TabNavItem href={`/users/${data.id}/${UserDetailsTabs.Roles}`}>
{t('user_details.tab_roles')}
</TabNavItem>
<TabNavItem href={`/users/${data.id}/${UserDetailsTabs.Logs}`}>
{t('user_details.tab_logs')}

View file

@ -16,6 +16,8 @@ const user_details = {
congratulations: 'Der Benutzer wurde erfolgreich zurückgesetzt',
new_password: 'Neues Passwort:',
},
tab_settings: 'Settings', // UNTRANSLATED
tab_roles: 'Roles', // UNTRANSLATED
tab_logs: 'Benutzer-Logs',
settings: 'Settings', // UNTRANSLATED
settings_description:
@ -40,6 +42,13 @@ const user_details = {
'Du entfernst die bestehende <name/> Identität. Bist du sicher, dass du das tun willst?',
},
suspended: 'Suspended', // UNTRANSLATED
roles: {
name_column: 'Role', // UNTRANSLATED
description_column: 'Description', // UNTRANSLATED
assign_button: 'Assign Roles', // UNTRANSLATED
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;

View file

@ -14,6 +14,8 @@ const user_details = {
congratulations: 'This user has been reset',
new_password: 'New password:',
},
tab_settings: 'Settings',
tab_roles: 'Roles',
tab_logs: 'User logs',
settings: 'Settings',
settings_description:
@ -38,6 +40,13 @@ const user_details = {
'You are removing the existing <name/> identity. Are you sure you want to do that?',
},
suspended: 'Suspended',
roles: {
name_column: 'Role',
description_column: 'Description',
assign_button: 'Assign Roles',
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;

View file

@ -16,6 +16,8 @@ const user_details = {
congratulations: 'Cet utilisateur a été réinitialisé',
new_password: 'Nouveau mot de passe :',
},
tab_settings: 'Settings', // UNTRANSLATED
tab_roles: 'Roles', // UNTRANSLATED
tab_logs: "Journaux de l'utilisateur",
settings: 'Settings', // UNTRANSLATED
settings_description:
@ -40,6 +42,13 @@ const user_details = {
"Vous supprimez l'identité existante <nom/>. Etes-vous sûr de vouloir faire ça ?",
},
suspended: 'Suspended', // UNTRANSLATED
roles: {
name_column: 'Role', // UNTRANSLATED
description_column: 'Description', // UNTRANSLATED
assign_button: 'Assign Roles', // UNTRANSLATED
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;

View file

@ -14,6 +14,8 @@ const user_details = {
congratulations: '해당 사용자의 비밀번호가 성공적으로 초기화 되었어요.',
new_password: '새로운 비밀번호:',
},
tab_settings: 'Settings', // UNTRANSLATED
tab_roles: 'Roles', // UNTRANSLATED
tab_logs: '사용자 기록',
settings: '설정',
settings_description:
@ -37,6 +39,13 @@ const user_details = {
deletion_confirmation: '<name/> 신원을 삭제하려고 해요. 정말로 진행할까요?',
},
suspended: '정지됨',
roles: {
name_column: 'Role', // UNTRANSLATED
description_column: 'Description', // UNTRANSLATED
assign_button: 'Assign Roles', // UNTRANSLATED
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;

View file

@ -14,6 +14,8 @@ const user_details = {
congratulations: 'Este usuário foi redefinido',
new_password: 'Nova senha:',
},
tab_settings: 'Settings', // UNTRANSLATED
tab_roles: 'Roles', // UNTRANSLATED
tab_logs: 'Logs',
settings: 'Configurações',
settings_description:
@ -38,6 +40,13 @@ const user_details = {
'Você está removendo a identidade <name/> existente. Você tem certeza que deseja fazer isso?',
},
suspended: 'Suspenso',
roles: {
name_column: 'Role', // UNTRANSLATED
description_column: 'Description', // UNTRANSLATED
assign_button: 'Assign Roles', // UNTRANSLATED
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;

View file

@ -16,6 +16,8 @@ const user_details = {
congratulations: 'Este utilizador foi redefinido',
new_password: 'Nova password:',
},
tab_settings: 'Settings', // UNTRANSLATED
tab_roles: 'Roles', // UNTRANSLATED
tab_logs: 'Registros do utilizador',
settings: 'Settings', // UNTRANSLATED
settings_description:
@ -40,6 +42,13 @@ const user_details = {
'Está removendo a identidade <name/> existente. Tem a certeza que deseja fazer isso?',
},
suspended: 'Suspended', // UNTRANSLATED
roles: {
name_column: 'Role', // UNTRANSLATED
description_column: 'Description', // UNTRANSLATED
assign_button: 'Assign Roles', // UNTRANSLATED
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;

View file

@ -14,6 +14,8 @@ const user_details = {
congratulations: 'Bu kullanıcı sıfırlandı',
new_password: 'Yeni şifre:',
},
tab_settings: 'Settings', // UNTRANSLATED
tab_roles: 'Roles', // UNTRANSLATED
tab_logs: 'Kullanıcı kayıtları',
settings: 'Settings', // UNTRANSLATED
settings_description:
@ -38,6 +40,13 @@ const user_details = {
'Mevcut <name/> kimliğini kaldırıyorsunuz. Bunu yapmak istediğinizden emin misiniz?',
},
suspended: 'Suspended', // UNTRANSLATED
roles: {
name_column: 'Role', // UNTRANSLATED
description_column: 'Description', // UNTRANSLATED
assign_button: 'Assign Roles', // UNTRANSLATED
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;

View file

@ -14,6 +14,8 @@ const user_details = {
congratulations: '该用户已被重置',
new_password: '新密码:',
},
tab_settings: 'Settings', // UNTRANSLATED
tab_roles: 'Roles', // UNTRANSLATED
tab_logs: '用户日志',
settings: 'Settings', // UNTRANSLATED
settings_description:
@ -36,6 +38,13 @@ const user_details = {
deletion_confirmation: '你在正要删除现有的 <name /> 身份,是否确认?',
},
suspended: '已禁用',
roles: {
name_column: 'Role', // UNTRANSLATED
description_column: 'Description', // UNTRANSLATED
assign_button: 'Assign Roles', // UNTRANSLATED
delete_description: 'TBD', // UNTRANSLATED
deleted: 'The role {{name}} has been successfully deleted from the user.', // UNTRANSLATED
},
};
export default user_details;