From fc900c8ce621cf36c756a1b74e70907bbd03bfca Mon Sep 17 00:00:00 2001 From: jialin Date: Sat, 9 May 2026 16:22:38 +0800 Subject: [PATCH] fix: ssh key, template test --- src/layouts/index.tsx | 2 +- src/locales/en-US/gpuservice.ts | 35 +++ src/locales/en-US/no-result.ts | 7 +- src/locales/ja-JP/gpuservice.ts | 33 +++ src/locales/ja-JP/no-result.ts | 7 +- src/locales/ru-RU/gpuservice.ts | 35 +++ src/locales/ru-RU/no-result.ts | 7 +- src/locales/tr-TR/gpuservice.ts | 34 +++ src/locales/tr-TR/no-result.ts | 6 +- src/locales/zh-CN/gpuservice.ts | 33 +++ src/locales/zh-CN/no-result.ts | 5 +- .../_components/form-overlay-view.module.less | 22 ++ src/pages/_components/form-overlay-view.tsx | 108 ++++++---- src/pages/backends/styles/backend-list.less | 1 + .../instances/components/add-modal.tsx | 44 +++- .../gpu-service/instances/config/mock-data.ts | 95 -------- .../gpu-service/instances/config/types.ts | 1 + .../gpu-service/instances/forms/index.tsx | 94 +++----- .../instances/forms/instance-type-overlay.tsx | 94 -------- .../instances/forms/instance-type.tsx | 33 +-- .../instances/forms/storage-overlay.tsx | 74 +++++++ .../instances/forms/storage-volume.tsx | 66 ++++-- .../instances/forms/template-overlay.tsx | 94 -------- .../instances/forms/template-selector.tsx | 11 +- .../gpu-service/instances/forms/template.tsx | 203 ------------------ .../gpu-service/public-keys/apis/index.ts | 56 ++--- src/pages/gpu-service/public-keys/index.tsx | 76 +++++-- .../public-keys/services/use-create-sshkey.ts | 25 +-- .../public-keys/services/use-get-sshkey.ts | 22 +- .../public-keys/services/use-update-sshkey.ts | 27 +-- src/pages/gpu-service/public-keys/types.ts | 8 +- .../gpu-service/storage/config/mock-data.ts | 65 ------ .../storage/services/use-query-storage.ts | 31 +++ src/pages/gpu-service/templates/apis/index.ts | 2 +- .../templates/components/template-card.tsx | 117 ++++++---- .../gpu-service/templates/config/index.tsx | 35 ++- .../gpu-service/templates/config/mock-data.ts | 106 --------- .../gpu-service/templates/config/types.ts | 31 +-- .../gpu-service/templates/forms/basic.tsx | 169 +++++++++++---- src/pages/gpu-service/templates/forms/env.tsx | 29 ++- .../gpu-service/templates/forms/index.tsx | 48 +++-- .../gpu-service/templates/forms/ports.tsx | 28 ++- src/pages/gpu-service/templates/index.tsx | 69 ++++-- .../templates/services/use-query-templates.ts | 27 +++ src/pages/resources/config/gpu-driver.ts | 2 +- 45 files changed, 1027 insertions(+), 1090 deletions(-) create mode 100644 src/locales/en-US/gpuservice.ts create mode 100644 src/locales/ja-JP/gpuservice.ts create mode 100644 src/locales/ru-RU/gpuservice.ts create mode 100644 src/locales/tr-TR/gpuservice.ts create mode 100644 src/locales/zh-CN/gpuservice.ts delete mode 100644 src/pages/gpu-service/instances/config/mock-data.ts delete mode 100644 src/pages/gpu-service/instances/forms/instance-type-overlay.tsx create mode 100644 src/pages/gpu-service/instances/forms/storage-overlay.tsx delete mode 100644 src/pages/gpu-service/instances/forms/template-overlay.tsx delete mode 100644 src/pages/gpu-service/instances/forms/template.tsx delete mode 100644 src/pages/gpu-service/storage/config/mock-data.ts create mode 100644 src/pages/gpu-service/storage/services/use-query-storage.ts delete mode 100644 src/pages/gpu-service/templates/config/mock-data.ts create mode 100644 src/pages/gpu-service/templates/services/use-query-templates.ts diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 1c9b9ccf..84e892ff 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -356,7 +356,7 @@ export default (props: any) => { config={{ apiBaseUrl: GPUSTACK_API_BASE_URL, theme: userSettings.theme, - iconUrl: '//at.alicdn.com/t/c/font_4613488_tzcsatubq4f.js', + iconUrl: '//at.alicdn.com/t/c/font_4613488_nujs54seb6.js', isDarkTheme: userSettings.isDarkTheme, defaultColorPrimary: COLOR_PRIMARY }} diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts new file mode 100644 index 00000000..9d71709f --- /dev/null +++ b/src/locales/en-US/gpuservice.ts @@ -0,0 +1,35 @@ +export default { + 'gpuservice.template': 'GPU Instance Template', + 'gpuservice.template.add': 'Add Instance Template', + 'gpuservice.template.edit': 'Edit Instance Template', + 'gpuservice.template.filter.name': 'Filter by name', + 'gpuservice.template.filter.vendor': 'Filter by vendor', + 'gpuservice.template.image': 'Image', + 'gpuservice.template.imagePullPolicy': 'Image Pull Policy', + 'gpuservice.template.imagePullPolicy.always': 'Always', + 'gpuservice.template.imagePullPolicy.ifNotPresent': 'If Not Present', + 'gpuservice.template.imagePullPolicy.never': 'Never', + 'gpuservice.template.command': 'Container Startup Command', + 'gpuservice.template.command.placeholder': + 'One argument per line, e.g.:\n/bin/bash\n-c\nyour command', + 'gpuservice.template.mountPath': 'Mount Path', + 'gpuservice.template.containerDisk': 'Container Disk (Gi)', + 'gpuservice.template.memory': 'Memory (Gi)', + 'gpuservice.template.ports': 'Ports', + 'gpuservice.template.ports.add': 'Add Port', + 'gpuservice.template.ports.invalid': + 'Please complete the port configuration.', + 'gpuservice.template.env': 'Environment Variables', + 'gpuservice.template.env.add': 'Add Environment Variable', + 'gpuservice.template.env.invalid': + 'Please complete the environment variables.', + 'gpuservice.template.env.name': 'Name', + 'gpuservice.template.env.value': 'Value', + 'gpuservice.template.card.image': 'Image', + 'gpuservice.template.card.mount': 'Mount', + 'gpuservice.template.card.resources': 'Resources', + 'gpuservice.template.card.ports': 'Ports', + 'gpuservice.publicKey.label': 'SSH Public Key', + 'gpuservice.publicKey.placeholder': + 'Begin with ssh-rsa or ssh-ed25519. One Public Key per line.\n\nView Public Key:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub' +}; diff --git a/src/locales/en-US/no-result.ts b/src/locales/en-US/no-result.ts index 87ae927d..ee2d155f 100644 --- a/src/locales/en-US/no-result.ts +++ b/src/locales/en-US/no-result.ts @@ -52,5 +52,10 @@ export default { 'noresult.instances.title': 'No instances', 'noresult.instances.subTitle': 'There are no running model instances. Deploy a model to create one automatically.', - 'noresult.instances.nofound': 'No matching instances found.' + 'noresult.instances.nofound': 'No matching instances found.', + 'noresult.gpuservice.template.title': 'No Instance Templates', + 'noresult.gpuservice.template.subTitle': + 'Once you create an instance template, it will appear here.', + 'noresult.gpuservice.template.nofound': + 'No matching instance templates found.' }; diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts new file mode 100644 index 00000000..8a9578da --- /dev/null +++ b/src/locales/ja-JP/gpuservice.ts @@ -0,0 +1,33 @@ +export default { + 'gpuservice.template': 'GPU インスタンステンプレート', + 'gpuservice.template.add': 'インスタンステンプレートを追加', + 'gpuservice.template.edit': 'インスタンステンプレートを編集', + 'gpuservice.template.filter.name': '名前でフィルター', + 'gpuservice.template.filter.vendor': 'ベンダーでフィルター', + 'gpuservice.template.image': 'コンテナイメージ', + 'gpuservice.template.imagePullPolicy': 'イメージ取得ポリシー', + 'gpuservice.template.imagePullPolicy.always': '常に取得', + 'gpuservice.template.imagePullPolicy.ifNotPresent': '存在しない場合に取得', + 'gpuservice.template.imagePullPolicy.never': '取得しない', + 'gpuservice.template.command': 'コンテナ起動コマンド', + 'gpuservice.template.command.placeholder': + '1行に1つの引数、例:\n/bin/bash\n-c\nyour command', + 'gpuservice.template.mountPath': 'マウントパス', + 'gpuservice.template.containerDisk': 'コンテナディスク (Gi)', + 'gpuservice.template.memory': 'メモリ (Gi)', + 'gpuservice.template.ports': 'ポート', + 'gpuservice.template.ports.add': 'ポートを追加', + 'gpuservice.template.ports.invalid': 'ポート設定を完成させてください。', + 'gpuservice.template.env': '環境変数', + 'gpuservice.template.env.add': '環境変数を追加', + 'gpuservice.template.env.invalid': '環境変数を完成させてください。', + 'gpuservice.template.env.name': '名前', + 'gpuservice.template.env.value': '値', + 'gpuservice.template.card.image': 'イメージ', + 'gpuservice.template.card.mount': 'マウント', + 'gpuservice.template.card.resources': 'リソース', + 'gpuservice.template.card.ports': 'ポート', + 'gpuservice.publicKey.label': 'SSH 公開鍵', + 'gpuservice.publicKey.placeholder': + 'ssh-rsa または ssh-ed25519 で始まり、各公開鍵は1行ずつ記述します\n\n公開鍵を確認:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub' +}; diff --git a/src/locales/ja-JP/no-result.ts b/src/locales/ja-JP/no-result.ts index 87ae927d..a7b1bf32 100644 --- a/src/locales/ja-JP/no-result.ts +++ b/src/locales/ja-JP/no-result.ts @@ -52,5 +52,10 @@ export default { 'noresult.instances.title': 'No instances', 'noresult.instances.subTitle': 'There are no running model instances. Deploy a model to create one automatically.', - 'noresult.instances.nofound': 'No matching instances found.' + 'noresult.instances.nofound': 'No matching instances found.', + 'noresult.gpuservice.template.title': 'インスタンステンプレートなし', + 'noresult.gpuservice.template.subTitle': + 'インスタンステンプレートを作成すると、ここに表示されます。', + 'noresult.gpuservice.template.nofound': + '一致するインスタンステンプレートが見つかりません。' }; diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts new file mode 100644 index 00000000..22c03ba9 --- /dev/null +++ b/src/locales/ru-RU/gpuservice.ts @@ -0,0 +1,35 @@ +export default { + 'gpuservice.template': 'Шаблон экземпляра GPU', + 'gpuservice.template.add': 'Добавить шаблон экземпляра', + 'gpuservice.template.edit': 'Редактировать шаблон экземпляра', + 'gpuservice.template.filter.name': 'Фильтр по имени', + 'gpuservice.template.filter.vendor': 'Фильтр по производителю', + 'gpuservice.template.image': 'Образ', + 'gpuservice.template.imagePullPolicy': 'Политика загрузки образа', + 'gpuservice.template.imagePullPolicy.always': 'Всегда загружать', + 'gpuservice.template.imagePullPolicy.ifNotPresent': + 'Загружать при отсутствии', + 'gpuservice.template.imagePullPolicy.never': 'Никогда не загружать', + 'gpuservice.template.command': 'Команда запуска контейнера', + 'gpuservice.template.command.placeholder': + 'Один аргумент на строку, например:\n/bin/bash\n-c\nyour command', + 'gpuservice.template.mountPath': 'Путь монтирования', + 'gpuservice.template.containerDisk': 'Диск контейнера (Gi)', + 'gpuservice.template.memory': 'Память (Gi)', + 'gpuservice.template.ports': 'Порты', + 'gpuservice.template.ports.add': 'Добавить порт', + 'gpuservice.template.ports.invalid': 'Заполните настройки портов полностью.', + 'gpuservice.template.env': 'Переменные окружения', + 'gpuservice.template.env.add': 'Добавить переменную окружения', + 'gpuservice.template.env.invalid': + 'Заполните переменные окружения полностью.', + 'gpuservice.template.env.name': 'Имя', + 'gpuservice.template.env.value': 'Значение', + 'gpuservice.template.card.image': 'Образ', + 'gpuservice.template.card.mount': 'Монтирование', + 'gpuservice.template.card.resources': 'Ресурсы', + 'gpuservice.template.card.ports': 'Порты', + 'gpuservice.publicKey.label': 'Открытый ключ SSH', + 'gpuservice.publicKey.placeholder': + 'Начинается с ssh-rsa или ssh-ed25519, по одному открытому ключу на строку\n\nПросмотр открытого ключа:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub' +}; diff --git a/src/locales/ru-RU/no-result.ts b/src/locales/ru-RU/no-result.ts index 5f4e43d4..7b1a6b74 100644 --- a/src/locales/ru-RU/no-result.ts +++ b/src/locales/ru-RU/no-result.ts @@ -53,7 +53,12 @@ export default { 'noresult.instances.title': 'No instances', 'noresult.instances.subTitle': 'There are no running model instances. Deploy a model to create one automatically.', - 'noresult.instances.nofound': 'No matching instances found.' + 'noresult.instances.nofound': 'No matching instances found.', + 'noresult.gpuservice.template.title': 'Нет шаблонов экземпляров', + 'noresult.gpuservice.template.subTitle': + 'После создания шаблона экземпляра он появится здесь.', + 'noresult.gpuservice.template.nofound': + 'Подходящие шаблоны экземпляров не найдены.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts new file mode 100644 index 00000000..43000327 --- /dev/null +++ b/src/locales/tr-TR/gpuservice.ts @@ -0,0 +1,34 @@ +export default { + 'gpuservice.template': 'GPU Örnek Şablonu', + 'gpuservice.template.add': 'Örnek Şablonu Ekle', + 'gpuservice.template.edit': 'Örnek Şablonunu Düzenle', + 'gpuservice.template.filter.name': 'Ada göre filtrele', + 'gpuservice.template.filter.vendor': 'Tedarikçiye göre filtrele', + 'gpuservice.template.image': 'İmaj', + 'gpuservice.template.imagePullPolicy': 'İmaj Çekme Politikası', + 'gpuservice.template.imagePullPolicy.always': 'Her zaman çek', + 'gpuservice.template.imagePullPolicy.ifNotPresent': 'Yoksa çek', + 'gpuservice.template.imagePullPolicy.never': 'Asla çekme', + 'gpuservice.template.command': 'Konteyner Başlatma Komutu', + 'gpuservice.template.command.placeholder': + 'Her satırda bir argüman, örn.:\n/bin/bash\n-c\nyour command', + 'gpuservice.template.mountPath': 'Bağlama Yolu', + 'gpuservice.template.containerDisk': 'Konteyner Diski (Gi)', + 'gpuservice.template.memory': 'Bellek (Gi)', + 'gpuservice.template.ports': 'Bağlantı Noktaları', + 'gpuservice.template.ports.add': 'Bağlantı Noktası Ekle', + 'gpuservice.template.ports.invalid': + 'Bağlantı noktası yapılandırmasını eksiksiz doldurun.', + '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.', + 'gpuservice.template.env.name': 'Ad', + 'gpuservice.template.env.value': 'Değer', + 'gpuservice.template.card.image': 'İmaj', + 'gpuservice.template.card.mount': 'Bağlama', + 'gpuservice.template.card.resources': 'Kaynaklar', + 'gpuservice.template.card.ports': 'Bağlantı Noktaları', + 'gpuservice.publicKey.label': 'SSH Açık Anahtarı', + 'gpuservice.publicKey.placeholder': + 'ssh-rsa veya ssh-ed25519 ile başlar, her açık anahtar ayrı bir satırda\n\nAçık anahtarı görüntüle:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub' +}; diff --git a/src/locales/tr-TR/no-result.ts b/src/locales/tr-TR/no-result.ts index 160e75e6..317cf6ca 100644 --- a/src/locales/tr-TR/no-result.ts +++ b/src/locales/tr-TR/no-result.ts @@ -54,7 +54,11 @@ export default { 'noresult.instances.title': 'No instances', 'noresult.instances.subTitle': 'There are no running model instances. Deploy a model to create one automatically.', - 'noresult.instances.nofound': 'No matching instances found.' + 'noresult.instances.nofound': 'No matching instances found.', + 'noresult.gpuservice.template.title': 'Örnek Şablonu Yok', + 'noresult.gpuservice.template.subTitle': + 'Bir örnek şablonu oluşturduğunuzda burada görünecek.', + 'noresult.gpuservice.template.nofound': 'Eşleşen örnek şablonu bulunamadı.' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts new file mode 100644 index 00000000..be2d7c9c --- /dev/null +++ b/src/locales/zh-CN/gpuservice.ts @@ -0,0 +1,33 @@ +export default { + 'gpuservice.template': 'GPU 实例模板', + 'gpuservice.template.add': '添加实例模板', + 'gpuservice.template.edit': '编辑实例模板', + 'gpuservice.template.filter.name': '按名称过滤', + 'gpuservice.template.filter.vendor': '按厂商过滤', + 'gpuservice.template.image': '镜像', + 'gpuservice.template.imagePullPolicy': '镜像拉取策略', + 'gpuservice.template.imagePullPolicy.always': '总是拉取', + 'gpuservice.template.imagePullPolicy.ifNotPresent': '不存在时拉取', + 'gpuservice.template.imagePullPolicy.never': '从不拉取', + 'gpuservice.template.command': '容器启动命令', + 'gpuservice.template.command.placeholder': + '每行一个参数,例如:\n/bin/bash\n-c\nyour command', + 'gpuservice.template.mountPath': '挂载路径', + 'gpuservice.template.containerDisk': '容器磁盘(Gi)', + 'gpuservice.template.memory': '内存(Gi)', + 'gpuservice.template.ports': '端口', + 'gpuservice.template.ports.add': '添加端口', + 'gpuservice.template.ports.invalid': '请填写完整的端口配置', + 'gpuservice.template.env': '环境变量', + 'gpuservice.template.env.add': '添加环境变量', + 'gpuservice.template.env.invalid': '请填写完整的环境变量', + 'gpuservice.template.env.name': '名称', + 'gpuservice.template.env.value': '值', + 'gpuservice.template.card.image': '镜像', + 'gpuservice.template.card.mount': '挂载', + 'gpuservice.template.card.resources': '资源', + 'gpuservice.template.card.ports': '端口', + 'gpuservice.publicKey.label': 'SSH 公钥', + 'gpuservice.publicKey.placeholder': + '以 ssh-rsa 或 ssh-ed25519 开头,每个 Public Key 单独一行\n\n查看 Public Key:\n- RSA\ncat ~/.ssh/id_rsa.pub\n- Ed25519\ncat ~/.ssh/id_ed25519.pub' +}; diff --git a/src/locales/zh-CN/no-result.ts b/src/locales/zh-CN/no-result.ts index bf437984..fa02f45b 100644 --- a/src/locales/zh-CN/no-result.ts +++ b/src/locales/zh-CN/no-result.ts @@ -50,5 +50,8 @@ export default { 'noresult.instances.title': '暂无实例', 'noresult.instances.subTitle': '当前没有运行中的模型实例,部署模型后将自动创建。', - 'noresult.instances.nofound': '未找到匹配的实例' + 'noresult.instances.nofound': '未找到匹配的实例', + 'noresult.gpuservice.template.title': '暂无实例模板', + 'noresult.gpuservice.template.subTitle': '创建一个实例模板后会显示在这里', + 'noresult.gpuservice.template.nofound': '未找到匹配的实例模板' }; diff --git a/src/pages/_components/form-overlay-view.module.less b/src/pages/_components/form-overlay-view.module.less index 2ca2dcba..f3a228da 100644 --- a/src/pages/_components/form-overlay-view.module.less +++ b/src/pages/_components/form-overlay-view.module.less @@ -1,3 +1,18 @@ +.mask { + position: fixed; + inset: 0; + z-index: 1004; + height: 100%; + background-color: var(--ant-color-bg-mask); + opacity: 0; + transition: opacity var(--ant-motion-duration-slow) + var(--ant-motion-ease-in-out); +} + +.maskOpen { + opacity: 1; +} + .overlay { position: fixed; top: 0; @@ -15,6 +30,13 @@ -6px 0 16px 0 rgb(0 0 0 / 8%), -3px 0 6px -4px rgb(0 0 0 / 12%), -9px 0 28px 8px rgb(0 0 0 / 5%); + transform: translateX(100%); + transition: transform var(--ant-motion-duration-slow) + var(--ant-motion-ease-in-out); +} + +.overlayOpen { + transform: translateX(0); } .header { diff --git a/src/pages/_components/form-overlay-view.tsx b/src/pages/_components/form-overlay-view.tsx index 7ab4de89..b15f5190 100644 --- a/src/pages/_components/form-overlay-view.tsx +++ b/src/pages/_components/form-overlay-view.tsx @@ -1,5 +1,6 @@ import { ColumnWrapper, IconFont } from '@gpustack/core-ui'; import { Button, Tag } from 'antd'; +import classNames from 'classnames'; import React from 'react'; import { createPortal } from 'react-dom'; import styles from './form-overlay-view.module.less'; @@ -15,6 +16,7 @@ type FormOverlayViewProps = { width?: number | string; className?: string; style?: React.CSSProperties; + maskClosable?: boolean; getContainer?: () => HTMLElement | null | undefined; }; @@ -29,60 +31,90 @@ const FormOverlayView: React.FC = ({ width = 600, className, style, + maskClosable = false, getContainer }) => { const [container, setContainer] = React.useState(null); + const [mounted, setMounted] = React.useState(false); + const [active, setActive] = React.useState(false); React.useEffect(() => { - if (!open) { - setContainer(null); + if (open) { + setContainer(getContainer?.() ?? null); + setMounted(true); return; } - setContainer(getContainer?.() ?? null); + setActive(false); }, [getContainer, open]); - if (!open) { - return null; - } + React.useEffect(() => { + if (!mounted || !container) { + return; + } - if (!container) { + const id = requestAnimationFrame(() => setActive(true)); + return () => cancelAnimationFrame(id); + }, [mounted, container]); + + const handleTransitionEnd = (e: React.TransitionEvent) => { + if (e.target !== e.currentTarget || e.propertyName !== 'transform') { + return; + } + if (!open && !active) { + setMounted(false); + setContainer(null); + } + }; + + if (!mounted || !container) { return null; } return createPortal( -
-
-
- +
+
- {children} - -
, +
+
+ + {children} + +
+ , container ); }; diff --git a/src/pages/backends/styles/backend-list.less b/src/pages/backends/styles/backend-list.less index 9daf36b4..b69e199b 100644 --- a/src/pages/backends/styles/backend-list.less +++ b/src/pages/backends/styles/backend-list.less @@ -6,6 +6,7 @@ justify-content: center; top: 0; left: 0; + min-height: 160px; max-height: 400px; right: 0; diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 8015e430..1f40e274 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -5,7 +5,8 @@ import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui'; import { Empty, Input, Typography } from 'antd'; import { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; -import { mockTemplateData } from '../../templates/config/mock-data'; +import useQueryTemplates from '../../templates/services/use-query-templates'; +import { ListItem as TemplateItem } from '../../templates/config/types'; import { FormData, InstanceTypeItem, ListItem } from '../config/types'; import GPUServiceInstanceForm from '../forms'; import TemplateSelector from '../forms/template-selector'; @@ -64,14 +65,18 @@ const AddModal: React.FC = ({ const [templateKeyword, setTemplateKeyword] = useState(''); const { detailData, fetchData } = useQueryInstanceTypes(); + const { detailData: templatesData, fetchData: fetchTemplates } = + useQueryTemplates(); useEffect(() => { if (open) { fetchData({}); + fetchTemplates({ page: 1, perPage: 100 }); } - }, [open, fetchData]); + }, [open, fetchData, fetchTemplates]); const instanceTypeList = detailData?.items || []; + const templateList = templatesData?.items || []; const filteredInstanceTypes = useMemo(() => { const keyword = instanceKeyword.trim().toLowerCase(); @@ -93,14 +98,14 @@ const AddModal: React.FC = ({ const filteredTemplates = useMemo(() => { const keyword = templateKeyword.trim().toLowerCase(); if (!keyword) { - return mockTemplateData; + return templateList; } - return mockTemplateData.filter((item) => - [item.name, item.image, item.volumeMount].some((text) => + return templateList.filter((item) => + [item.name, item.spec?.image, item.spec?.volumeMount].some((text) => (text || '').toLowerCase().includes(keyword) ) ); - }, [templateKeyword]); + }, [templateKeyword, templateList]); const handleSubmit = () => { form.current?.submit(); @@ -118,7 +123,30 @@ const AddModal: React.FC = ({ }; const handleInstanceTypeChange = (item: InstanceTypeItem) => { - setInstanceTypeName(item.metadata?.name || item.name); + const name = item.metadata?.name || item.name; + setInstanceTypeName(name); + form.current?.setFieldsValue({ + type: name, + spec: { resources: { accelerator: '1' } } + }); + }; + + const handleTemplateChange = (id: number, item: TemplateItem) => { + setTemplateId(id); + form.current?.setFieldsValue({ + spec: { + image: item.spec?.image, + imagePullPolicy: item.spec?.imagePullPolicy, + command: item.spec?.command || [], + ports: item.spec?.ports || [], + env: item.spec?.env || [], + volumeMount: item.spec?.volumeMount, + resources: { + cpu: item.spec?.resources?.cpu, + ram: item.spec?.resources?.ram + } + } + }); }; return ( @@ -194,7 +222,7 @@ const AddModal: React.FC = ({ ) : ( diff --git a/src/pages/gpu-service/instances/config/mock-data.ts b/src/pages/gpu-service/instances/config/mock-data.ts deleted file mode 100644 index fd21d53c..00000000 --- a/src/pages/gpu-service/instances/config/mock-data.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { InstanceStatusValueMap } from '.'; -import { ListItem } from './types'; - -export const mockInstanceData: (ListItem & { cluster_id: number })[] = [ - { - id: 1, - cluster_id: 1, - metadata: { - name: 'cuda-dev-01', - namespace: 'default' - }, - spec: { - type: 'nvidia-l4-small', - image: 'nvidia/cuda:12.4.1-devel-ubuntu22.04', - displayName: 'CUDA Dev 01', - command: ['/bin/bash'], - ports: [{ protocol: 'tcp', port: 22 }], - env: [], - volumeMount: '/workspace', - resources: { - cpu: '4', - ram: '16Gi', - accelerator: '1' - }, - description: 'CUDA development workspace', - volume: { - persistent: { name: 'local-nvme-cache' } - }, - sshPublicKey: { name: 'default' } - }, - status: InstanceStatusValueMap.Ready, - created_at: '2026-04-01T10:00:00Z', - updated_at: '2026-04-10T10:00:00Z' - }, - { - id: 2, - cluster_id: 1, - metadata: { - name: 'training-job-a100', - namespace: 'default' - }, - spec: { - type: 'nvidia-a100-training', - image: 'pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel', - displayName: 'Training A100', - command: ['python', 'train.py'], - ports: [], - env: [{ name: 'PYTHONUNBUFFERED', value: '1' }], - volumeMount: '/data', - resources: { - cpu: '32', - ram: '128Gi', - accelerator: '4' - }, - description: 'PyTorch training environment', - volume: { - persistent: { name: 'shared-model-store' } - }, - sshPublicKey: { name: '' } - }, - status: InstanceStatusValueMap.Pending, - created_at: '2026-04-02T10:00:00Z', - updated_at: '2026-04-11T10:00:00Z' - }, - { - id: 3, - cluster_id: 1, - metadata: { - name: 'inference-vllm', - namespace: 'default' - }, - spec: { - type: 'nvidia-h100-inference', - image: 'vllm/vllm-openai:latest', - displayName: 'Inference vLLM', - command: ['python', '-m', 'vllm.entrypoints.openai.api_server'], - ports: [{ protocol: 'tcp', port: 8080 }], - env: [{ name: 'VLLM_WORKER_MULTIPROC_METHOD', value: 'spawn' }], - volumeMount: '/models', - resources: { - cpu: '16', - ram: '64Gi', - accelerator: '2' - }, - description: 'OpenAI-compatible inference service', - volume: { - ephemeral: { capacity: '100Gi' } - }, - sshPublicKey: { name: '' } - }, - status: InstanceStatusValueMap.Error, - created_at: '2026-04-03T10:00:00Z', - updated_at: '2026-04-12T10:00:00Z' - } -]; diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts index f5befcf2..cd978086 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -35,6 +35,7 @@ export interface FormData { spec: { type: string; image: string; + imagePullPolicy: ImagePullPolicy; displayName: string; command: string[]; ports: InstancePort[]; diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 64245c4d..28fab11b 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -18,15 +18,10 @@ import { useMemo, useRef } from 'react'; -import { mockStorageData } from '../../storage/config/mock-data'; -import { mockTemplateData } from '../../templates/config/mock-data'; -import { - EnvItem as TemplateEnvItem, - PortItem as TemplatePortItem -} from '../../templates/config/types'; +import { DefaultImagePullPolicy } from '../../templates/config'; import TemplateBasicForm from '../../templates/forms/basic'; import { DEFAULT_SSH_PUBLIC_KEY_NAME, StorageModeValueMap } from '../config'; -import { FormData, ListItem } from '../config/types'; +import { FormData, InstancePort, ListItem } from '../config/types'; import Basic from './basic'; import InstanceTypeFormItem from './instance-type'; import StorageVolume from './storage-volume'; @@ -34,16 +29,6 @@ import StorageVolume from './storage-volume'; const SSH_PORT = 22; type InstanceFormValues = FormData & { - // instance-type holders - type?: string; - gpu_count?: number; - // template holders (mirrored into spec.* on submit) - image?: string; - command?: string[]; - ports?: TemplatePortItem[]; - env?: TemplateEnvItem[]; - volumeMount?: string; - resources?: { cpu?: string; ram?: string }; // storage holders storage_mode?: string; storage_name?: string; @@ -75,7 +60,7 @@ const requiredFields = { }, [TABKeysMap.INSTANCE_TYPE]: { sort: 2, - fields: ['type', 'gpu_count'] + fields: ['type'] }, [TABKeysMap.TEMPLATE]: { sort: 3, @@ -144,7 +129,8 @@ const GPUServiceInstanceForm: React.FC = forwardRef( [] ); - const ports = (Form.useWatch('ports', form) || []) as TemplatePortItem[]; + const ports = (Form.useWatch(['spec', 'ports'], form) || + []) as InstancePort[]; const hasSshPort = useMemo( () => ports.some((p) => Number(p?.port) === SSH_PORT), [ports] @@ -180,20 +166,10 @@ const GPUServiceInstanceForm: React.FC = forwardRef( name: currentData.metadata?.name, namespace: currentData.metadata?.namespace }, - spec: { ...currentData.spec }, - type: currentData.spec?.type, - gpu_count: currentData.spec?.resources?.accelerator - ? Number(currentData.spec.resources.accelerator) || 1 - : 1, - // template holders mirrored from spec.* - image: currentData.spec?.image, - command: currentData.spec?.command || [], - ports: (currentData.spec?.ports || []) as TemplatePortItem[], - env: (currentData.spec?.env || []) as TemplateEnvItem[], - volumeMount: currentData.spec?.volumeMount, - resources: { - cpu: currentData.spec?.resources?.cpu, - ram: currentData.spec?.resources?.ram + spec: { + ...currentData.spec, + imagePullPolicy: + currentData.spec?.imagePullPolicy || DefaultImagePullPolicy }, storage_mode: isExisting ? StorageModeValueMap.Existing @@ -207,53 +183,42 @@ const GPUServiceInstanceForm: React.FC = forwardRef( return; } - const defaultTemplate = mockTemplateData[0]; - const defaultStorage = mockStorageData[0]; - form.setFieldsValue({ metadata: { namespace }, spec: { description: '', - displayName: '' - } as FormData['spec'], - gpu_count: 1, - // template holders seeded from default template - image: defaultTemplate?.image || '', - command: defaultTemplate?.command || [], - ports: (defaultTemplate?.ports || []) as TemplatePortItem[], - env: (defaultTemplate?.env || []) as TemplateEnvItem[], - volumeMount: defaultTemplate?.volumeMount || '', - resources: { - cpu: defaultTemplate?.resources?.cpu, - ram: defaultTemplate?.resources?.ram - }, + displayName: '', + image: '', + imagePullPolicy: DefaultImagePullPolicy, + command: [], + ports: [], + env: [], + volumeMount: '', + resources: { + accelerator: '1' + } + } as Partial as FormData['spec'], storage_mode: StorageModeValueMap.Existing, - storage_name: defaultStorage?.metadata?.name, local_storage_size_gb: 50, enable_ssh: false }); }, [action, currentData, form, open, namespace]); const buildPayload = (values: InstanceFormValues): FormData => { - const acceleratorStr = values.gpu_count - ? String(values.gpu_count) - : undefined; + const acceleratorVal = values.spec?.resources?.accelerator; + const acceleratorStr = + acceleratorVal !== undefined && + acceleratorVal !== null && + (acceleratorVal as unknown) !== '' + ? String(acceleratorVal) + : undefined; const spec: FormData['spec'] = { ...values.spec, - type: values.type || values.spec?.type, - image: values.image ?? values.spec?.image, - command: values.command ?? values.spec?.command, - ports: (values.ports ?? - values.spec?.ports) as FormData['spec']['ports'], - env: (values.env ?? values.spec?.env) as FormData['spec']['env'], - volumeMount: values.volumeMount ?? values.spec?.volumeMount, resources: { ...values.spec?.resources, - cpu: values.resources?.cpu ?? values.spec?.resources?.cpu, - ram: values.resources?.ram ?? values.spec?.resources?.ram, accelerator: acceleratorStr } } as FormData['spec']; @@ -272,7 +237,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( } // SSH public key — include only when checkbox is checked AND port 22 is in ports - const portsList = (values.ports || []) as TemplatePortItem[]; + const portsList = (values.spec?.ports || []) as InstancePort[]; const has22 = portsList.some((p) => Number(p?.port) === SSH_PORT); if (values.enable_ssh && has22) { spec.sshPublicKey = { name: DEFAULT_SSH_PUBLIC_KEY_NAME }; @@ -300,6 +265,9 @@ const GPUServiceInstanceForm: React.FC = forwardRef( }, resetFields: () => { form.resetFields(); + }, + setFieldsValue: (values: Partial) => { + form.setFieldsValue(values as any); } })); diff --git a/src/pages/gpu-service/instances/forms/instance-type-overlay.tsx b/src/pages/gpu-service/instances/forms/instance-type-overlay.tsx deleted file mode 100644 index c45baa65..00000000 --- a/src/pages/gpu-service/instances/forms/instance-type-overlay.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import FormOverlayView from '@/pages/_components/form-overlay-view'; -import { SearchOutlined } from '@ant-design/icons'; -import { Empty, Input } from 'antd'; -import { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import InstanceTypeList from '../components/instance-type-list'; -import { InstanceTypeItem } from '../config/types'; - -const DrawerBody = styled.div` - display: flex; - flex-direction: column; - gap: 16px; -`; - -interface InstanceTypeOverlayProps { - open: boolean; - value?: string; - dataList: InstanceTypeItem[]; - onCancel: () => void; - onChange?: (item: InstanceTypeItem) => void; -} - -const InstanceTypeOverlay: React.FC = ({ - open, - value, - dataList, - onCancel, - onChange -}) => { - const [keyword, setKeyword] = useState(''); - - const filteredOptions = useMemo(() => { - const currentKeyword = keyword.trim().toLowerCase(); - if (!currentKeyword) { - return dataList; - } - - return dataList.filter((item) => { - const name = item.metadata?.name || item.name || ''; - return [ - name, - item.spec?.memory ?? '', - item.status?.cpu?.capacity ?? '', - item.status?.ram?.capacity ?? '', - item.status?.accelerator?.remaining ?? '' - ].some((text) => String(text).toLowerCase().includes(currentKeyword)); - }); - }, [keyword, dataList]); - - const handleChange = (item: InstanceTypeItem) => { - onChange?.(item); - onCancel(); - }; - - const getOverlayContainer = useCallback(() => { - const containers = document.querySelectorAll( - '.ant-layout-content' - ); - - return containers[containers.length - 1] ?? null; - }, []); - - return ( - - - } - placeholder="搜索名称、显存、内存或 vCPU" - value={keyword} - onChange={(e) => setKeyword(e.target.value)} - /> - {filteredOptions.length > 0 ? ( - - ) : ( - - )} - - - ); -}; - -export default InstanceTypeOverlay; diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index f9bc0d7a..5e54d4f7 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -4,12 +4,11 @@ import { InputNumber as CInputNumber } from '@gpustack/core-ui'; import { Flex, Form, Tag } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import styled from 'styled-components'; import { InstanceTypePhaseValueMap } from '../config'; import { FormData, InstanceTypeItem } from '../config/types'; import useQueryInstanceTypes from '../services/use-query-instance-types'; -import InstanceTypeOverlay from './instance-type-overlay'; const FieldBlock = styled.div` margin-bottom: 24px; @@ -54,18 +53,12 @@ const InstanceTypePicker: React.FC = ({ onChange, dataList }) => { - const [open, setOpen] = useState(false); - const selected = useMemo(() => { return dataList.find( (item) => (item.metadata?.name || item.name) === value ); }, [value, dataList]); - const handleChange = (item: InstanceTypeItem) => { - onChange?.(item.metadata?.name || item.name); - }; - return ( <> @@ -97,21 +90,12 @@ const InstanceTypePicker: React.FC = ({
- setOpen(false)} - onChange={handleChange} - /> ); }; const InstanceTypeFormItem = () => { - const form = Form.useFormInstance< - FormData & { type?: string; gpu_count?: number } - >(); + const form = Form.useFormInstance(); const typeName = Form.useWatch('type', form) as string | undefined; const { detailData, fetchData } = useQueryInstanceTypes(); @@ -148,13 +132,14 @@ const InstanceTypeFormItem = () => { return; } - const currentGpuCount = form.getFieldValue('gpu_count') ?? 1; - if (currentGpuCount > maxGpuCount) { - form.setFieldValue('gpu_count', maxGpuCount); + const currentAccelerator = + Number(form.getFieldValue(['spec', 'resources', 'accelerator'])) || 1; + if (currentAccelerator > maxGpuCount) { + form.setFieldValue(['spec', 'resources', 'accelerator'], maxGpuCount); } - if (currentGpuCount < 1) { - form.setFieldValue('gpu_count', 1); + if (currentAccelerator < 1) { + form.setFieldValue(['spec', 'resources', 'accelerator'], 1); } }, [form, typeName, maxGpuCount, dataList]); @@ -174,7 +159,7 @@ const InstanceTypeFormItem = () => { void; + onSubmit: (values: StorageFormData) => Promise | void; +} + +const StorageOverlay: React.FC = ({ + open, + namespace, + onCancel, + onSubmit +}) => { + const formRef = useRef(null); + + const handleSubmit = () => { + formRef.current?.submit(); + }; + + const handleCancel = () => { + formRef.current?.resetFields(); + onCancel(); + }; + + const handleFinish = async (values: StorageFormData) => { + await onSubmit(values); + }; + + const getOverlayContainer = useCallback(() => { + const containers = document.querySelectorAll( + '.ant-layout-content' + ); + + return containers[containers.length - 1] ?? null; + }, []); + + return ( + + } + > + + + ); +}; + +export default StorageOverlay; diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index 795611c1..9e1ac252 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -1,10 +1,14 @@ +import { useModel } from '@@/plugin-model'; import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui'; -import { Button, Flex, Form, Radio } from 'antd'; -import { useEffect } from 'react'; +import { Button, Flex, Form, message, Radio } from 'antd'; +import { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { mockStorageData } from '../../storage/config/mock-data'; +import { FormData as StorageFormData } from '../../storage/config/types'; +import useCreateStorage from '../../storage/services/use-create-storage'; +import useQueryStorage from '../../storage/services/use-query-storage'; import { StorageModeValueMap } from '../config'; import { FormData } from '../config/types'; +import StorageOverlay from './storage-overlay'; const FieldBlock = styled.div` margin-bottom: 24px; @@ -17,6 +21,9 @@ type StorageHolderFields = { }; const StorageVolume = () => { + const { initialState } = useModel('@@initialState'); + const namespace = initialState?.currentUser?.org_name || 'default'; + const form = Form.useFormInstance(); const storageMode = Form.useWatch('storage_mode', form) as string | undefined; const storageName = Form.useWatch('storage_name', form) as string | undefined; @@ -24,6 +31,24 @@ const StorageVolume = () => { | number | undefined; + const [overlayOpen, setOverlayOpen] = useState(false); + + const { fetchData: createStorage } = useCreateStorage(); + const { detailData: storageData, fetchData: fetchStorage } = useQueryStorage(); + + useEffect(() => { + fetchStorage({}); + }, [fetchStorage]); + + const storageOptions = useMemo( + () => + (storageData?.items || []).map((item) => ({ + label: `${item.metadata?.name} / ${item.spec?.capacity ?? '-'}`, + value: item.metadata?.name + })), + [storageData] + ); + useEffect(() => { if (storageMode === StorageModeValueMap.Existing) { form.setFieldValue(['spec', 'volume'], { @@ -38,6 +63,18 @@ const StorageVolume = () => { } }, [form, storageMode, storageName, localSize]); + const handleCreateStorage = async (values: StorageFormData) => { + try { + await createStorage({ data: values }); + await fetchStorage({}); + form.setFieldValue('storage_name', values.metadata.name); + setOverlayOpen(false); + message.success('操作成功'); + } catch (error) { + message.error('操作失败'); + } + }; + return ( { ]} /> - @@ -79,14 +121,7 @@ const StorageVolume = () => { } ]} > - )} @@ -103,6 +138,13 @@ const StorageVolume = () => { )} + + setOverlayOpen(false)} + onSubmit={handleCreateStorage} + /> ); }; diff --git a/src/pages/gpu-service/instances/forms/template-overlay.tsx b/src/pages/gpu-service/instances/forms/template-overlay.tsx deleted file mode 100644 index 4a3f00c2..00000000 --- a/src/pages/gpu-service/instances/forms/template-overlay.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import FormOverlayView from '@/pages/_components/form-overlay-view'; -import { PlusOutlined, SearchOutlined } from '@ant-design/icons'; -import { Button, Empty, Flex, Input } from 'antd'; -import { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { mockTemplateData } from '../../templates/config/mock-data'; -import TemplateSelector from './template-selector'; - -const DrawerBody = styled.div` - display: flex; - flex-direction: column; - gap: 16px; -`; - -interface TemplateOverlayProps { - open: boolean; - value?: number; - onCancel: () => void; - onChange?: (value: number) => void; - onCreate?: () => void; -} - -const TemplateOverlay: React.FC = ({ - open, - value, - onCancel, - onChange, - onCreate -}) => { - const [keyword, setKeyword] = useState(''); - - const filteredTemplates = useMemo(() => { - const currentKeyword = keyword.trim().toLowerCase(); - if (!currentKeyword) { - return mockTemplateData; - } - - return mockTemplateData.filter((item) => { - return [item.name, item.image, item.volumeMount].some((text) => - (text || '').toLowerCase().includes(currentKeyword) - ); - }); - }, [keyword]); - - const handleChange = (id: number) => { - onChange?.(id); - onCancel(); - }; - - const getOverlayContainer = useCallback(() => { - const containers = document.querySelectorAll( - '.ant-layout-content' - ); - - return containers[containers.length - 1] ?? null; - }, []); - - return ( - - - - } - placeholder="搜索模板名称、镜像或挂载路径" - value={keyword} - onChange={(e) => setKeyword(e.target.value)} - /> - - - {filteredTemplates.length > 0 ? ( - - ) : ( - - )} - - - ); -}; - -export default TemplateOverlay; diff --git a/src/pages/gpu-service/instances/forms/template-selector.tsx b/src/pages/gpu-service/instances/forms/template-selector.tsx index ce763b9e..6fb45218 100644 --- a/src/pages/gpu-service/instances/forms/template-selector.tsx +++ b/src/pages/gpu-service/instances/forms/template-selector.tsx @@ -1,6 +1,5 @@ import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui'; import styled from 'styled-components'; -import { mockTemplateData } from '../../templates/config/mock-data'; import { ListItem as TemplateItem } from '../../templates/config/types'; const TemplateGrid = styled.div` @@ -43,14 +42,14 @@ const TemplateContent = styled.div` interface TemplateSelectorProps { value?: number; - onChange?: (value: number) => void; + onChange?: (value: number, item: TemplateItem) => void; dataList?: TemplateItem[]; } const TemplateSelector: React.FC = ({ value, onChange, - dataList = mockTemplateData + dataList = [] }) => { return ( @@ -63,7 +62,7 @@ const TemplateSelector: React.FC = ({ height={102} active={value === item.id} disabled={item.status !== 'enabled'} - onClick={() => onChange?.(item.id)} + onClick={() => onChange?.(item.id, item)} >
@@ -76,14 +75,14 @@ const TemplateSelector: React.FC = ({ 镜像: - {item.image || '-'} + {item.spec?.image || '-'}
挂载: - {item.volumeMount || '-'} + {item.spec?.volumeMount || '-'}
diff --git a/src/pages/gpu-service/instances/forms/template.tsx b/src/pages/gpu-service/instances/forms/template.tsx deleted file mode 100644 index e38f782e..00000000 --- a/src/pages/gpu-service/instances/forms/template.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { PageAction } from '@/config'; -import { EditOutlined } from '@ant-design/icons'; -import { AutoTooltip, Input as CInput } from '@gpustack/core-ui'; -import { Button, Flex, Form, message } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { - createGPUServiceTemplate, - updateGPUServiceTemplate -} from '../../templates/apis'; -import AddTemplateModal from '../../templates/components/add-modal'; -import { mockTemplateData } from '../../templates/config/mock-data'; -import { - FormData as TemplateFormData, - ListItem as TemplateListItem -} from '../../templates/config/types'; -import useCreateTemplate from '../../templates/hooks/use-create-template'; -import { FormData } from '../config/types'; -import TemplateOverlay from './template-overlay'; - -const FieldBlock = styled.div` - margin-bottom: 24px; -`; - -const SelectedCard = styled.div` - display: grid; - grid-template-columns: minmax(0, 1fr) max-content; - align-items: center; - gap: 12px; - padding: 14px; - border: 1px solid var(--ant-color-border); - border-radius: var(--ant-border-radius-lg); - background: var(--ant-color-bg-container); -`; - -const SummaryTitle = styled.div` - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 8px; - color: var(--ant-color-text); - font-weight: 500; -`; - -const SummaryMeta = styled.div` - display: grid; - gap: 4px; - color: var(--ant-color-text-secondary); - font-size: 13px; -`; - -interface TemplateSelectorProps { - value?: number; - onChange?: (value: number) => void; - onCreate?: () => void; - onEdit?: (row: TemplateListItem) => void; -} - -const TemplatePicker: React.FC = ({ - value, - onChange, - onCreate, - onEdit -}) => { - const [open, setOpen] = useState(false); - - const selected = useMemo(() => { - return mockTemplateData.find((item) => item.id === value); - }, [value]); - - return ( - <> - -
- - - {selected?.name || '请选择实例模板'} - - - - - 镜像: {selected?.image || '-'} - - 挂载: {selected?.volumeMount || '-'} - -
- - - - -
- setOpen(false)} - onChange={onChange} - onCreate={() => { - onCreate?.(); - }} - /> - - ); -}; - -const TemplateFormItem = () => { - const form = Form.useFormInstance(); - const templateId = Form.useWatch('template_id', form) as number | undefined; - const { openTemplateModalStatus, openTemplateModal, closeTemplateModal } = - useCreateTemplate(); - - const selectedTemplate = useMemo(() => { - return mockTemplateData.find((item) => item.id === templateId); - }, [templateId]); - - useEffect(() => { - if (!templateId && mockTemplateData[0]) { - form.setFieldValue('template_id', mockTemplateData[0].id); - return; - } - - if (selectedTemplate) { - form.setFieldValue(['spec', 'image'], selectedTemplate.image); - form.setFieldValue(['spec', 'command'], selectedTemplate.command || []); - form.setFieldValue(['spec', 'ports'], selectedTemplate.ports || []); - form.setFieldValue(['spec', 'env'], selectedTemplate.env || []); - form.setFieldValue(['spec', 'volumeMount'], selectedTemplate.volumeMount); - const currentResources = form.getFieldValue(['spec', 'resources']) || {}; - form.setFieldValue(['spec', 'resources'], { - ...currentResources, - cpu: selectedTemplate.resources?.cpu, - ram: selectedTemplate.resources?.ram - }); - } - }, [form, selectedTemplate, templateId]); - - const handleAddTemplate = () => { - openTemplateModal(PageAction.CREATE, '添加实例模板'); - }; - - const handleEditTemplate = (row: TemplateListItem) => { - openTemplateModal(PageAction.EDIT, '编辑实例模板', row); - }; - - const handleTemplateModalOk = async (data: TemplateFormData) => { - try { - if (openTemplateModalStatus.action === PageAction.EDIT) { - await updateGPUServiceTemplate({ - id: openTemplateModalStatus.currentData!.id, - data - }); - } else { - await createGPUServiceTemplate({ data }); - } - closeTemplateModal(); - message.success('操作成功'); - } catch (error) { - message.error('操作失败'); - } - }; - - return ( -
- - - - - - - name={['spec', 'image']} hidden> - - - - -
- ); -}; - -export default TemplateFormItem; diff --git a/src/pages/gpu-service/public-keys/apis/index.ts b/src/pages/gpu-service/public-keys/apis/index.ts index 3313994f..ce909ab8 100644 --- a/src/pages/gpu-service/public-keys/apis/index.ts +++ b/src/pages/gpu-service/public-keys/apis/index.ts @@ -1,55 +1,35 @@ import { request } from '@umijs/max'; import { FormData, ListItem } from '../types'; -export const GPU_SERVICE_PUBLIC_KEY_API = (namespace: string) => - `/proxy/apis/worker.gpustack.ai/v1/namespaces/${namespace}/instancesshpublickeys`; +export const GPU_SERVICE_PUBLIC_KEY_API = '/gpu-instance-ssh-public-keys/data'; export async function queryGPUServicePublicKeys( - params: Global.K8sSearchParams, + params: Global.SearchParams, options?: any ) { - return request>( - GPU_SERVICE_PUBLIC_KEY_API(params.namespace || ''), - { - method: 'GET', - params, - cancelToken: options?.token - } - ); + return request(GPU_SERVICE_PUBLIC_KEY_API, { + method: 'GET', + params, + cancelToken: options?.token + }); } -export async function createGPUServicePublicKey(params: { - namespace: string; - data: FormData; -}) { - return request(GPU_SERVICE_PUBLIC_KEY_API(params.namespace), { +export async function createGPUServicePublicKey(params: { data: FormData }) { + return request(GPU_SERVICE_PUBLIC_KEY_API, { method: 'POST', data: params.data }); } -export async function updateGPUServicePublicKey(params: { - namespace: string; - id: number; - data: FormData; -}) { - return request( - `${GPU_SERVICE_PUBLIC_KEY_API(params.namespace)}/${params.id}`, - { - method: 'PUT', - data: params.data - } - ); +export async function updateGPUServicePublicKey(params: { data: FormData }) { + return request(`${GPU_SERVICE_PUBLIC_KEY_API}`, { + method: 'PUT', + data: params.data + }); } -export async function deleteGPUServicePublicKey(params: { - namespace: string; - id: number; -}) { - return request( - `${GPU_SERVICE_PUBLIC_KEY_API(params.namespace)}/${params.id}`, - { - method: 'DELETE' - } - ); +export async function deleteGPUServicePublicKey(id: number) { + return request(`${GPU_SERVICE_PUBLIC_KEY_API}/${id}`, { + method: 'DELETE' + }); } diff --git a/src/pages/gpu-service/public-keys/index.tsx b/src/pages/gpu-service/public-keys/index.tsx index fdf73d6d..b41fc038 100644 --- a/src/pages/gpu-service/public-keys/index.tsx +++ b/src/pages/gpu-service/public-keys/index.tsx @@ -1,31 +1,42 @@ import { INPUT_WIDTH } from '@/constants'; -import { Textarea } from '@gpustack/core-ui'; +import { Input as CInput, Textarea, useAppUtils } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Button, Form, message } from 'antd'; -import React from 'react'; +import { Button, Form } from 'antd'; +import React, { useEffect } from 'react'; import PageBox from '../../_components/page-box'; - -interface PublicKeyFormData { - public_key?: string; -} - -const PLACEHOLDER = `以 ssh-rsa 或 ssh-ed25519 开头,每个 Public Key 单独一行 - -查看 Public Key: -- RSA -cat ~/.ssh/id_rsa.pub -- Ed25519 -cat ~/.ssh/id_ed25519.pub`; +import useGetSshkey from './services/use-get-sshkey'; +import useUpdateSshkey from './services/use-update-sshkey'; +import { FormData } from './types'; const GPUServicePublicKeys: React.FC = () => { const intl = useIntl(); - const [form] = Form.useForm(); + const [form] = Form.useForm(); + const { getRuleMessage } = useAppUtils(); + const { fetchData: fetchSshkey } = useGetSshkey(); + const { fetchData: updateSshkey, loading: updating } = useUpdateSshkey(); + + useEffect(() => { + fetchSshkey({ page: 1, perPage: 100 }).then((res) => { + const data = res?.spec?.data || ''; + form.setFieldsValue({ + name: res?.name, + spec: { + data + } + }); + }); + }, []); const handleSave = async () => { + form.submit(); + }; + + const onFinish = async (values: FormData) => { try { - await form.validateFields(); - message.success('操作成功'); - } catch { + await updateSshkey({ + data: values + }); + } catch (error) { // ignore } }; @@ -36,12 +47,32 @@ const GPUServicePublicKeys: React.FC = () => { style={{ width: INPUT_WIDTH.large }} name="gpuServicePublicKeyForm" form={form} + onFinish={onFinish} + initialValues={{ + name: 'SSHPublicKey', + spec: { + data: '' + } + }} > - name="public_key"> + name="name" hidden> + + + + name={['spec', 'data']} + rules={[ + { + required: true, + message: getRuleMessage('input', 'gpuservice.publicKey.label') + } + ]} + >