0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): audit log table (#1000)

This commit is contained in:
Wang Sijie 2022-06-01 16:54:24 +08:00 committed by GitHub
parent 19380d0873
commit fdd12de1cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 321 additions and 1 deletions

View file

@ -18,6 +18,7 @@ import ApiResourceDetails from './pages/ApiResourceDetails';
import ApiResources from './pages/ApiResources';
import ApplicationDetails from './pages/ApplicationDetails';
import Applications from './pages/Applications';
import AuditLogs from './pages/AuditLogs';
import Callback from './pages/Callback';
import ConnectorDetails from './pages/ConnectorDetails';
import Connectors from './pages/Connectors';
@ -84,6 +85,9 @@ const Main = () => {
<Route index element={<Users />} />
<Route path=":id" element={<UserDetails />} />
</Route>
<Route path="audit-logs">
<Route index element={<AuditLogs />} />
</Route>
<Route path="sign-in-experience">
<Route index element={<Navigate to="experience" />} />
<Route path=":tab" element={<SignInExperience />} />

View file

@ -0,0 +1,12 @@
@use '@/scss/underscore' as _;
.userName {
font: var(--body-medium);
color: var(--color-text);
.userId {
font: var(--body-small);
color: var(--color-caption);
margin-left: _.unit(1);
}
}

View file

@ -0,0 +1,32 @@
import { User } from '@logto/schemas';
import React from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import { RequestError } from '@/hooks/use-api';
import * as styles from './index.module.scss';
type Props = {
userId: string;
};
const UserName = ({ userId }: Props) => {
const { data, error } = useSWR<User, RequestError>(`/api/users/${userId}`);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const isLoading = !data && !error;
if (isLoading) {
return null;
}
return (
<div className={styles.userName}>
<span>{data?.name ?? t('users.unnamed')}</span>
<span className={styles.userId}>{userId}</span>
</div>
);
};
export default UserName;

View file

@ -0,0 +1,21 @@
type LogEventTitle = Record<string, string>;
export const logEventTitle: LogEventTitle = Object.freeze({
RegisterUsernamePassword: 'Register with username and password',
RegisterEmailSendPasscode: 'Register with email (send passcode)',
RegisterEmail: 'Register with email',
RegisterSmsSendPasscode: 'Register with SMS (send passcode)',
RegisterSms: 'Register with SMS',
RegisterSocialBind: 'Bind social account',
RegisterSocial: 'Register with social account',
SignInUsernamePassword: 'Sign in with username and password',
SignInEmailSendPasscode: 'Sign in with email (send passcode)',
SignInEmail: 'Register with email',
SignInSmsSendPasscode: 'Sign in with SMS (send passcode)',
SignInSms: 'Sign in with SMS',
SignInSocialBind: 'Sign in with social related account',
SignInSocial: 'Sign in with social account',
CodeExchangeToken: 'Exchange token by auth code',
RefreshTokenExchangeToken: 'Exchagne token by refresh token',
RevokeToken: 'Revoke token',
});

View file

@ -0,0 +1,12 @@
import React from 'react';
const Failed = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.2664 11.9937L19.5317 6.7284C20.1561 6.10454 20.1561 5.09229 19.5317 4.4679C18.9073 3.84403 17.8961 3.84403 17.2717 4.4679L12.0065 9.73317L6.74119 4.46843C6.11732 3.84403 5.10508 3.84403 4.48068 4.46843C3.85682 5.09229 3.85682 6.10454 4.48068 6.72893L9.74596 11.9937L4.4679 17.2717C3.84403 17.8956 3.84403 18.9078 4.4679 19.5317C4.77956 19.8439 5.18925 20 5.59788 20C6.00651 20 6.4162 19.8439 6.7284 19.5317L12.0059 14.2542L17.2707 19.5189C17.5829 19.8311 17.992 19.9872 18.4007 19.9872C18.8093 19.9872 19.2184 19.8311 19.5306 19.5189C20.155 18.8951 20.155 17.8828 19.5306 17.2589L14.2664 11.9937Z"
fill="currentColor"
/>
</svg>
);
export default Failed;

View file

@ -0,0 +1,12 @@
import React from 'react';
const Success = () => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19.6767 4.22177C19.039 3.80823 18.1876 3.98978 17.7745 4.62705L10.0676 16.5065L6.30911 13.0313C5.75115 12.5155 4.88143 12.5494 4.36565 13.1074C3.84988 13.6649 3.8838 14.5355 4.44176 15.0508L9.37673 19.6135C9.37673 19.6135 9.5184 19.7355 9.58396 19.7781C9.81503 19.9285 10.0745 20 10.3313 20C10.7815 20 11.223 19.779 11.4866 19.3728L20.082 6.12396C20.4955 5.48668 20.314 4.63485 19.6767 4.22177Z"
fill="currentColor"
/>
</svg>
);
export default Success;

View file

@ -0,0 +1,28 @@
@use '@/scss/underscore' as _;
.eventName {
display: flex;
align-items: center;
white-space: nowrap;
.icon {
flex-shrink: 0;
margin-right: _.unit(1);
width: 24px;
height: 24px;
&.success {
color: var(--color-success-60);
}
&.fail {
color: var(--color-error-40);
}
}
.title {
font: var(--font-body-medium);
color: var(--color-text-link);
text-decoration: none;
}
}

View file

