diff --git a/src/locales/en-US/usage.ts b/src/locales/en-US/usage.ts index 588f2e76..67438fc8 100644 --- a/src/locales/en-US/usage.ts +++ b/src/locales/en-US/usage.ts @@ -90,6 +90,7 @@ export default { // --- Resource events --- 'usage.events.resourceType': 'Resource type', 'usage.events.eventType': 'Event type', + 'usage.events.resourceName': 'Filter by name', 'usage.events.col.time': 'Time', 'usage.events.col.resource': 'Resource', 'usage.events.col.event': 'Event', diff --git a/src/locales/ja-JP/usage.ts b/src/locales/ja-JP/usage.ts index 8740a752..dfc09e70 100644 --- a/src/locales/ja-JP/usage.ts +++ b/src/locales/ja-JP/usage.ts @@ -90,6 +90,7 @@ export default { // --- Resource events --- 'usage.events.resourceType': 'リソースタイプ', 'usage.events.eventType': 'イベントタイプ', + 'usage.events.resourceName': '名前で絞り込み', 'usage.events.col.time': '時刻', 'usage.events.col.resource': 'リソース', 'usage.events.col.event': 'イベント', diff --git a/src/locales/ru-RU/usage.ts b/src/locales/ru-RU/usage.ts index 2d27e472..b15dabf7 100644 --- a/src/locales/ru-RU/usage.ts +++ b/src/locales/ru-RU/usage.ts @@ -90,6 +90,7 @@ export default { // --- Resource events --- 'usage.events.resourceType': 'Тип ресурса', 'usage.events.eventType': 'Тип события', + 'usage.events.resourceName': 'Фильтр по имени', 'usage.events.col.time': 'Время', 'usage.events.col.resource': 'Ресурс', 'usage.events.col.event': 'Событие', diff --git a/src/locales/tr-TR/usage.ts b/src/locales/tr-TR/usage.ts index ecbc6e74..e82d4356 100644 --- a/src/locales/tr-TR/usage.ts +++ b/src/locales/tr-TR/usage.ts @@ -90,6 +90,7 @@ export default { // --- Resource events --- 'usage.events.resourceType': 'Kaynak türü', 'usage.events.eventType': 'Olay türü', + 'usage.events.resourceName': 'Ada göre filtrele', 'usage.events.col.time': 'Zaman', 'usage.events.col.resource': 'Kaynak', 'usage.events.col.event': 'Olay', diff --git a/src/locales/zh-CN/usage.ts b/src/locales/zh-CN/usage.ts index 7c588eb9..b967d73d 100644 --- a/src/locales/zh-CN/usage.ts +++ b/src/locales/zh-CN/usage.ts @@ -90,6 +90,7 @@ export default { // --- Resource events --- 'usage.events.resourceType': '资源类型', 'usage.events.eventType': '事件类型', + 'usage.events.resourceName': '按名称筛选', 'usage.events.col.time': '时间', 'usage.events.col.resource': '资源', 'usage.events.col.event': '事件', diff --git a/src/pages/usage/apis/resource.ts b/src/pages/usage/apis/resource.ts index 7eafad2b..dc873cb4 100644 --- a/src/pages/usage/apis/resource.ts +++ b/src/pages/usage/apis/resource.ts @@ -399,6 +399,7 @@ export async function queryResourceEvents( scope?: 'self' | 'all'; filters?: ResourceUsageFilters; resource_types?: string[]; + resource_name?: string; event_types?: string[]; page?: number; perPage?: number; @@ -412,9 +413,13 @@ export async function queryResourceEvents( end_date: data.end_date, scope: data.scope ?? 'all', resource_type: data.resource_types?.[0], - // GET endpoints take creator_ids as a CSV string (avoids axios array - // serialization quirks); the server splits it back into a list. + // GET endpoints take list params as CSV strings (avoids axios array + // serialization quirks); the server splits them back into lists. ...(creatorIds?.length ? { creator_ids: creatorIds.join(',') } : {}), + ...(data.event_types?.length + ? { event_types: data.event_types.join(',') } + : {}), + ...(data.resource_name ? { resource_name: data.resource_name } : {}), page: data.page ?? 1, perPage: data.perPage ?? 50 }, diff --git a/src/pages/usage/components/resource-events.tsx b/src/pages/usage/components/resource-events.tsx index ac2d0726..9d6f18cf 100644 --- a/src/pages/usage/components/resource-events.tsx +++ b/src/pages/usage/components/resource-events.tsx @@ -9,25 +9,31 @@ * event-type selects ride in the bar's ``extra`` slot. */ import { useAccess, useIntl } from '@umijs/max'; -import { Select, Table, Tag } from 'antd'; +import { Input, Select, Table, Tag } from 'antd'; import dayjs from 'dayjs'; -import React, { useEffect, useMemo, useState } from 'react'; +import _ from 'lodash'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; import { queryResourceEvents, ResourceEventItem, ResourceEventsResponse } from '../apis/resource'; -import useResourceMeta from '../hooks/use-resource-meta'; import ResourceFilterBar from './resource-filter-bar'; +// Only these four are ever emitted (see resource_event_logger): create/delete +// + the metering-window pair. updated/attached/detached exist as enum values +// but are intentionally not recorded, so they're not offered as filters. const EVENT_COLOR: Record = { created: 'green', deleted: 'red', phase_to_metered: 'blue', - phase_left_metered: 'orange', - updated: 'cyan', - attached: 'purple', - detached: 'gold' + phase_left_metered: 'orange' }; // Humanize a failure phase enum for display, e.g. "SSHPublicKeyCreateFailed" → @@ -85,18 +91,6 @@ const ResourceEvents: React.FC = () => { { value: 'phase_left_metered', label: intl.formatMessage({ id: 'usage.events.type.stopped' }) - }, - { - value: 'updated', - label: intl.formatMessage({ id: 'usage.events.type.updated' }) - }, - { - value: 'attached', - label: intl.formatMessage({ id: 'usage.events.type.attached' }) - }, - { - value: 'detached', - label: intl.formatMessage({ id: 'usage.events.type.detached' }) } ], [intl] @@ -112,42 +106,70 @@ const ResourceEvents: React.FC = () => { [EVENT_TYPE_OPTIONS] ); - const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>([ - dayjs().subtract(29, 'day'), - dayjs() - ]); - const [selectedUsers, setSelectedUsers] = useState([]); - const [resourceTypes, setResourceTypes] = useState([]); - const [eventTypes, setEventTypes] = useState([]); - const [page, setPage] = useState(1); - const [data, setData] = useState(null); - const [refreshKey, setRefreshKey] = useState(0); - - const { creators: userOptions } = useResourceMeta(scope); - - const fetch = async () => { - try { - const res = await queryResourceEvents({ - start_date: dateRange[0].format('YYYY-MM-DD'), - end_date: dateRange[1].format('YYYY-MM-DD'), - scope, - filters: selectedUsers.length - ? { creator_ids: selectedUsers } - : undefined, - resource_types: resourceTypes, - event_types: eventTypes, - page, - perPage: 50 - }); - setData(res); - } catch { - // Keep last response on failure. - } + // All filters live in one query-params object; ``resourceType`` is single + // (the backend filters by one) and ``nameQuery`` is the debounced substring. + type QueryParams = { + dateRange: [dayjs.Dayjs, dayjs.Dayjs]; + resourceType?: string; + eventTypes: string[]; + nameQuery: string; + page: number; }; + const [queryParams, setQueryParams] = useState({ + dateRange: [dayjs().subtract(29, 'day'), dayjs()], + resourceType: undefined, + eventTypes: [], + nameQuery: '', + page: 1 + }); + const [data, setData] = useState(null); + // Latest params, so the stable debounced name handler reads current values. + const queryRef = useRef(queryParams); + queryRef.current = queryParams; + + // Single fetch entry point: merge the patch into the current params, persist + // them, then request — triggered from each handler rather than from effect + // dependencies, so there's exactly one request per user action. + const fetchEvents = useCallback( + async (patch: Partial) => { + const params = { ...queryRef.current, ...patch }; + setQueryParams(params); + try { + const res = await queryResourceEvents({ + start_date: params.dateRange[0].format('YYYY-MM-DD'), + end_date: params.dateRange[1].format('YYYY-MM-DD'), + scope, + resource_types: params.resourceType + ? [params.resourceType] + : undefined, + resource_name: params.nameQuery || undefined, + event_types: params.eventTypes, + page: params.page, + perPage: 50 + }); + setData(res); + } catch { + // Keep last response on failure. + } + }, + [scope] + ); + + // First load only — subsequent fetches are driven by the handlers below. useEffect(() => { - fetch(); - }, [dateRange, selectedUsers, resourceTypes, eventTypes, page, refreshKey]); + fetchEvents({}); + }, []); + + // Debounced name search; a filter change always resets to page 1. + const onNameChange = useMemo( + () => + _.debounce( + (value: string) => fetchEvents({ nameQuery: value.trim(), page: 1 }), + 400 + ), + [fetchEvents] + ); const columns = useMemo( () => [ @@ -204,32 +226,32 @@ const ResourceEvents: React.FC = () => { return (
{ - setDateRange(dates); - setPage(1); - }} - canManageUsers={canManageUsers} - userOptions={userOptions} - selectedUsers={selectedUsers} - onUsersChange={(ids) => { - setSelectedUsers(ids); - setPage(1); - }} - onRefresh={() => setRefreshKey((k) => k + 1)} + value={queryParams.dateRange} + onChange={(dates) => fetchEvents({ dateRange: dates, page: 1 })} + // No user filter here — the events list has no User column. Scope is + // still applied to the query (members see only their own events). + canManageUsers={false} + userOptions={[]} + selectedUsers={[]} + onUsersChange={() => {}} + onRefresh={() => fetchEvents({})} extra={ <> + onNameChange(e.target.value)} + style={{ width: 200 }} + />