From 27d57d7563401f3a411123778ad2f97b1c005660 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 18 May 2026 18:58:07 +0800 Subject: [PATCH] feat: instance pv events --- src/locales/en-US/gpuservice.ts | 2 + src/locales/ja-JP/gpuservice.ts | 2 + src/locales/ru-RU/gpuservice.ts | 2 + src/locales/tr-TR/gpuservice.ts | 2 + src/locales/zh-CN/gpuservice.ts | 2 + src/pages/gpu-service/instances/apis/index.ts | 25 ++ .../components/view-events-modal.tsx | 283 ++++++++++-------- .../services/use-query-volume-events.ts | 46 +++ 8 files changed, 243 insertions(+), 121 deletions(-) create mode 100644 src/pages/gpu-service/instances/services/use-query-volume-events.ts diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index 938aa806..88f2df4e 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -76,6 +76,8 @@ export default { 'gpuservice.instance.event.lastSeen': 'Last Seen', 'gpuservice.instance.event.recentHourTip': 'Only events from the last hour are shown', + 'gpuservice.instance.event.tab.instance': 'Instance Events', + 'gpuservice.instance.event.tab.volume': 'Volume Events', 'gpuservice.instance.recreate.confirm.title': 'Confirm recreation', 'gpuservice.instance.recreate.confirm.content': 'The current instance will be deleted first, then recreated with the current configuration.\n {name}', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index e9ce2b3e..50887b7e 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -74,6 +74,8 @@ export default { 'gpuservice.instance.event.lastSeen': '最終発生', 'gpuservice.instance.event.recentHourTip': '直近 1 時間のイベントのみ表示されます', + 'gpuservice.instance.event.tab.instance': 'インスタンスイベント', + 'gpuservice.instance.event.tab.volume': 'ボリュームイベント', 'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか', 'gpuservice.instance.recreate.confirm.content': '現在のインスタンスを削除した後、現在の構成で再作成します。\n {name}', diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index 79fbff5b..8c0d9fa1 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -75,6 +75,8 @@ export default { 'gpuservice.instance.event.lastSeen': 'Последнее событие', 'gpuservice.instance.event.recentHourTip': 'Отображаются только события за последний час', + 'gpuservice.instance.event.tab.instance': 'События экземпляра', + 'gpuservice.instance.event.tab.volume': 'События тома', 'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание', 'gpuservice.instance.recreate.confirm.content': 'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n {name}', diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index 066ece9c..b4067925 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -74,6 +74,8 @@ export default { 'gpuservice.instance.event.lastSeen': 'Son Görülen', 'gpuservice.instance.event.recentHourTip': 'Yalnızca son bir saatteki olaylar gösterilir', + 'gpuservice.instance.event.tab.instance': 'Örnek Olayları', + 'gpuservice.instance.event.tab.volume': 'Birim Olayları', 'gpuservice.instance.recreate.confirm.title': 'Yeniden oluşturma onaylansın mı', 'gpuservice.instance.recreate.confirm.content': diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index c589e2fe..9f436751 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -69,6 +69,8 @@ export default { 'gpuservice.instance.event.count': '次数', 'gpuservice.instance.event.lastSeen': '最近发生', 'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。', + 'gpuservice.instance.event.tab.instance': '实例事件', + 'gpuservice.instance.event.tab.volume': '存储卷事件', 'gpuservice.instance.recreate.confirm.title': '确认重新创建', 'gpuservice.instance.recreate.confirm.content': '系统将先删除当前实例,然后使用当前配置重新创建。\n {name}', diff --git a/src/pages/gpu-service/instances/apis/index.ts b/src/pages/gpu-service/instances/apis/index.ts index c7099a69..c815a63e 100644 --- a/src/pages/gpu-service/instances/apis/index.ts +++ b/src/pages/gpu-service/instances/apis/index.ts @@ -187,6 +187,31 @@ export async function queryGPUServiceInstanceEvents( ); } +export async function queryGPUServiceInstancePVEvents( + params: { + namespace: string; + name: string; + clusterID?: number; + }, + options?: any +) { + if (!params.clusterID) { + return; + } + return request( + `${GPU_SERVICE_INSTANCE_PV_EVENTS_API({ + namespace: params.namespace, + clusterID: params.clusterID, + name: params.name + })}`, + { + method: 'GET', + params: omitPathParams(_.omit(params, ['name'])), + cancelToken: options?.token + } + ); +} + export async function queryGPUServiceInstanceLog( params: InstanceLogQueryParams & { namespace: string; diff --git a/src/pages/gpu-service/instances/components/view-events-modal.tsx b/src/pages/gpu-service/instances/components/view-events-modal.tsx index 6d9ed3b8..8e535b44 100644 --- a/src/pages/gpu-service/instances/components/view-events-modal.tsx +++ b/src/pages/gpu-service/instances/components/view-events-modal.tsx @@ -1,4 +1,3 @@ -import { createAxiosToken } from '@/hooks/use-chunk-request'; import { ReloadOutlined } from '@ant-design/icons'; import { AlertBlockInfo, @@ -7,12 +6,13 @@ import { StatusTag } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Button, Table } from 'antd'; +import { Button, Table, Tabs, TabsProps } from 'antd'; import type { ColumnsType } from 'antd/lib/table'; import dayjs from 'dayjs'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { queryGPUServiceInstanceEvents } from '../apis'; +import React, { useEffect, useMemo, useState } from 'react'; import { InstanceEventItem } from '../config/types'; +import useQueryInstanceEvents from '../services/use-query-instance-events'; +import useQueryVolumeEvents from '../services/use-query-volume-events'; type ViewEventsModalProps = { open: boolean; @@ -31,119 +31,170 @@ const eventTypeStatus: Record = { const ViewEventsModal: React.FC = (props) => { const intl = useIntl(); - const { open, onCancel, name, namespace, clusterID } = props || {}; - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(false); - const tokenRef = useRef | null>(null); + const { open, onCancel, name } = props || {}; + const [activeKey, setActiveKey] = useState('instance'); - const fetchEvents = useCallback(async () => { - if (!name || !clusterID || !namespace) return; - tokenRef.current?.cancel?.(); - const source = createAxiosToken(); - tokenRef.current = source; - setLoading(true); - try { - const res = await queryGPUServiceInstanceEvents( - { name, namespace, clusterID }, - { token: source.token } - ); - setEvents(res?.items ?? []); - } catch (e) { - // request cancellation or network failure — keep prior items - } finally { - setLoading(false); - } - }, [name, namespace, clusterID]); + const { + detailData: instanceEventsData, + loading: instanceLoading, + cancelRequest: cancelInstanceRequest, + fetchData: fetchInstanceEvents + } = useQueryInstanceEvents(); + + const { + detailData: volumeEventsData, + loading: volumeLoading, + cancelRequest: cancelVolumeRequest, + fetchData: fetchVolumeEvents + } = useQueryVolumeEvents(); + + const instanceEvents = instanceEventsData?.items ?? []; + const volumeEvents = volumeEventsData?.items ?? []; + + const refreshAll = () => { + if (!name) return; + fetchInstanceEvents({ name }); + fetchVolumeEvents({ name }); + }; useEffect(() => { if (open) { - fetchEvents(); + refreshAll(); } else { - tokenRef.current?.cancel?.(); - setEvents([]); + cancelInstanceRequest(); + cancelVolumeRequest(); } return () => { - tokenRef.current?.cancel?.(); + cancelInstanceRequest(); + cancelVolumeRequest(); }; - }, [open, fetchEvents]); + }, [open]); - const handleCancel = useCallback(() => { - tokenRef.current?.cancel?.(); + const handleCancel = () => { + cancelInstanceRequest(); + cancelVolumeRequest(); onCancel(); - }, [onCancel]); + }; - const columns: ColumnsType = [ - { - title: intl.formatMessage({ id: 'common.table.type' }), - dataIndex: 'type', - key: 'type', - width: 110, - render: (value: string) => ( - - ) - }, - { - title: intl.formatMessage({ id: 'gpuservice.instance.event.reason' }), - dataIndex: 'reason', - key: 'reason', - width: 180, - ellipsis: { showTitle: false }, - render: (text: string) => ( - - {text || '-'} - - ) - }, - { - title: intl.formatMessage({ id: 'gpuservice.instance.event.message' }), - dataIndex: 'message', - key: 'message', - ellipsis: { showTitle: false }, - render: (text: string) => {text || '-'} - }, - { - title: intl.formatMessage({ id: 'gpuservice.instance.event.source' }), - key: 'source', - width: 200, - ellipsis: { showTitle: false }, - render: (_text, record) => { - const src = - record.source?.component || - record.reportingComponent || - record.reportingInstance || - '-'; - return ( - - {src} - - ); - } - }, - { - title: intl.formatMessage({ id: 'gpuservice.instance.event.count' }), - dataIndex: 'count', - key: 'count', - width: 80, - render: (value?: number) => value ?? '-' - }, - { - title: intl.formatMessage({ id: 'gpuservice.instance.event.lastSeen' }), - key: 'lastTimestamp', - width: 180, - render: (_text, record) => - formatRelative( - record.lastTimestamp || - record.series?.lastObservedTime || - record.eventTime + const columns: ColumnsType = useMemo( + () => [ + { + title: intl.formatMessage({ id: 'common.table.type' }), + dataIndex: 'type', + key: 'type', + width: 110, + render: (value: string) => ( + ) + }, + { + title: intl.formatMessage({ id: 'gpuservice.instance.event.reason' }), + dataIndex: 'reason', + key: 'reason', + width: 180, + ellipsis: { showTitle: false }, + render: (text: string) => ( + + {text || '-'} + + ) + }, + { + title: intl.formatMessage({ id: 'gpuservice.instance.event.message' }), + dataIndex: 'message', + key: 'message', + ellipsis: { showTitle: false }, + render: (text: string) => {text || '-'} + }, + { + title: intl.formatMessage({ id: 'gpuservice.instance.event.source' }), + key: 'source', + width: 200, + ellipsis: { showTitle: false }, + render: (_text, record) => { + const src = + record.source?.component || + record.reportingComponent || + record.reportingInstance || + '-'; + return ( + + {src} + + ); + } + }, + { + title: intl.formatMessage({ id: 'gpuservice.instance.event.count' }), + dataIndex: 'count', + key: 'count', + width: 80, + render: (value?: number) => value ?? '-' + }, + { + title: intl.formatMessage({ + id: 'gpuservice.instance.event.lastSeen' + }), + key: 'lastTimestamp', + width: 180, + render: (_text, record) => + formatRelative( + record.lastTimestamp || + record.series?.lastObservedTime || + record.eventTime + ) + } + ], + [intl] + ); + + const renderTable = ( + dataSource: InstanceEventItem[], + tableLoading: boolean + ) => ( + + `${record.metadata?.name}-${record.metadata?.namespace}` + } + loading={{ + spinning: tableLoading, + size: 'middle' + }} + virtual + scroll={{ y: 'calc(100vh - 80px)' }} + pagination={false} + /> + ); + + const tabItems: TabsProps['items'] = [ + { + key: 'instance', + label: intl.formatMessage({ + id: 'gpuservice.instance.event.tab.instance' + }), + children: renderTable(instanceEvents, instanceLoading) + }, + { + key: 'volume', + label: intl.formatMessage({ + id: 'gpuservice.instance.event.tab.volume' + }), + children: renderTable(volumeEvents, volumeLoading) } ]; + const isLoading = instanceLoading || volumeLoading; + return ( = (props) => { type="text" size="small" icon={} - onClick={fetchEvents} - loading={loading} + onClick={refreshAll} + loading={isLoading} /> } @@ -188,23 +239,13 @@ const ViewEventsModal: React.FC = (props) => { contentStyle={{ paddingInline: 0 }} > -
- `${record.metadata?.name}-${record.metadata.namespace}` - } - loading={{ - spinning: loading, - size: 'middle' - }} - virtual - scroll={{ y: 'calc(100vh - 80px)' }} - pagination={false} - >
+ ); }; diff --git a/src/pages/gpu-service/instances/services/use-query-volume-events.ts b/src/pages/gpu-service/instances/services/use-query-volume-events.ts new file mode 100644 index 00000000..d9266994 --- /dev/null +++ b/src/pages/gpu-service/instances/services/use-query-volume-events.ts @@ -0,0 +1,46 @@ +import { currentClusterAtom } from '@/atoms/gpuservice'; +import { getCurrentOrgNamespace } from '@/atoms/user'; +import { useQueryData } from '@gpustack/core-ui'; +import { useAtomValue } from 'jotai'; +import { useCallback } from 'react'; +import { queryGPUServiceInstancePVEvents } from '../apis'; +import { InstanceEvents } from '../config/types'; + +interface QueryVolumeEventsParams { + name: string; + pretty?: string; +} + +export default function useQueryVolumeEvents() { + const currentCluster = useAtomValue(currentClusterAtom); + const clusterID = currentCluster?.id; + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); + + const fetchDetail = useCallback( + (params: QueryVolumeEventsParams, options?: any) => + queryGPUServiceInstancePVEvents( + { + namespace, + clusterID, + name: params.name + }, + options + ), + [namespace, clusterID] + ); + + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + InstanceEvents, + QueryVolumeEventsParams + >({ + fetchDetail, + key: 'volumeEvents' + }); + + return { + detailData, + loading, + cancelRequest, + fetchData + }; +}