@ -0,0 +1,43 @@
import classNames from 'classnames';
import React from 'react';
import { Link } from 'react-router-dom';
import { logEventTitle } from '@/consts/logs';
import Failed from '@/icons/Failed';
import Success from '@/icons/Success';
import * as styles from './index.module.scss';
type Props = {
type: string;
isSuccess: boolean;
to?: string;
};
const EventName = ({ type, isSuccess, to }: Props) => {
const title = logEventTitle[type] ?? type;
return (
<div className={styles.eventName}>
<div className={classNames(styles.icon, isSuccess ? styles.success : styles.fail)}>
{isSuccess ? <Success /> : <Failed />}
</div>
<div>
{to && (
<Link
className={styles.title}
to={to}
onClick={(event) => {
event.stopPropagation();
}}
>
{title}
</Link>
)}
{!to && <div className={styles.title}>{title}</div>}
</div>
</div>
);
};
export default EventName;

View file

@ -0,0 +1,24 @@
@use '@/scss/underscore' as _;
.card {
@include _.flex-column;
}
.headline {
display: flex;
justify-content: space-between;
}
.table {
margin-top: _.unit(4);
flex: 1;
}
.pagination {
margin-top: _.unit(4);
min-height: 32px;
}
.eventName {
width: 360px;
}

View file

@ -0,0 +1,106 @@
import { LogDTO, LogResult } from '@logto/schemas';
import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
import useSWR from 'swr';
import ApplicationName from '@/components/ApplicationName';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import Pagination from '@/components/Pagination';
import TableEmpty from '@/components/Table/TableEmpty';
import TableError from '@/components/Table/TableError';
import TableLoading from '@/components/Table/TableLoading';
import UserName from '@/components/UserName';
import { RequestError } from '@/hooks/use-api';
import * as tableStyles from '@/scss/table.module.scss';
import EventName from './components/EventName';
import * as styles from './index.module.scss';
const pageSize = 20;
const AuditLogs = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [query, setQuery] = useSearchParams();
const pageIndex = Number(query.get('page') ?? '1');
const { data, error, mutate } = useSWR<[LogDTO[], number], RequestError>(
`/api/logs?page=${pageIndex}&page_size=${pageSize}`
);
const isLoading = !data && !error;
const navigate = useNavigate();
const [logs, totalCount] = data ?? [];
return (
<Card className={styles.card}>
<div className={styles.headline}>
<CardTitle title="logs.title" subtitle="logs.subtitle" />
</div>
<div className={classNames(styles.table, tableStyles.scrollable)}>
<table>
<colgroup>
<col className={styles.eventName} />
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th>{t('logs.event')}</th>
<th>{t('logs.user')}</th>
<th>{t('logs.application')}</th>
<th>{t('logs.time')}</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={4}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={2} />}
{logs?.length === 0 && <TableEmpty columns={2} />}
{logs?.map(({ type, payload, createdAt, id }) => (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(`/audit-logs/${id}`);
}}
>
<td>
<EventName type={type} isSuccess={payload.result === LogResult.Success} />
</td>
<td>{payload.userId ? <UserName userId={payload.userId} /> : '-'}</td>
<td>
{payload.applicationId ? (
<ApplicationName applicationId={payload.applicationId} />
) : (
'-'
)}
</td>
<td>{new Date(createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className={styles.pagination}>
{!!totalCount && (
<Pagination
pageCount={Math.ceil(totalCount / pageSize)}
pageIndex={pageIndex}
onChange={(page) => {
setQuery({ page: String(page) });
}}
/>
)}
</div>
</Card>
);
};
export default AuditLogs;

View file

@ -7,6 +7,7 @@ import Modal from 'react-modal';
import { useNavigate, useSearchParams } from 'react-router-dom';
import useSWR from 'swr';
import ApplicationName from '@/components/ApplicationName';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
@ -23,7 +24,6 @@ import Plus from '@/icons/Plus';
import * as modalStyles from '@/scss/modal.module.scss';
import * as tableStyles from '@/scss/table.module.scss';
import ApplicationName from './components/ApplicationName';
import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss';

View file

@ -531,6 +531,14 @@ const translation = {
weekly_active_users: 'Weeky active users',
monthly_active_users: 'Monthly active users',
},
logs: {
title: 'Audit Logs',
subtitle: 'View log data of authentication events made by your admin and users.',
event: 'Event',
user: 'User',
application: 'Application',
time: 'Time',
},
session_expired: {
title: 'Session Expired',
subtitle:

View file

@ -527,6 +527,14 @@ const translation = {
weekly_active_users: '周活用户',
monthly_active_users: '月活用户',
},
logs: {
title: '审计日志',
subtitle: '查看用户日志',
event: '事件',
user: '用户',
application: '应用',
time: '时间',
},
session_expired: {
title: '会话已过期',
subtitle: '会话可能已过期,您已被登出. 请点击下方按钮重新登录到管理界面.',

View file

@ -1,3 +1,5 @@
import { Log } from '../db-entries';
export enum LogResult {
Success = 'Success',
Error = 'Error',
@ -137,3 +139,11 @@ export type LogPayloads = {
export type LogType = keyof LogPayloads;
export type LogPayload = LogPayloads[LogType];
export type LogDTO = Omit<Log, 'payload'> & {
payload: {
userId?: string;
applicationId?: string;
result?: string;
};
};