Files
gpustack-ui/src/pages/gpu-service/instances/components/view-events-modal.tsx
T
2026-05-26 19:16:50 +08:00

276 lines
7.0 KiB
TypeScript

import { ReloadOutlined } from '@ant-design/icons';
import {
AlertBlockInfo,
AutoTooltip,
ScrollerModal,
StatusTag
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Table, Tabs, TabsProps } from 'antd';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
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;
name: string;
namespace: string;
volumeName?: string;
clusterID?: number;
hasPersistentVolume?: boolean;
onCancel: () => void;
};
const formatRelative = (text?: string) => (text ? dayjs(text).fromNow() : '-');
const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
Normal: 'success',
Warning: 'warning'
};
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
const intl = useIntl();
const {
open,
onCancel,
name,
namespace,
clusterID,
volumeName,
hasPersistentVolume
} = props || {};
const [activeKey, setActiveKey] = useState('instance');
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 || !namespace || !clusterID) return;
fetchInstanceEvents({ name, namespace, clusterID });
if (hasPersistentVolume) {
fetchVolumeEvents({ name: volumeName || '', namespace, clusterID });
}
};
useEffect(() => {
if (open) {
refreshAll();
} else {
cancelInstanceRequest();
cancelVolumeRequest();
}
return () => {
cancelInstanceRequest();
cancelVolumeRequest();
};
}, [open]);
const handleCancel = () => {
cancelInstanceRequest();
cancelVolumeRequest();
onCancel();
};
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)
},
...(hasPersistentVolume
? [
{
key: 'volume',
label: intl.formatMessage({
id: 'gpuservice.instance.event.tab.volume'
}),
children: renderTable(volumeEvents, volumeLoading)
}
]
: [])
];
const isLoading = instanceLoading || (hasPersistentVolume && volumeLoading);
return (
<ScrollerModal
title={
<span
className="flex flex-center"
style={{ gap: 8, paddingInline: 24 }}
>
<span style={{ fontWeight: 'var(--font-weight-bold)' }}>
{intl.formatMessage({ id: 'common.button.viewevent' })}
</span>
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={refreshAll}
loading={isLoading}
/>
</span>
}
zIndex={3000}
open={open}
centered={true}
onCancel={handleCancel}
destroyOnHidden={true}
closeIcon={true}
mask={{
closable: false
}}
keyboard={true}
styles={{
wrapper: {
borderRadius: 0
},
container: {
paddingInline: 0
}
}}
width="1000px"
maxContentHeight="100vh"
footer={null}
>
<div style={{ marginBlock: '8px 16px' }}>
<AlertBlockInfo
message={intl.formatMessage({
id: 'gpuservice.instance.event.recentHourTip'
})}
type="warning"
contentStyle={{ paddingInline: 0 }}
></AlertBlockInfo>
</div>
<Tabs
size="small"
type="card"
activeKey={activeKey}
onChange={setActiveKey}
items={tabItems}
/>
</ScrollerModal>
);
};
export default ViewEventsModal;