feat: instance pv events

This commit is contained in:
jialin
2026-05-25 22:42:02 +08:00
committed by jialin
parent 2ebb8f44b7
commit 27d57d7563
8 changed files with 243 additions and 121 deletions
+2
View File
@@ -76,6 +76,8 @@ export default {
'gpuservice.instance.event.lastSeen': 'Last Seen', 'gpuservice.instance.event.lastSeen': 'Last Seen',
'gpuservice.instance.event.recentHourTip': 'gpuservice.instance.event.recentHourTip':
'Only events from the last hour are shown', '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.title': 'Confirm recreation',
'gpuservice.instance.recreate.confirm.content': 'gpuservice.instance.recreate.confirm.content':
'The current instance will be deleted first, then recreated with the current configuration.\n <span style="font-size: 13px;font-weight: 700">{name}</span>', 'The current instance will be deleted first, then recreated with the current configuration.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
+2
View File
@@ -74,6 +74,8 @@ export default {
'gpuservice.instance.event.lastSeen': '最終発生', 'gpuservice.instance.event.lastSeen': '最終発生',
'gpuservice.instance.event.recentHourTip': 'gpuservice.instance.event.recentHourTip':
'直近 1 時間のイベントのみ表示されます', '直近 1 時間のイベントのみ表示されます',
'gpuservice.instance.event.tab.instance': 'インスタンスイベント',
'gpuservice.instance.event.tab.volume': 'ボリュームイベント',
'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか', 'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか',
'gpuservice.instance.recreate.confirm.content': 'gpuservice.instance.recreate.confirm.content':
'現在のインスタンスを削除した後、現在の構成で再作成します。\n <span style="font-size: 13px;font-weight: 700">{name}</span>', '現在のインスタンスを削除した後、現在の構成で再作成します。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
+2
View File
@@ -75,6 +75,8 @@ export default {
'gpuservice.instance.event.lastSeen': 'Последнее событие', 'gpuservice.instance.event.lastSeen': 'Последнее событие',
'gpuservice.instance.event.recentHourTip': 'gpuservice.instance.event.recentHourTip':
'Отображаются только события за последний час', 'Отображаются только события за последний час',
'gpuservice.instance.event.tab.instance': 'События экземпляра',
'gpuservice.instance.event.tab.volume': 'События тома',
'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание', 'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание',
'gpuservice.instance.recreate.confirm.content': 'gpuservice.instance.recreate.confirm.content':
'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n <span style="font-size: 13px;font-weight: 700">{name}</span>', 'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
+2
View File
@@ -74,6 +74,8 @@ export default {
'gpuservice.instance.event.lastSeen': 'Son Görülen', 'gpuservice.instance.event.lastSeen': 'Son Görülen',
'gpuservice.instance.event.recentHourTip': 'gpuservice.instance.event.recentHourTip':
'Yalnızca son bir saatteki olaylar gösterilir', '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': 'gpuservice.instance.recreate.confirm.title':
'Yeniden oluşturma onaylansın mı', 'Yeniden oluşturma onaylansın mı',
'gpuservice.instance.recreate.confirm.content': 'gpuservice.instance.recreate.confirm.content':
+2
View File
@@ -69,6 +69,8 @@ export default {
'gpuservice.instance.event.count': '次数', 'gpuservice.instance.event.count': '次数',
'gpuservice.instance.event.lastSeen': '最近发生', 'gpuservice.instance.event.lastSeen': '最近发生',
'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。', 'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。',
'gpuservice.instance.event.tab.instance': '实例事件',
'gpuservice.instance.event.tab.volume': '存储卷事件',
'gpuservice.instance.recreate.confirm.title': '确认重新创建', 'gpuservice.instance.recreate.confirm.title': '确认重新创建',
'gpuservice.instance.recreate.confirm.content': 'gpuservice.instance.recreate.confirm.content':
'系统将先删除当前实例,然后使用当前配置重新创建。\n <span style="font-size: 13px;font-weight: 700">{name}</span>', '系统将先删除当前实例,然后使用当前配置重新创建。\n <span style="font-size: 13px;font-weight: 700">{name}</span>',
@@ -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<InstanceEvents>(
`${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( export async function queryGPUServiceInstanceLog(
params: InstanceLogQueryParams & { params: InstanceLogQueryParams & {
namespace: string; namespace: string;
@@ -1,4 +1,3 @@
import { createAxiosToken } from '@/hooks/use-chunk-request';
import { ReloadOutlined } from '@ant-design/icons'; import { ReloadOutlined } from '@ant-design/icons';
import { import {
AlertBlockInfo, AlertBlockInfo,
@@ -7,12 +6,13 @@ import {
StatusTag StatusTag
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; 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 type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { queryGPUServiceInstanceEvents } from '../apis';
import { InstanceEventItem } from '../config/types'; import { InstanceEventItem } from '../config/types';
import useQueryInstanceEvents from '../services/use-query-instance-events';
import useQueryVolumeEvents from '../services/use-query-volume-events';
type ViewEventsModalProps = { type ViewEventsModalProps = {
open: boolean; open: boolean;
@@ -31,119 +31,170 @@ const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => { const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
const intl = useIntl(); const intl = useIntl();
const { open, onCancel, name, namespace, clusterID } = props || {}; const { open, onCancel, name } = props || {};
const [events, setEvents] = useState<InstanceEventItem[]>([]); const [activeKey, setActiveKey] = useState('instance');
const [loading, setLoading] = useState(false);
const tokenRef = useRef<ReturnType<typeof createAxiosToken> | null>(null);
const fetchEvents = useCallback(async () => { const {
if (!name || !clusterID || !namespace) return; detailData: instanceEventsData,
tokenRef.current?.cancel?.(); loading: instanceLoading,
const source = createAxiosToken(); cancelRequest: cancelInstanceRequest,
tokenRef.current = source; fetchData: fetchInstanceEvents
setLoading(true); } = useQueryInstanceEvents();
try {
const res = await queryGPUServiceInstanceEvents( const {
{ name, namespace, clusterID }, detailData: volumeEventsData,
{ token: source.token } loading: volumeLoading,
); cancelRequest: cancelVolumeRequest,
setEvents(res?.items ?? []); fetchData: fetchVolumeEvents
} catch (e) { } = useQueryVolumeEvents();
// request cancellation or network failure — keep prior items
} finally { const instanceEvents = instanceEventsData?.items ?? [];
setLoading(false); const volumeEvents = volumeEventsData?.items ?? [];
}
}, [name, namespace, clusterID]); const refreshAll = () => {
if (!name) return;
fetchInstanceEvents({ name });
fetchVolumeEvents({ name });
};
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchEvents(); refreshAll();
} else { } else {
tokenRef.current?.cancel?.(); cancelInstanceRequest();
setEvents([]); cancelVolumeRequest();
} }
return () => { return () => {
tokenRef.current?.cancel?.(); cancelInstanceRequest();
cancelVolumeRequest();
}; };
}, [open, fetchEvents]); }, [open]);
const handleCancel = useCallback(() => { const handleCancel = () => {
tokenRef.current?.cancel?.(); cancelInstanceRequest();
cancelVolumeRequest();
onCancel(); onCancel();
}, [onCancel]); };
const columns: ColumnsType<InstanceEventItem> = [ const columns: ColumnsType<InstanceEventItem> = useMemo(
{ () => [
title: intl.formatMessage({ id: 'common.table.type' }), {
dataIndex: 'type', title: intl.formatMessage({ id: 'common.table.type' }),
key: 'type', dataIndex: 'type',
width: 110, key: 'type',
render: (value: string) => ( width: 110,
<StatusTag render: (value: string) => (
statusValue={{ <StatusTag
status: eventTypeStatus[value] ?? 'inactive', statusValue={{
text: value || '-' status: eventTypeStatus[value] ?? 'inactive',
}} text: value || '-'
/> }}
) />
},
{
title: intl.formatMessage({ id: 'gpuservice.instance.event.reason' }),
dataIndex: 'reason',
key: 'reason',
width: 180,
ellipsis: { showTitle: false },
render: (text: string) => (
<AutoTooltip ghost style={{ maxWidth: 180 }}>
{text || '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'gpuservice.instance.event.message' }),
dataIndex: 'message',
key: 'message',
ellipsis: { showTitle: false },
render: (text: string) => <AutoTooltip ghost>{text || '-'}</AutoTooltip>
},
{
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 (
<AutoTooltip ghost style={{ maxWidth: 200 }}>
{src}
</AutoTooltip>
);
}
},
{
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
) )
},
{
title: intl.formatMessage({ id: 'gpuservice.instance.event.reason' }),
dataIndex: 'reason',
key: 'reason',
width: 180,
ellipsis: { showTitle: false },
render: (text: string) => (
<AutoTooltip ghost style={{ maxWidth: 180 }}>
{text || '-'}
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'gpuservice.instance.event.message' }),
dataIndex: 'message',
key: 'message',
ellipsis: { showTitle: false },
render: (text: string) => <AutoTooltip ghost>{text || '-'}</AutoTooltip>
},
{
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 (
<AutoTooltip ghost style={{ maxWidth: 200 }}>
{src}
</AutoTooltip>
);
}
},
{
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
) => (
<Table
columns={columns}
className={'scroll-table'}
tableLayout={'auto'}
style={{ width: '100%', minHeight: 400 }}
dataSource={dataSource}
rowKey={(record) =>
`${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 ( return (
<ScrollerModal <ScrollerModal
title={ title={
@@ -155,8 +206,8 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
type="text" type="text"
size="small" size="small"
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={fetchEvents} onClick={refreshAll}
loading={loading} loading={isLoading}
/> />
</span> </span>
} }
@@ -188,23 +239,13 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
contentStyle={{ paddingInline: 0 }} contentStyle={{ paddingInline: 0 }}
></AlertBlockInfo> ></AlertBlockInfo>
</div> </div>
<Table <Tabs
columns={columns} size="small"
className={'scroll-table'} type="card"
tableLayout={'auto'} activeKey={activeKey}
style={{ width: '100%', minHeight: 400 }} onChange={setActiveKey}
dataSource={events || []} items={tabItems}
rowKey={(record) => />
`${record.metadata?.name}-${record.metadata.namespace}`
}
loading={{
spinning: loading,
size: 'middle'
}}
virtual
scroll={{ y: 'calc(100vh - 80px)' }}
pagination={false}
></Table>
</ScrollerModal> </ScrollerModal>
); );
}; };
@@ -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
};
}