From 87c8a3a64dd6a409bf07fae2d8a31dda3b75a568 Mon Sep 17 00:00:00 2001 From: jialin Date: Fri, 15 May 2026 16:05:14 +0800 Subject: [PATCH] feat: add instance recreate action --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/locales/en-US/common.ts | 2 + src/locales/en-US/gpuservice.ts | 11 + src/locales/ja-JP/common.ts | 2 + src/locales/ja-JP/gpuservice.ts | 11 + src/locales/ru-RU/common.ts | 2 + src/locales/ru-RU/gpuservice.ts | 12 + src/locales/tr-TR/common.ts | 2 + src/locales/tr-TR/gpuservice.ts | 13 ++ src/locales/zh-CN/common.ts | 2 + src/locales/zh-CN/gpuservice.ts | 10 + src/pages/gpu-service/instances/apis/index.ts | 37 ++- .../components/instance-type-item.tsx | 2 +- .../components/view-events-modal.tsx | 212 ++++++++++++++++++ .../instances/components/view-logs-modal.tsx | 3 + .../gpu-service/instances/config/index.ts | 12 + .../gpu-service/instances/config/types.ts | 1 + .../gpu-service/instances/forms/basic.tsx | 6 +- .../gpu-service/instances/forms/index.tsx | 29 ++- .../instances/forms/storage-volume.tsx | 30 ++- .../instances/hooks/use-instances-columns.tsx | 24 +- .../instances/hooks/use-view-events.ts | 51 +++++ .../instances/hooks/use-view-logs.ts | 9 +- src/pages/gpu-service/instances/index.tsx | 66 +++++- .../services/use-query-instance-log.ts | 4 +- src/pages/gpu-service/storage/config/index.ts | 12 +- src/pages/gpu-service/storage/config/types.ts | 1 + src/pages/gpu-service/storage/forms/basic.tsx | 7 +- src/pages/gpu-service/storage/forms/index.tsx | 2 +- .../storage/hooks/use-storage-columns.tsx | 5 +- src/pages/gpu-service/storage/index.tsx | 14 +- .../gpu-service/templates/forms/basic.tsx | 27 ++- src/pages/gpu-service/templates/forms/env.tsx | 12 +- .../gpu-service/templates/forms/ports.tsx | 73 ++++-- src/pages/gpu-service/templates/index.tsx | 4 +- 36 files changed, 616 insertions(+), 106 deletions(-) create mode 100644 src/pages/gpu-service/instances/components/view-events-modal.tsx create mode 100644 src/pages/gpu-service/instances/hooks/use-view-events.ts diff --git a/package.json b/package.json index 59d25683..20828665 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@ant-design/pro-components": "3.1.0-0", "@antv/g6": "^5.0.51", "@braintree/sanitize-url": "^7.1.1", - "@gpustack/core-ui": "^1.0.11", + "@gpustack/core-ui": "^1.0.12", "@huggingface/gguf": "^0.1.7", "@huggingface/hub": "^0.15.1", "@huggingface/tasks": "^0.11.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 146a934a..c8f7930f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^7.1.1 version: 7.1.2 '@gpustack/core-ui': - specifier: ^1.0.11 - version: 1.0.11(czdvzceysqw7iv6pct2ucnb23e) + specifier: ^1.0.12 + version: 1.0.12(czdvzceysqw7iv6pct2ucnb23e) '@huggingface/gguf': specifier: ^0.1.7 version: 0.1.18 @@ -1484,8 +1484,8 @@ packages: resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz} deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package - '@gpustack/core-ui@1.0.11': - resolution: {integrity: sha512-1NpR0oT8e93BAMEtzAR9XZkOGercohG3P/QwIiEclHDaILbReUeGWoT1t1sP5yazu4v7uXRucV4rhPApLQ5gBA==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.11.tgz} + '@gpustack/core-ui@1.0.12': + resolution: {integrity: sha512-2nwQ7i5UXO0Slm2mrZxxx4C8+932xBw2Q9laqZ+OLZscBq7070YEty383MGeujTnIKwrijBcP7L3boCPr8cyWQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.12.tgz} peerDependencies: '@ant-design/icons': '>=6.0.0' '@ant-design/pro-components': 3.1.0-0 @@ -10790,7 +10790,7 @@ snapshots: '@formatjs/intl-utils@2.3.0': {} - '@gpustack/core-ui@1.0.11(czdvzceysqw7iv6pct2ucnb23e)': + '@gpustack/core-ui@1.0.12(czdvzceysqw7iv6pct2ucnb23e)': dependencies: '@ant-design/icons': 6.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@ant-design/pro-components': 3.1.0-0(antd@6.3.7(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 12424437..a0e6b03e 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -52,6 +52,8 @@ export default { 'common.button.authorize': 'Role Authorization', 'common.button.confirm': 'Confirm', 'common.button.viewlog': 'View Logs', + 'common.button.viewevent': 'View Events', + 'common.button.recreate': 'Recreate', 'common.table.operation': 'Operations', 'common.table.createTime': 'Created', 'common.table.updateTime': 'Updated', diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index ec166546..9f657000 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -25,6 +25,7 @@ export default { 'gpuservice.template.ports.name': 'Name', 'gpuservice.template.ports.name.max': 'Port name cannot exceed 16 characters.', + 'gpuservice.template.ports.name.duplicate': 'Port names must be unique.', 'gpuservice.template.env': 'Environment Variables', 'gpuservice.template.env.add': 'Add Environment Variable', 'gpuservice.template.env.invalid': @@ -66,6 +67,16 @@ export default { 'gpuservice.instance.template.mount': 'Mount', 'gpuservice.instance.connect': 'Connect', 'gpuservice.instance.connect.copySshCommand': 'Copy SSH Command', + 'gpuservice.instance.event.reason': 'Reason', + 'gpuservice.instance.event.message': 'Message', + 'gpuservice.instance.event.source': 'Source', + 'gpuservice.instance.event.count': 'Count', + 'gpuservice.instance.event.lastSeen': 'Last Seen', + 'gpuservice.instance.event.recentHourTip': + 'Only events from the last hour are shown', + '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}', 'gpuservice.storage': 'Storage', 'gpuservice.storage.add': 'Add Storage', 'gpuservice.storage.edit': 'Edit Storage', diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index b62562cd..c9c98ea0 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -52,6 +52,8 @@ export default { 'common.button.authorize': 'ロール認可', 'common.button.confirm': '確認', 'common.button.viewlog': 'ログを表示', + 'common.button.viewevent': 'イベントを表示', + 'common.button.recreate': '再作成', 'common.table.operation': '操作', 'common.table.createTime': '作成日時', 'common.table.updateTime': '更新日時', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index 645372b9..c189dd55 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -24,6 +24,7 @@ export default { 'gpuservice.template.ports.name': '名前', 'gpuservice.template.ports.name.max': 'ポート名は 16 文字以内で入力してください。', + 'gpuservice.template.ports.name.duplicate': 'ポート名は重複できません。', 'gpuservice.template.env': '環境変数', 'gpuservice.template.env.add': '環境変数を追加', 'gpuservice.template.env.invalid': '環境変数を完成させてください。', @@ -64,6 +65,16 @@ export default { 'gpuservice.instance.template.mount': 'マウント', 'gpuservice.instance.connect': '接続', 'gpuservice.instance.connect.copySshCommand': 'SSH コマンドをコピー', + 'gpuservice.instance.event.reason': '理由', + 'gpuservice.instance.event.message': 'メッセージ', + 'gpuservice.instance.event.source': 'ソース', + 'gpuservice.instance.event.count': '回数', + 'gpuservice.instance.event.lastSeen': '最終発生', + 'gpuservice.instance.event.recentHourTip': + '直近 1 時間のイベントのみ表示されます', + 'gpuservice.instance.recreate.confirm.title': '再作成を確認しますか', + 'gpuservice.instance.recreate.confirm.content': + '現在のインスタンスを削除した後、現在の構成で再作成します。\n {name}', 'gpuservice.storage': 'ストレージ', 'gpuservice.storage.add': 'ストレージを追加', 'gpuservice.storage.edit': 'ストレージを編集', diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index 94d6889d..f0220935 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -52,6 +52,8 @@ export default { 'common.button.authorize': 'Настройка прав', 'common.button.confirm': 'Подтвердить', 'common.button.viewlog': 'Просмотр логов', + 'common.button.viewevent': 'Просмотр событий', + 'common.button.recreate': 'Пересоздать', 'common.table.operation': 'Действия', 'common.table.createTime': 'Создано', 'common.table.updateTime': 'Обновлено', diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index a5efd9c1..8d6e820f 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -25,6 +25,8 @@ export default { 'gpuservice.template.ports.name': 'Имя', 'gpuservice.template.ports.name.max': 'Имя порта не должно превышать 16 символов.', + 'gpuservice.template.ports.name.duplicate': + 'Имена портов должны быть уникальными.', 'gpuservice.template.env': 'Переменные окружения', 'gpuservice.template.env.add': 'Добавить переменную окружения', 'gpuservice.template.env.invalid': @@ -66,6 +68,16 @@ export default { 'gpuservice.instance.template.mount': 'Монтирование', 'gpuservice.instance.connect': 'Подключение', 'gpuservice.instance.connect.copySshCommand': 'Скопировать команду SSH', + 'gpuservice.instance.event.reason': 'Причина', + 'gpuservice.instance.event.message': 'Сообщение', + 'gpuservice.instance.event.source': 'Источник', + 'gpuservice.instance.event.count': 'Кол-во', + 'gpuservice.instance.event.lastSeen': 'Последнее событие', + 'gpuservice.instance.event.recentHourTip': + 'Отображаются только события за последний час', + 'gpuservice.instance.recreate.confirm.title': 'Подтвердить пересоздание', + 'gpuservice.instance.recreate.confirm.content': + 'Текущий экземпляр будет сначала удалён, а затем пересоздан с текущей конфигурацией.\n {name}', 'gpuservice.storage': 'Хранилище', 'gpuservice.storage.add': 'Добавить хранилище', 'gpuservice.storage.edit': 'Редактировать хранилище', diff --git a/src/locales/tr-TR/common.ts b/src/locales/tr-TR/common.ts index 2d198dd8..5ecea64d 100644 --- a/src/locales/tr-TR/common.ts +++ b/src/locales/tr-TR/common.ts @@ -52,6 +52,8 @@ export default { 'common.button.authorize': 'Rol Yetkilendirme', 'common.button.confirm': 'Onayla', 'common.button.viewlog': 'Günlükleri Görüntüle', + 'common.button.viewevent': 'Olayları Görüntüle', + 'common.button.recreate': 'Yeniden Oluştur', 'common.table.operation': 'İşlemler', 'common.table.createTime': 'Oluşturulma', 'common.table.updateTime': 'Güncellenme', diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index 7c05331a..72b96300 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -25,6 +25,8 @@ export default { 'gpuservice.template.ports.name': 'Ad', 'gpuservice.template.ports.name.max': 'Bağlantı noktası adı 16 karakterden uzun olamaz.', + 'gpuservice.template.ports.name.duplicate': + 'Bağlantı noktası adları benzersiz olmalıdır.', 'gpuservice.template.env': 'Ortam Değişkenleri', 'gpuservice.template.env.add': 'Ortam Değişkeni Ekle', 'gpuservice.template.env.invalid': 'Ortam değişkenlerini eksiksiz doldurun.', @@ -65,6 +67,17 @@ export default { 'gpuservice.instance.template.mount': 'Bağlama', 'gpuservice.instance.connect': 'Bağlan', 'gpuservice.instance.connect.copySshCommand': 'SSH Komutunu Kopyala', + 'gpuservice.instance.event.reason': 'Neden', + 'gpuservice.instance.event.message': 'Mesaj', + 'gpuservice.instance.event.source': 'Kaynak', + 'gpuservice.instance.event.count': 'Sayı', + 'gpuservice.instance.event.lastSeen': 'Son Görülen', + 'gpuservice.instance.event.recentHourTip': + 'Yalnızca son bir saatteki olaylar gösterilir', + 'gpuservice.instance.recreate.confirm.title': + 'Yeniden oluşturma onaylansın mı', + 'gpuservice.instance.recreate.confirm.content': + 'Mevcut örnek önce silinecek, ardından mevcut yapılandırmayla yeniden oluşturulacaktır.\n {name}', 'gpuservice.storage': 'Depolama', 'gpuservice.storage.add': 'Depolama Ekle', 'gpuservice.storage.edit': 'Depolamayı Düzenle', diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index ffe4dc80..e5330fcf 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -50,6 +50,8 @@ export default { 'common.button.authorize': '角色授权', 'common.button.confirm': '确定', 'common.button.viewlog': '查看日志', + 'common.button.viewevent': '查看事件', + 'common.button.recreate': '重新创建', 'common.table.operation': '操作', 'common.table.createTime': '创建时间', 'common.table.updateTime': '更新时间', diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index 7cb77080..eed4bc9e 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -22,6 +22,7 @@ export default { 'gpuservice.template.ports.invalid': '请填写完整的端口配置', 'gpuservice.template.ports.name': '名称', 'gpuservice.template.ports.name.max': '端口名称不能超过 16 个字符', + 'gpuservice.template.ports.name.duplicate': '端口名称不能重复', 'gpuservice.template.env': '环境变量', 'gpuservice.template.env.add': '添加环境变量', 'gpuservice.template.env.invalid': '请填写完整的环境变量', @@ -61,6 +62,15 @@ export default { 'gpuservice.instance.template.mount': '挂载', 'gpuservice.instance.connect': '连接', 'gpuservice.instance.connect.copySshCommand': '复制 SSH 命令', + 'gpuservice.instance.event.reason': '原因', + 'gpuservice.instance.event.message': '消息', + 'gpuservice.instance.event.source': '来源', + 'gpuservice.instance.event.count': '次数', + 'gpuservice.instance.event.lastSeen': '最近发生', + 'gpuservice.instance.event.recentHourTip': '仅显示最近一小时的事件。', + 'gpuservice.instance.recreate.confirm.title': '确认重新创建', + 'gpuservice.instance.recreate.confirm.content': + '系统将先删除当前实例,然后使用当前配置重新创建。\n {name}', 'gpuservice.storage': '存储', 'gpuservice.storage.add': '添加存储', 'gpuservice.storage.edit': '编辑存储', diff --git a/src/pages/gpu-service/instances/apis/index.ts b/src/pages/gpu-service/instances/apis/index.ts index cfaed4b1..fe86a270 100644 --- a/src/pages/gpu-service/instances/apis/index.ts +++ b/src/pages/gpu-service/instances/apis/index.ts @@ -17,8 +17,27 @@ export const GPU_SERVICE_INSTANCES_API = (params: { export const GPU_SERVICE_INSTANCES_TYPE_API = (params: { clusterID?: number; -}) => - `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`; +}) => { + return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`; +}; + +export const GPU_SERVICE_INSTANCES_LOG_API = (params: { + namespace: string; + name: string; + clusterID?: number; +}) => { + return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/log`; +}; + +export const GPU_SERVICE_INSTANCES_EVENTS_API = (params: { + namespace: string; + name: string; + clusterID?: number; +}) => { + return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/events`; +}; + +// =========== Instances =========== export async function queryGPUServiceInstances( params: Global.K8sSearchParams & { @@ -147,10 +166,11 @@ export async function queryGPUServiceInstanceEvents( return; } return request( - `${GPU_SERVICE_INSTANCES_API({ + `${GPU_SERVICE_INSTANCES_EVENTS_API({ namespace: params.namespace, - clusterID: params.clusterID - })}/${params.name}/events`, + clusterID: params.clusterID, + name: params.name + })}`, { method: 'GET', params: omitPathParams(_.omit(params, ['name'])), @@ -171,10 +191,11 @@ export async function queryGPUServiceInstanceLog( return; } return request( - `${GPU_SERVICE_INSTANCES_API({ + `${GPU_SERVICE_INSTANCES_LOG_API({ namespace: params.namespace, - clusterID: params.clusterID - })}/${params.name}/log`, + clusterID: params.clusterID, + name: params.name + })}`, { method: 'GET', params: omitPathParams(_.omit(params, ['name'])), diff --git a/src/pages/gpu-service/instances/components/instance-type-item.tsx b/src/pages/gpu-service/instances/components/instance-type-item.tsx index 3a3e8133..886cbc0e 100644 --- a/src/pages/gpu-service/instances/components/instance-type-item.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-item.tsx @@ -135,7 +135,7 @@ const InstanceTypeItem: React.FC = ({ <> - + {intl.formatMessage({ diff --git a/src/pages/gpu-service/instances/components/view-events-modal.tsx b/src/pages/gpu-service/instances/components/view-events-modal.tsx new file mode 100644 index 00000000..6d9ed3b8 --- /dev/null +++ b/src/pages/gpu-service/instances/components/view-events-modal.tsx @@ -0,0 +1,212 @@ +import { createAxiosToken } from '@/hooks/use-chunk-request'; +import { ReloadOutlined } from '@ant-design/icons'; +import { + AlertBlockInfo, + AutoTooltip, + ScrollerModal, + StatusTag +} from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Button, Table } 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 { InstanceEventItem } from '../config/types'; + +type ViewEventsModalProps = { + open: boolean; + name: string; + namespace: string; + clusterID?: number; + onCancel: () => void; +}; + +const formatRelative = (text?: string) => (text ? dayjs(text).fromNow() : '-'); + +const eventTypeStatus: Record = { + Normal: 'success', + Warning: 'warning' +}; + +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 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]); + + useEffect(() => { + if (open) { + fetchEvents(); + } else { + tokenRef.current?.cancel?.(); + setEvents([]); + } + return () => { + tokenRef.current?.cancel?.(); + }; + }, [open, fetchEvents]); + + const handleCancel = useCallback(() => { + tokenRef.current?.cancel?.(); + 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 + ) + } + ]; + + return ( + + + {intl.formatMessage({ id: 'common.button.viewevent' })} + + + {action === PageAction.CREATE && ( + + )} {storageMode === StorageModeValueMap.Existing && ( @@ -121,6 +131,7 @@ const StorageVolume = () => { ]} >