feat: instance pv events
This commit is contained in:
@@ -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 <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
|
||||
@@ -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 <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
|
||||
@@ -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 <span style="font-size: 13px;font-weight: 700">{name}</span>',
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 <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(
|
||||
params: InstanceLogQueryParams & {
|
||||
namespace: string;
|
||||
|
||||
@@ -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<string, 'success' | 'warning' | 'error'> = {
|
||||
|
||||
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { open, onCancel, name, namespace, clusterID } = props || {};
|
||||
const [events, setEvents] = useState<InstanceEventItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const tokenRef = useRef<ReturnType<typeof createAxiosToken> | 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<InstanceEventItem> = [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.type' }),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 110,
|
||||
render: (value: string) => (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
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
|
||||
const columns: ColumnsType<InstanceEventItem> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.type' }),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 110,
|
||||
render: (value: string) => (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
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
|
||||
)
|
||||
}
|
||||
],
|
||||
[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 (
|
||||
<ScrollerModal
|
||||
title={
|
||||
@@ -155,8 +206,8 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchEvents}
|
||||
loading={loading}
|
||||
onClick={refreshAll}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
@@ -188,23 +239,13 @@ const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
contentStyle={{ paddingInline: 0 }}
|
||||
></AlertBlockInfo>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
className={'scroll-table'}
|
||||
tableLayout={'auto'}
|
||||
style={{ width: '100%', minHeight: 400 }}
|
||||
dataSource={events || []}
|
||||
rowKey={(record) =>
|
||||
`${record.metadata?.name}-${record.metadata.namespace}`
|
||||
}
|
||||
loading={{
|
||||
spinning: loading,
|
||||
size: 'middle'
|
||||
}}
|
||||
virtual
|
||||
scroll={{ y: 'calc(100vh - 80px)' }}
|
||||
pagination={false}
|
||||
></Table>
|
||||
<Tabs
|
||||
size="small"
|
||||
type="card"
|
||||
activeKey={activeKey}
|
||||
onChange={setActiveKey}
|
||||
items={tabItems}
|
||||
/>
|
||||
</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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user