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

feat(console): audit log filters ()

* feat(console): audit log table

* feat(console): audit log filters
This commit is contained in:
Wang Sijie 2022-06-02 11:15:32 +08:00 committed by GitHub
parent 498b3708ef
commit a0d562f7f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 183 additions and 9 deletions
packages
console/src
components
icons
pages
AuditLogs
components
ApplicationSelector
EventSelector
index.module.scssindex.tsx
UserDetails/components
phrases/src/locales

View file

@ -37,6 +37,9 @@
}
&.small {
height: 24px;
width: 24px;
> svg {
height: 16px;
width: 16px;
@ -44,6 +47,9 @@
}
&.medium {
height: 28px;
width: 28px;
> svg {
height: 20px;
width: 20px;
@ -51,6 +57,9 @@
}
&.large {
height: 32px;
width: 32px;
> svg {
height: 24px;
width: 24px;

View file

@ -36,6 +36,13 @@
}
}
.clear {
position: absolute;
right: _.unit(2);
top: 6px;
display: none;
}
.arrow {
position: absolute;
right: _.unit(2);
@ -45,4 +52,14 @@
fill: var(--color-icon);
}
}
&.clearable:hover {
.clear {
display: block;
}
.arrow {
display: none;
}
}
}

View file

@ -1,9 +1,11 @@
import classNames from 'classnames';
import React, { ReactNode, useRef, useState } from 'react';
import React, { ReactEventHandler, ReactNode, useRef, useState } from 'react';
import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow';
import Close from '@/icons/Close';
import Dropdown, { DropdownItem } from '../Dropdown';
import IconButton from '../IconButton';
import * as styles from './index.module.scss';
type Option = {
@ -14,12 +16,22 @@ type Option = {
type Props = {
value?: string;
options: Option[];
onChange?: (value: string) => void;
onChange?: (value?: string) => void;
isReadOnly?: boolean;
hasError?: boolean;
placeholder?: ReactNode;
isClearable?: boolean;
};
const Select = ({ value, options, onChange, isReadOnly, hasError }: Props) => {
const Select = ({
value,
options,
onChange,
isReadOnly,
hasError,
placeholder,
isClearable,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<HTMLInputElement>(null);
const current = options.find((option) => value && option.value === value);
@ -29,6 +41,12 @@ const Select = ({ value, options, onChange, isReadOnly, hasError }: Props) => {
setIsOpen(false);
};
const handleClear: ReactEventHandler<HTMLButtonElement> = (event) => {
onChange?.(undefined);
setIsOpen(false);
event.stopPropagation();
};
return (
<>
<div
@ -37,7 +55,8 @@ const Select = ({ value, options, onChange, isReadOnly, hasError }: Props) => {
styles.select,
isOpen && styles.open,
isReadOnly && styles.readOnly,
hasError && styles.error
hasError && styles.error,
isClearable && value && styles.clearable
)}
role="button"
onClick={() => {
@ -46,7 +65,12 @@ const Select = ({ value, options, onChange, isReadOnly, hasError }: Props) => {
}
}}
>
{current?.title}
{current?.title ?? placeholder}
{isClearable && (
<IconButton className={styles.clear} size="small" onClick={handleClear}>
<Close />
</IconButton>
)}
<div className={styles.arrow}>{isOpen ? <KeyboardArrowUp /> : <KeyboardArrowDown />}</div>
</div>
<Dropdown

View file

@ -0,0 +1,12 @@
import React from 'react';
const Fail = () => (
<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 Fail;

View file

@ -0,0 +1,33 @@
import { adminConsoleApplicationId, Application } from '@logto/schemas';
import React from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import Select from '@/components/Select';
type Props = {
value?: string;
onChange: (value?: string) => void;
};
const ApplicationSelector = ({ value, onChange }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data } = useSWR<Application[]>('/api/applications');
const options =
data?.map(({ id, name }) => ({
value: id,
title: name,
})) ?? [];
return (
<Select
isClearable
value={value}
options={[{ value: adminConsoleApplicationId, title: 'Admin Console' }, ...options]}
placeholder={t('logs.application')}
onChange={onChange}
/>
);
};
export default ApplicationSelector;

View file

@ -0,0 +1,30 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Select from '@/components/Select';
import { logEventTitle } from '@/consts/logs';
type Props = {
value?: string;
onChange: (value?: string) => void;
};
const EventSelector = ({ value, onChange }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const options = Object.entries(logEventTitle).map(([value, title]) => ({
value,
title,
}));
return (
<Select
isClearable
value={value}
options={options}
placeholder={t('logs.event')}
onChange={onChange}
/>
);
};
export default EventSelector;

View file

@ -9,6 +9,27 @@
justify-content: space-between;
}
.filter {
display: flex;
justify-content: right;
align-items: center;
.title {
color: var(--color-caption);
font: var(--font-body-medium);
}
.eventSelector {
width: 300px;
margin-left: _.unit(2);
}
.applicationSelector {
width: 250px;
margin-left: _.unit(2);
}
}
.table {
margin-top: _.unit(4);
flex: 1;

View file

@ -1,4 +1,5 @@
import { LogDTO, LogResult } from '@logto/schemas';
import { conditionalString } from '@silverhand/essentials';
import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
@ -16,7 +17,9 @@ import UserName from '@/components/UserName';
import { RequestError } from '@/hooks/use-api';
import * as tableStyles from '@/scss/table.module.scss';
import ApplicationSelector from './components/ApplicationSelector';
import EventName from './components/EventName';
import EventSelector from './components/EventSelector';
import * as styles from './index.module.scss';
const pageSize = 20;
@ -25,8 +28,12 @@ const AuditLogs = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [query, setQuery] = useSearchParams();
const pageIndex = Number(query.get('page') ?? '1');
const event = query.get('event');
const applicationId = query.get('applicationId');
const { data, error, mutate } = useSWR<[LogDTO[], number], RequestError>(
`/api/logs?page=${pageIndex}&page_size=${pageSize}`
`/api/logs?page=${pageIndex}&page_size=${pageSize}${conditionalString(
event && `&logType=${event}`
)}${conditionalString(applicationId && `&applicationId=${applicationId}`)}`
);
const isLoading = !data && !error;
const navigate = useNavigate();
@ -37,6 +44,25 @@ const AuditLogs = () => {
<div className={styles.headline}>
<CardTitle title="logs.title" subtitle="logs.subtitle" />
</div>
<div className={styles.filter}>
<div className={styles.title}>{t('logs.filter_by')}</div>
<div className={styles.eventSelector}>
<EventSelector
value={event ?? undefined}
onChange={(value) => {
setQuery({ event: value ?? '' });
}}
/>
</div>
<div className={styles.applicationSelector}>
<ApplicationSelector
value={applicationId ?? undefined}
onChange={(value) => {
setQuery({ applicationId: value ?? '' });
}}
/>
</div>
</div>
<div className={classNames(styles.table, tableStyles.scrollable)}>
<table>
<colgroup>
@ -61,8 +87,8 @@ const AuditLogs = () => {
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={2} />}
{logs?.length === 0 && <TableEmpty columns={2} />}
{isLoading && <TableLoading columns={4} />}
{logs?.length === 0 && <TableEmpty columns={4} />}
{logs?.map(({ type, payload, createdAt, id }) => (
<tr
key={id}

View file

@ -34,7 +34,7 @@ const RoleSelect = ({ value, onChange }: Props) => {
throw new Error('Unsupported user role value');
}, [value]);
const handleChange = (value: string) => {
const handleChange = (value?: string) => {
onChange?.(value === roleAdmin ? ['admin'] : []);
};

View file

@ -538,6 +538,7 @@ const translation = {
user: 'User',
application: 'Application',
time: 'Time',
filter_by: 'Filter by',
},
session_expired: {
title: 'Session Expired',

View file

@ -534,6 +534,7 @@ const translation = {
user: '用户',
application: '应用',
time: '时间',
filter_by: '过滤',
},
session_expired: {
title: '会话已过期',