diff --git a/packages/console/src/components/IconButton/index.module.scss b/packages/console/src/components/IconButton/index.module.scss index ffddf5cec..d62425206 100644 --- a/packages/console/src/components/IconButton/index.module.scss +++ b/packages/console/src/components/IconButton/index.module.scss @@ -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; diff --git a/packages/console/src/components/Select/index.module.scss b/packages/console/src/components/Select/index.module.scss index 5134f2149..a64cda538 100644 --- a/packages/console/src/components/Select/index.module.scss +++ b/packages/console/src/components/Select/index.module.scss @@ -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; + } + } } diff --git a/packages/console/src/components/Select/index.tsx b/packages/console/src/components/Select/index.tsx index 7882c52c9..baa33ce85 100644 --- a/packages/console/src/components/Select/index.tsx +++ b/packages/console/src/components/Select/index.tsx @@ -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(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 = (event) => { + onChange?.(undefined); + setIsOpen(false); + event.stopPropagation(); + }; + return ( <>
{ 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 && ( + + + + )}
{isOpen ? : }
( + + + +); + +export default Fail; diff --git a/packages/console/src/pages/AuditLogs/components/ApplicationSelector/index.tsx b/packages/console/src/pages/AuditLogs/components/ApplicationSelector/index.tsx new file mode 100644 index 000000000..de93a3cb6 --- /dev/null +++ b/packages/console/src/pages/AuditLogs/components/ApplicationSelector/index.tsx @@ -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('/api/applications'); + const options = + data?.map(({ id, name }) => ({ + value: id, + title: name, + })) ?? []; + + return ( + + ); +}; + +export default EventSelector; diff --git a/packages/console/src/pages/AuditLogs/index.module.scss b/packages/console/src/pages/AuditLogs/index.module.scss index 12bf9022c..fba4f4d6c 100644 --- a/packages/console/src/pages/AuditLogs/index.module.scss +++ b/packages/console/src/pages/AuditLogs/index.module.scss @@ -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; diff --git a/packages/console/src/pages/AuditLogs/index.tsx b/packages/console/src/pages/AuditLogs/index.tsx index 5c5df95cf..cae4cd20b 100644 --- a/packages/console/src/pages/AuditLogs/index.tsx +++ b/packages/console/src/pages/AuditLogs/index.tsx @@ -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 = () => {
+
+
{t('logs.filter_by')}
+
+ { + setQuery({ event: value ?? '' }); + }} + /> +
+
+ { + setQuery({ applicationId: value ?? '' }); + }} + /> +
+
@@ -61,8 +87,8 @@ const AuditLogs = () => { onRetry={async () => mutate(undefined, true)} /> )} - {isLoading && } - {logs?.length === 0 && } + {isLoading && } + {logs?.length === 0 && } {logs?.map(({ type, payload, createdAt, id }) => ( { throw new Error('Unsupported user role value'); }, [value]); - const handleChange = (value: string) => { + const handleChange = (value?: string) => { onChange?.(value === roleAdmin ? ['admin'] : []); }; diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index 8fcf5f309..8e01ded89 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -538,6 +538,7 @@ const translation = { user: 'User', application: 'Application', time: 'Time', + filter_by: 'Filter by', }, session_expired: { title: 'Session Expired', diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 82673a82e..3cbeccd10 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -534,6 +534,7 @@ const translation = { user: '用户', application: '应用', time: '时间', + filter_by: '过滤', }, session_expired: { title: '会话已过期',