diff --git a/packages/console/src/components/AuditLogTable/index.module.scss b/packages/console/src/components/AuditLogTable/index.module.scss index a8a919643..4f60e0ae3 100644 --- a/packages/console/src/components/AuditLogTable/index.module.scss +++ b/packages/console/src/components/AuditLogTable/index.module.scss @@ -1,19 +1,9 @@ @use '@/scss/underscore' as _; -.container { - display: flex; - flex-direction: column; - height: 100%; -} - .filter { display: flex; justify-content: flex-end; align-items: center; - padding: _.unit(3); - border-bottom: 1px solid var(--color-divider); - background-color: var(--color-layer-1); - border-radius: 12px 12px 0 0; .title { color: var(--color-text-secondary); @@ -30,24 +20,3 @@ margin-left: _.unit(2); } } - -.tableLayout { - display: flex; - flex-direction: column; - max-height: 100%; - overflow-y: auto; - flex: 1; - - .tableContainer { - border-top-left-radius: 0; - border-top-right-radius: 0; - } -} - -.pagination { - margin-top: _.unit(4); -} - -.eventName { - width: 360px; -} diff --git a/packages/console/src/components/AuditLogTable/index.tsx b/packages/console/src/components/AuditLogTable/index.tsx index 2a58c86af..1ce88a74d 100644 --- a/packages/console/src/components/AuditLogTable/index.tsx +++ b/packages/console/src/components/AuditLogTable/index.tsx @@ -1,45 +1,41 @@ import type { Log } from '@logto/schemas'; import { LogResult } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; -import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import useSWR from 'swr'; import ApplicationName from '@/components/ApplicationName'; -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 { defaultPageSize } from '@/consts'; import type { RequestError } from '@/hooks/use-api'; -import * as tableStyles from '@/scss/table.module.scss'; +import usePageSearchParameters from '@/hooks/use-page-search-parameters'; import { buildUrl } from '@/utilities/url'; +import Table from '../Table'; +import type { Column } from '../Table/types'; 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; - type Props = { userId?: string; className?: string; }; -const defaultTableColumn = 4; - const AuditLogTable = ({ userId, className }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { pathname } = useLocation(); - const [query, setQuery] = useSearchParams(); - const pageIndex = Number(query.get('page') ?? '1'); - const event = query.get('event'); - const applicationId = query.get('applicationId'); + const pageSize = defaultPageSize; + const [{ page, event, applicationId }, updatePageSearchParameters] = usePageSearchParameters({ + page: 1, + event: '', + applicationId: '', + }); const url = buildUrl('/api/logs', { - page: String(pageIndex), + page: String(page), page_size: String(pageSize), ...conditional(event && { logType: event }), ...conditional(applicationId && { applicationId }), @@ -50,109 +46,89 @@ const AuditLogTable = ({ userId, className }: Props) => { const isLoading = !data && !error; const navigate = useNavigate(); const [logs, totalCount] = data ?? []; - const showUserColumn = !userId; - const tableColumnCount = showUserColumn ? defaultTableColumn : defaultTableColumn - 1; + const isUserColumnVisible = !userId; - const updateQuery = (key: string, value: string) => { - const queries: Record = {}; - - for (const [key, value] of query.entries()) { - // eslint-disable-next-line @silverhand/fp/no-mutation - queries[key] = value; - } - - setQuery({ - ...queries, - [key]: value, - }); + const eventColumn: Column = { + title: t('logs.event'), + dataIndex: 'event', + colSpan: isUserColumnVisible ? 5 : 6, + render: ({ key, payload: { result } }) => ( + + ), }; + const userColumn: Column = { + title: t('logs.user'), + dataIndex: 'user', + colSpan: 5, + render: ({ payload: { userId } }) => (userId ? :
-
), + }; + + const applicationColumn: Column = { + title: t('logs.application'), + dataIndex: 'application', + colSpan: isUserColumnVisible ? 3 : 5, + render: ({ payload: { applicationId } }) => + applicationId ? :
-
, + }; + + const timeColumn: Column = { + title: t('logs.time'), + dataIndex: 'time', + colSpan: isUserColumnVisible ? 3 : 5, + render: ({ createdAt }) => new Date(createdAt).toLocaleString(), + }; + + const columns: Array> = [ + eventColumn, + conditional(isUserColumnVisible && userColumn), + applicationColumn, + timeColumn, + // eslint-disable-next-line unicorn/prefer-native-coercion-functions + ].filter((column): column is Column => Boolean(column)); + return ( -
-
+ { + navigate(`${pathname}/${id}`); + }} + filter={
{t('logs.filter_by')}
{ - updateQuery('event', value ?? ''); + value={event} + onChange={(event) => { + updatePageSearchParameters({ event, page: undefined }); }} />
{ - updateQuery('applicationId', value ?? ''); + value={applicationId} + onChange={(applicationId) => { + updatePageSearchParameters({ applicationId, page: undefined }); }} />
-
-
- - - {showUserColumn && } - - - - - - - {showUserColumn && } - - - - - - {!data && error && ( - mutate(undefined, true)} - /> - )} - {isLoading && } - {logs?.length === 0 && } - {logs?.map(({ key, payload, createdAt, id }) => ( - { - navigate(`${pathname}/${id}`); - }} - > - - {showUserColumn && ( - - )} - - - - ))} - -
{t('logs.event')}{t('logs.user')}{t('logs.application')}{t('logs.time')}
- - {payload.userId ? : '-'} - {payload.applicationId ? ( - - ) : ( - '-' - )} - {new Date(createdAt).toLocaleString()}
-
-
- { - updateQuery('page', String(page)); - }} - /> - + } + pagination={{ + pageIndex: Number(page), + totalCount, + pageSize, + onChange: (page) => { + updatePageSearchParameters({ page }); + }, + }} + isLoading={isLoading} + errorMessage={error?.body?.message ?? error?.message} + onRetry={async () => mutate(undefined, true)} + /> ); }; diff --git a/packages/console/src/components/Table/index.tsx b/packages/console/src/components/Table/index.tsx index 9682488d8..ab2c24f52 100644 --- a/packages/console/src/components/Table/index.tsx +++ b/packages/console/src/components/Table/index.tsx @@ -99,14 +99,14 @@ const Table = < {!hasData && errorMessage && ( )} - {!isLoading && !hasData && placeholder && ( + {!isLoading && !hasData && ( - {placeholder.content} + {placeholder?.content} )} {rowGroups.map(({ key, label, labelClassName, data }) => ( diff --git a/packages/console/src/hooks/use-page-search-parameters.ts b/packages/console/src/hooks/use-page-search-parameters.ts new file mode 100644 index 000000000..7df1fd72b --- /dev/null +++ b/packages/console/src/hooks/use-page-search-parameters.ts @@ -0,0 +1,62 @@ +import { conditional } from '@silverhand/essentials'; +import { useCallback } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +type Parameters = Record; + +type UsePageSearchParametersReturn = [ + { + [K in keyof T]: T[K]; + }, + (parameters: Partial) => void +]; + +/** + * Manage page search parameters + * + * @param config Define search parameter keys and their default value. E.g., `{ page: 1, keyword: '' }` + * @returns [pageSearchParams, updatePageSearchParams] + */ +const usePageSearchParameters = ( + config: T +): UsePageSearchParametersReturn => { + const [searchParameters, setSearchParameters] = useSearchParams(); + + const updatePageSearchParameters = useCallback( + (parameters: Partial) => { + const baseParameters = new URLSearchParams(searchParameters); + + for (const key of Object.keys(parameters)) { + // eslint-disable-next-line no-restricted-syntax + const value = parameters[key as keyof Partial]; + + if (value === undefined) { + baseParameters.delete(key); + } else { + baseParameters.set(key, String(value)); + } + } + + setSearchParameters(baseParameters); + }, + [searchParameters, setSearchParameters] + ); + + return [ + // eslint-disable-next-line no-restricted-syntax + Object.fromEntries( + Object.entries(config).map(([parameterKey, defaultValue]) => { + const locationParameterValue = searchParameters.get(parameterKey); + const parameterValue = + typeof defaultValue === 'string' + ? locationParameterValue ?? defaultValue + : conditional(locationParameterValue && Number(locationParameterValue)) ?? defaultValue; + + return [parameterKey, parameterValue]; + }) + ) as UsePageSearchParametersReturn[0], + updatePageSearchParameters, + ]; +}; + +export default usePageSearchParameters; diff --git a/packages/console/src/pages/AuditLogs/index.tsx b/packages/console/src/pages/AuditLogs/index.tsx index d5bdc11bf..e4dd54f90 100644 --- a/packages/console/src/pages/AuditLogs/index.tsx +++ b/packages/console/src/pages/AuditLogs/index.tsx @@ -8,9 +8,7 @@ const AuditLogs = () => {
-
- -
+ ); };