diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 3e5cd1d4..1c9b9ccf 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_ieeghfm8o3o.js', + iconUrl: '//at.alicdn.com/t/c/font_4613488_tzcsatubq4f.js', isDarkTheme: userSettings.isDarkTheme, defaultColorPrimary: COLOR_PRIMARY }} diff --git a/src/pages/gpu-service/constants.ts b/src/pages/gpu-service/constants.ts index 1267cd22..9bb5918d 100644 --- a/src/pages/gpu-service/constants.ts +++ b/src/pages/gpu-service/constants.ts @@ -3,6 +3,7 @@ export const apiVersion = 'worker.gpustack.ai/v1'; export const KindMapping = { sshPublicKey: 'InstanceSSHPublicKey', instance: 'Instance', + instanceType: 'InstanceType', instancePersistentVolume: 'InstancePersistentVolume', instanceTemplate: 'InstanceTemplate' }; diff --git a/src/pages/gpu-service/instances/apis/index.ts b/src/pages/gpu-service/instances/apis/index.ts index 249e6ff4..a7e93176 100644 --- a/src/pages/gpu-service/instances/apis/index.ts +++ b/src/pages/gpu-service/instances/apis/index.ts @@ -1,6 +1,5 @@ import { request } from '@umijs/max'; -import { mockInstanceData } from '../config/mock-data'; -import { FormData, InstanceTypeItem, ListItem } from '../config/types'; +import { InstanceTypeItem, ListItem } from '../config/types'; export const GPU_SERVICE_INSTANCES_API = (namespace: string) => `/proxy/apis/worker.gpustack.ai/v1/namespaces/${namespace}/instances`; @@ -12,61 +11,64 @@ export async function queryGPUServiceInstances( params: Global.K8sSearchParams, options?: any ) { - // return request>(GPU_SERVICE_INSTANCES_API, { - // method: 'GET', - // params, - // cancelToken: options?.token - // }); - const page = params.page || 1; - const perPage = params.perPage || 10; - const search = params.search?.toLowerCase(); - const clusterId = params.cluster_id; - const filteredData = mockInstanceData.filter((item) => { - const matchSearch = search - ? item.name.toLowerCase().includes(search) - : true; - const matchCluster = clusterId ? item.cluster_id === clusterId : true; - return matchSearch && matchCluster; - }); - const start = (page - 1) * perPage; - const items = filteredData.slice(start, start + perPage); - - return { - items, - pagination: { - total: filteredData.length, - totalPage: Math.ceil(filteredData.length / perPage), - page, - perPage + return request>( + GPU_SERVICE_INSTANCES_API(params.namespace || ''), + { + method: 'GET', + params, + cancelToken: options?.token } - } as Global.K8sPageResponse; + ); } -export async function createGPUServiceInstance(params: { data: FormData }) { - // return request(GPU_SERVICE_INSTANCES_API, { - // method: 'POST', - // data: params.data - // }); - return true; -} - -export async function updateGPUServiceInstance(params: { - id: number; - data: FormData; -}) { - // return request(`${GPU_SERVICE_INSTANCES_API}/${params.id}`, { - // method: 'PUT', - // data: params.data - // }); - return true; -} - -export async function deleteGPUServiceInstance(id: number) { - return request(`${GPU_SERVICE_INSTANCES_API}/${id}`, { - method: 'DELETE' +export async function createGPUServiceInstance( + params: { + namespace: string; + data: Global.K8sCommonData; + }, + option?: any +) { + return request(GPU_SERVICE_INSTANCES_API(params.namespace), { + method: 'POST', + data: params.data, + cancelToken: option?.token }); } +export async function updateGPUServiceInstance( + params: { + namespace: string; + id: number; + data: Global.K8sCommonData; + }, + option?: any +) { + return request( + `${GPU_SERVICE_INSTANCES_API(params.namespace)}/${params.id}`, + { + method: 'PUT', + data: params.data, + cancelToken: option?.token + } + ); +} + +export async function deleteGPUServiceInstance( + params: { + namespace: string; + id: number; + }, + option?: any +) { + return request( + `${GPU_SERVICE_INSTANCES_API(params.namespace)}/${params.id}`, + { + method: 'DELETE', + cancelToken: option?.token + } + ); +} + // =========== Instance Types =========== export async function queryGPUServiceInstanceTypes( diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index 269c96bb..8015e430 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -3,13 +3,13 @@ import Separator from '@/pages/llmodels/components/separator'; import { SearchOutlined } from '@ant-design/icons'; import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui'; import { Empty, Input, Typography } from 'antd'; -import { useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { mockTemplateData } from '../../templates/config/mock-data'; -import { instanceTypeOptions } from '../config'; -import { FormData, ListItem } from '../config/types'; +import { FormData, InstanceTypeItem, ListItem } from '../config/types'; import GPUServiceInstanceForm from '../forms'; import TemplateSelector from '../forms/template-selector'; +import useQueryInstanceTypes from '../services/use-query-instance-types'; import InstanceTypeList from './instance-type-list'; const Container = styled.div` @@ -58,26 +58,37 @@ const AddModal: React.FC = ({ onCancel }) => { const form = useRef(null); - const [instanceTypeId, setInstanceTypeId] = useState(); + const [instanceTypeName, setInstanceTypeName] = useState(); const [templateId, setTemplateId] = useState(); const [instanceKeyword, setInstanceKeyword] = useState(''); const [templateKeyword, setTemplateKeyword] = useState(''); + const { detailData, fetchData } = useQueryInstanceTypes(); + + useEffect(() => { + if (open) { + fetchData({}); + } + }, [open, fetchData]); + + const instanceTypeList = detailData?.items || []; + const filteredInstanceTypes = useMemo(() => { const keyword = instanceKeyword.trim().toLowerCase(); if (!keyword) { - return instanceTypeOptions; + return instanceTypeList; } - return instanceTypeOptions.filter((item) => - [ - item.name, - String(item.gpu_count), - String(item.vram), - String(item.ram), - String(item.vCPU) - ].some((text) => text.toLowerCase().includes(keyword)) - ); - }, [instanceKeyword]); + return instanceTypeList.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(keyword)); + }); + }, [instanceKeyword, instanceTypeList]); const filteredTemplates = useMemo(() => { const keyword = templateKeyword.trim().toLowerCase(); @@ -85,13 +96,9 @@ const AddModal: React.FC = ({ return mockTemplateData; } return mockTemplateData.filter((item) => - [ - item.name, - item.image, - item.vendor, - item.volume_mount_path, - String(item.volume_size_gb ?? '') - ].some((text) => (text || '').toLowerCase().includes(keyword)) + [item.name, item.image, item.volumeMount].some((text) => + (text || '').toLowerCase().includes(keyword) + ) ); }, [templateKeyword]); @@ -110,6 +117,10 @@ const AddModal: React.FC = ({ }); }; + const handleInstanceTypeChange = (item: InstanceTypeItem) => { + setInstanceTypeName(item.metadata?.name || item.name); + }; + return ( = ({ } - placeholder="搜索名称、GPU、显存、内存或 vCPU" + placeholder="搜索名称、显存、内存或 vCPU" value={instanceKeyword} onChange={(e) => setInstanceKeyword(e.target.value)} /> {filteredInstanceTypes.length > 0 ? ( ) : ( @@ -174,7 +185,7 @@ const AddModal: React.FC = ({ } - placeholder="搜索模板名称、镜像、厂商或挂载路径" + placeholder="搜索模板名称、镜像或挂载路径" value={templateKeyword} onChange={(e) => setTemplateKeyword(e.target.value)} /> diff --git a/src/pages/gpu-service/instances/components/instance-type-list.tsx b/src/pages/gpu-service/instances/components/instance-type-list.tsx index 94e19b21..5373a3d6 100644 --- a/src/pages/gpu-service/instances/components/instance-type-list.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-list.tsx @@ -1,8 +1,8 @@ import { AutoTooltip, TemplateCard } from '@gpustack/core-ui'; import { Flex, Tag } from 'antd'; import styled from 'styled-components'; -import { InstanceTypeStatusValueMap, instanceTypeOptions } from '../config'; -import { InstanceItem } from '../config/types'; +import { InstanceTypePhaseValueMap } from '../config'; +import { InstanceTypeItem } from '../config/types'; const TypeGrid = styled.div` display: flex; @@ -38,43 +38,44 @@ const TypeMeta = styled.div` `; interface InstanceTypeListProps { - value?: number; - onChange?: (value: number) => void; - dataList?: InstanceItem[]; + value?: string; + onChange?: (item: InstanceTypeItem) => void; + dataList?: InstanceTypeItem[]; } +const isAvailable = (item: InstanceTypeItem) => + item.status?.phase === InstanceTypePhaseValueMap.Available; + const InstanceTypeList: React.FC = ({ value, onChange, - dataList = instanceTypeOptions + dataList = [] }) => { - const handleSelect = (item: InstanceItem) => { - if (item.status !== InstanceTypeStatusValueMap.Available) { - return; - } - - onChange?.(item.id); + const handleSelect = (item: InstanceTypeItem) => { + if (!isAvailable(item)) return; + onChange?.(item); }; return ( {dataList.map((item) => { - const disabled = item.status !== InstanceTypeStatusValueMap.Available; + const disabled = !isAvailable(item); + const name = item.metadata?.name || item.name; return ( handleSelect(item)} > - {item.name} + {name} = ({ color: 'var(--ant-color-text-tertiary)' }} > - 库存 {item.gpu_count} + 库存 {item.status?.accelerator?.remaining ?? '-'} - 显存 {item.vram} GiB + 显存 {item.spec?.memory ?? '-'} - 内存 {item.ram} GiB - CPU {item.vCPU} + 内存 {item.status?.ram?.capacity ?? '-'} + vCPU {item.status?.cpu?.capacity ?? '-'} diff --git a/src/pages/gpu-service/instances/config/index.ts b/src/pages/gpu-service/instances/config/index.ts index 6ef3f853..4db02bbc 100644 --- a/src/pages/gpu-service/instances/config/index.ts +++ b/src/pages/gpu-service/instances/config/index.ts @@ -1,7 +1,6 @@ import { StatusMaps } from '@/config'; import { StatusType } from '@/config/types'; import { icons } from '@gpustack/core-ui'; -import { InstanceItem } from './types'; export const InstanceStatusValueMap = { Ready: 'ready', @@ -37,42 +36,15 @@ export const rowActionList = [ } ]; -export const InstanceTypeStatusValueMap = { - Available: 'available', - Unavailable: 'unavailable' +export const InstanceTypePhaseValueMap = { + Available: 'Available', + Unavailable: 'Unavailable' }; -export const instanceTypeOptions: InstanceItem[] = [ - { - id: 1, - name: 'NVIDIA L4 Small', - vram: 24, - ram: 64, - vCPU: 16, - gpu_count: 1, - status: InstanceTypeStatusValueMap.Available - }, - { - id: 2, - name: 'NVIDIA A100 Training', - vram: 80, - ram: 256, - vCPU: 64, - gpu_count: 8, - status: InstanceTypeStatusValueMap.Available - }, - { - id: 3, - name: 'NVIDIA H100 Inference', - vram: 80, - ram: 512, - vCPU: 96, - gpu_count: 0, - status: InstanceTypeStatusValueMap.Unavailable - } -]; - export const StorageModeValueMap = { Existing: 'existing', Temporary: 'temporary' }; + +// Constant SSH public key resource name used when SSH is enabled +export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default'; diff --git a/src/pages/gpu-service/instances/config/mock-data.ts b/src/pages/gpu-service/instances/config/mock-data.ts index 17a41934..fd21d53c 100644 --- a/src/pages/gpu-service/instances/config/mock-data.ts +++ b/src/pages/gpu-service/instances/config/mock-data.ts @@ -1,58 +1,94 @@ import { InstanceStatusValueMap } from '.'; import { ListItem } from './types'; -export const mockInstanceData: ListItem[] = [ +export const mockInstanceData: (ListItem & { cluster_id: number })[] = [ { id: 1, - name: 'cuda-dev-01', - instance_type: 'NVIDIA L4 Small', - instance_type_id: 1, - template_id: 1, - image: 'nvidia/cuda:12.4.1-devel-ubuntu22.04', - gpu_count: 1, - replicas: 1, - storage_mode: 'existing', - storage_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, - endpoint: 'https://cuda-dev-01.example.com', - description: 'CUDA development workspace', created_at: '2026-04-01T10:00:00Z', updated_at: '2026-04-10T10:00:00Z' }, { id: 2, - name: 'training-job-a100', - instance_type: 'NVIDIA A100 Training', - instance_type_id: 2, - template_id: 2, - image: 'pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel', - gpu_count: 4, - replicas: 1, - storage_mode: 'existing', - storage_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, - endpoint: 'https://training-job-a100.example.com', - description: 'PyTorch training environment', created_at: '2026-04-02T10:00:00Z', updated_at: '2026-04-11T10:00:00Z' }, { id: 3, - name: 'inference-vllm', - instance_type: 'NVIDIA H100 Inference', - instance_type_id: 3, - template_id: 6, - image: 'vllm/vllm-openai:latest', - gpu_count: 2, - replicas: 2, - storage_mode: 'temporary', - local_storage_size_gb: 100, 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, - endpoint: 'http://inference-vllm.example.com', - description: 'OpenAI-compatible inference service', 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 f139b01d..f5befcf2 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -33,6 +33,7 @@ export interface FormData { namespace: string; }; spec: { + type: string; image: string; displayName: string; command: string[]; diff --git a/src/pages/gpu-service/instances/forms/basic.tsx b/src/pages/gpu-service/instances/forms/basic.tsx index 9907339e..2b913a91 100644 --- a/src/pages/gpu-service/instances/forms/basic.tsx +++ b/src/pages/gpu-service/instances/forms/basic.tsx @@ -7,7 +7,7 @@ const Basic = () => { <> data-field="name" - name="name" + name={['metadata', 'name']} rules={[ { required: true, @@ -17,7 +17,7 @@ const Basic = () => { > - name="description"> + name={['spec', 'description']}> diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index f4722b60..64245c4d 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -11,21 +11,53 @@ import { useWrapperContext } from '@gpustack/core-ui'; import { Form } from 'antd'; -import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; +import { + forwardRef, + useEffect, + useImperativeHandle, + 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 TemplateBasicForm from '../../templates/forms/basic'; -import { instanceTypeOptions, StorageModeValueMap } from '../config'; +import { DEFAULT_SSH_PUBLIC_KEY_NAME, StorageModeValueMap } from '../config'; import { FormData, ListItem } from '../config/types'; import Basic from './basic'; import InstanceTypeFormItem from './instance-type'; 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; + local_storage_size_gb?: number; + // ssh holder + enable_ssh?: boolean; +}; + interface InstanceFormProps { ref?: any; open: boolean; action: PageActionType; currentData?: ListItem | null; + namespace?: string; onFinish: (values: FormData) => Promise; } @@ -43,22 +75,28 @@ const requiredFields = { }, [TABKeysMap.INSTANCE_TYPE]: { sort: 2, - fields: ['instance_type_id'] + fields: ['type', 'gpu_count'] }, [TABKeysMap.TEMPLATE]: { sort: 3, - fields: ['template_id', 'gpu_count'] + fields: ['image', 'ports'] }, [TABKeysMap.STORAGE]: { sort: 4, - fields: ['storage_mode', 'storage_id', 'local_storage_size_gb'] + fields: ['storage_mode', 'storage_name', 'local_storage_size_gb'] } }; const GPUServiceInstanceForm: React.FC = forwardRef( (props, ref) => { - const { action, currentData, open, onFinish } = props; - const [form] = Form.useForm(); + const { + action, + currentData, + open, + namespace = 'default', + onFinish + } = props; + const [form] = Form.useForm(); const scrollTabsRef = useRef(null); const { getScrollElementScrollableHeight } = useWrapperContext(); const { @@ -76,32 +114,41 @@ const GPUServiceInstanceForm: React.FC = forwardRef( ] }); - const segmentOptions = [ - { - value: TABKeysMap.BASIC, - label: '基础信息', - icon: , - field: 'name' - }, - { - value: TABKeysMap.INSTANCE_TYPE, - label: '实例类型', - icon: , - field: 'instanceType' - }, - { - value: TABKeysMap.TEMPLATE, - label: '实例模板', - icon: , - field: 'vendor' - }, - { - value: TABKeysMap.STORAGE, - label: '存储卷', - icon: , - field: 'storage' - } - ]; + const segmentOptions = useMemo( + () => [ + { + value: TABKeysMap.BASIC, + label: '基础信息', + icon: , + field: 'name' + }, + { + value: TABKeysMap.INSTANCE_TYPE, + label: '实例类型', + icon: , + field: 'instanceType' + }, + { + value: TABKeysMap.TEMPLATE, + label: '实例模板', + icon: , + field: 'template' + }, + { + value: TABKeysMap.STORAGE, + label: '存储卷', + icon: , + field: 'storage' + } + ], + [] + ); + + const ports = (Form.useWatch('ports', form) || []) as TemplatePortItem[]; + const hasSshPort = useMemo( + () => ports.some((p) => Number(p?.port) === SSH_PORT), + [ports] + ); const onTargetChange = (key: string) => { scrollTabsRef.current?.handleTargetChange(key); @@ -120,38 +167,132 @@ const GPUServiceInstanceForm: React.FC = forwardRef( } if (action === PageAction.EDIT && currentData) { + const persistentName = currentData.spec?.volume?.persistent?.name; + const ephemeralCapacity = currentData.spec?.volume?.ephemeral?.capacity; + const isExisting = !!persistentName; + const isTemporary = !!ephemeralCapacity; + const localSizeGb = ephemeralCapacity + ? Number(String(ephemeralCapacity).replace(/Gi$/i, '')) || undefined + : undefined; + form.setFieldsValue({ - name: currentData.name, - description: currentData.description, - instance_type: currentData.instance_type, - instance_type_id: currentData.instance_type_id, - template_id: currentData.template_id, - image: currentData.image, - gpu_count: currentData.gpu_count, - replicas: currentData.replicas, - storage_mode: currentData.storage_mode, - storage_id: currentData.storage_id, - local_storage_size_gb: currentData.local_storage_size_gb + metadata: { + 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 + }, + storage_mode: isExisting + ? StorageModeValueMap.Existing + : isTemporary + ? StorageModeValueMap.Temporary + : StorageModeValueMap.Existing, + storage_name: persistentName, + local_storage_size_gb: localSizeGb, + enable_ssh: !!currentData.spec?.sshPublicKey?.name }); return; } - const defaultInstanceType = instanceTypeOptions[0]; const defaultTemplate = mockTemplateData[0]; const defaultStorage = mockStorageData[0]; form.setFieldsValue({ - instance_type: defaultInstanceType?.name, - instance_type_id: defaultInstanceType?.id, - template_id: defaultTemplate?.id, - image: defaultTemplate?.image, + metadata: { + namespace + }, + spec: { + description: '', + displayName: '' + } as FormData['spec'], gpu_count: 1, - replicas: 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 + }, storage_mode: StorageModeValueMap.Existing, - storage_id: defaultStorage?.id, - local_storage_size_gb: 50 + storage_name: defaultStorage?.metadata?.name, + local_storage_size_gb: 50, + enable_ssh: false }); - }, [action, currentData, form, open]); + }, [action, currentData, form, open, namespace]); + + const buildPayload = (values: InstanceFormValues): FormData => { + const acceleratorStr = values.gpu_count + ? String(values.gpu_count) + : 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']; + + // Storage volume mapping + if (values.storage_mode === StorageModeValueMap.Existing) { + spec.volume = { persistent: { name: values.storage_name || '' } }; + } else if (values.storage_mode === StorageModeValueMap.Temporary) { + spec.volume = { + ephemeral: { + capacity: values.local_storage_size_gb + ? `${values.local_storage_size_gb}Gi` + : '' + } + }; + } + + // SSH public key — include only when checkbox is checked AND port 22 is in ports + const portsList = (values.ports || []) as TemplatePortItem[]; + const has22 = portsList.some((p) => Number(p?.port) === SSH_PORT); + if (values.enable_ssh && has22) { + spec.sshPublicKey = { name: DEFAULT_SSH_PUBLIC_KEY_NAME }; + } else { + delete (spec as any).sshPublicKey; + } + + return { + metadata: { + name: values.metadata?.name, + namespace: values.metadata?.namespace || namespace + }, + spec + } as FormData; + }; + + const handleFinish = async (values: InstanceFormValues) => { + const payload = buildPayload(values); + await onFinish(payload); + }; useImperativeHandle(ref, () => ({ submit: () => { @@ -178,7 +319,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef(
@@ -208,18 +349,16 @@ const GPUServiceInstanceForm: React.FC = forwardRef( } ]} /> - - name="enable_ssh" - valuePropName="checked" - style={{ marginBottom: 8 }} - > - - - - data-field="name" - hidden - name={['spec', 'sshPublicKey', 'name']} - > + {hasSshPort && ( + + + + )} + hidden name={['spec', 'sshPublicKey', 'name']}> diff --git a/src/pages/gpu-service/instances/forms/instance-type-overlay.tsx b/src/pages/gpu-service/instances/forms/instance-type-overlay.tsx index d1098523..c45baa65 100644 --- a/src/pages/gpu-service/instances/forms/instance-type-overlay.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type-overlay.tsx @@ -4,7 +4,7 @@ import { Empty, Input } from 'antd'; import { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import InstanceTypeList from '../components/instance-type-list'; -import { instanceTypeOptions } from '../config'; +import { InstanceTypeItem } from '../config/types'; const DrawerBody = styled.div` display: flex; @@ -14,14 +14,16 @@ const DrawerBody = styled.div` interface InstanceTypeOverlayProps { open: boolean; - value?: number; + value?: string; + dataList: InstanceTypeItem[]; onCancel: () => void; - onChange?: (value: number) => void; + onChange?: (item: InstanceTypeItem) => void; } const InstanceTypeOverlay: React.FC = ({ open, value, + dataList, onCancel, onChange }) => { @@ -30,22 +32,23 @@ const InstanceTypeOverlay: React.FC = ({ const filteredOptions = useMemo(() => { const currentKeyword = keyword.trim().toLowerCase(); if (!currentKeyword) { - return instanceTypeOptions; + return dataList; } - return instanceTypeOptions.filter((item) => { + return dataList.filter((item) => { + const name = item.metadata?.name || item.name || ''; return [ - item.name, - String(item.gpu_count), - String(item.vram), - String(item.ram), - String(item.vCPU) - ].some((text) => text.toLowerCase().includes(currentKeyword)); + name, + item.spec?.memory ?? '', + item.status?.cpu?.capacity ?? '', + item.status?.ram?.capacity ?? '', + item.status?.accelerator?.remaining ?? '' + ].some((text) => String(text).toLowerCase().includes(currentKeyword)); }); - }, [keyword]); + }, [keyword, dataList]); - const handleChange = (id: number) => { - onChange?.(id); + const handleChange = (item: InstanceTypeItem) => { + onChange?.(item); onCancel(); }; @@ -70,7 +73,7 @@ const InstanceTypeOverlay: React.FC = ({ } - placeholder="搜索名称、GPU、显存、内存或 vCPU" + placeholder="搜索名称、显存、内存或 vCPU" value={keyword} onChange={(e) => setKeyword(e.target.value)} /> diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index 52bda0a4..f9bc0d7a 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -3,11 +3,12 @@ import { Input as CInput, InputNumber as CInputNumber } from '@gpustack/core-ui'; -import { Flex, Form } from 'antd'; +import { Flex, Form, Tag } from 'antd'; import { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { instanceTypeOptions, InstanceTypeStatusValueMap } from '../config'; -import { FormData } from '../config/types'; +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` @@ -16,7 +17,6 @@ const FieldBlock = styled.div` const SelectedCard = styled.div` display: grid; - height: 102px; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; gap: 12px; @@ -38,83 +38,112 @@ const SummaryTitle = styled.div` const SummaryMeta = styled.div` display: flex; flex-direction: column; - flex-wrap: wrap; gap: 4px; color: var(--ant-color-text-secondary); font-size: 13px; `; interface InstanceTypePickerProps { - value?: number; - onChange?: (value: number) => void; + value?: string; + onChange?: (value: string) => void; + dataList: InstanceTypeItem[]; } const InstanceTypePicker: React.FC = ({ value, - onChange + onChange, + dataList }) => { const [open, setOpen] = useState(false); const selected = useMemo(() => { - return instanceTypeOptions.find((item) => item.id === value); - }, [value]); + return dataList.find( + (item) => (item.metadata?.name || item.name) === value + ); + }, [value, dataList]); + + const handleChange = (item: InstanceTypeItem) => { + onChange?.(item.metadata?.name || item.name); + }; return ( <>
- + - {selected?.name || '请选择实例类型'} + {(selected?.metadata?.name || selected?.name) ?? + '请选择实例类型'} - {/* - 库存 {selected?.gpu_count} - */} + {selected && ( + + 库存 {selected?.status?.accelerator?.remaining ?? '-'} + + )} - 显存 {selected?.vram ?? '-'} GiB + 显存 {selected?.spec?.memory ?? '-'} - 内存 {selected?.ram ?? '-'} GiB - CPU {selected?.vCPU ?? '-'} + 内存 {selected?.status?.ram?.capacity ?? '-'} + vCPU {selected?.status?.cpu?.capacity ?? '-'}
- {/* */}
setOpen(false)} - onChange={onChange} + onChange={handleChange} /> ); }; const InstanceTypeFormItem = () => { - const form = Form.useFormInstance(); - const instanceTypeId = Form.useWatch('instance_type_id', form); + const form = Form.useFormInstance< + FormData & { type?: string; gpu_count?: number } + >(); + const typeName = Form.useWatch('type', form) as string | undefined; - const selectedInstanceType = useMemo(() => { - return instanceTypeOptions.find((item) => item.id === instanceTypeId); - }, [instanceTypeId]); - - const maxGpuCount = selectedInstanceType?.gpu_count ?? 1; + const { detailData, fetchData } = useQueryInstanceTypes(); useEffect(() => { - if (!instanceTypeId) { - const defaultType = instanceTypeOptions.find( - (item) => item.status === InstanceTypeStatusValueMap.Available + fetchData({}); + }, [fetchData]); + + const dataList = detailData?.items || []; + + const selectedInstanceType = useMemo(() => { + return dataList.find( + (item) => (item.metadata?.name || item.name) === typeName + ); + }, [dataList, typeName]); + + const maxGpuCount = useMemo(() => { + const remaining = selectedInstanceType?.status?.accelerator?.remaining; + const num = remaining ? Number(remaining) : NaN; + return Number.isFinite(num) && num > 0 ? num : 1; + }, [selectedInstanceType]); + + useEffect(() => { + if (!typeName && dataList.length > 0) { + const defaultType = dataList.find( + (item) => item.status?.phase === InstanceTypePhaseValueMap.Available ); if (defaultType) { - form.setFieldValue('instance_type_id', defaultType.id); + form.setFieldValue( + 'type', + defaultType.metadata?.name || defaultType.name + ); } return; } @@ -127,17 +156,13 @@ const InstanceTypeFormItem = () => { if (currentGpuCount < 1) { form.setFieldValue('gpu_count', 1); } - - if (selectedInstanceType) { - form.setFieldValue('instance_type', selectedInstanceType.name); - } - }, [form, instanceTypeId, maxGpuCount, selectedInstanceType]); + }, [form, typeName, maxGpuCount, dataList]); return ( <> - - name="instance_type_id" + { } ]} > - + - + { min={1} max={maxGpuCount} precision={0} - label={`GPU 数量`} + label="GPU 数量" required /> - name="instance_type" hidden> + @@ -183,12 +208,16 @@ const InstanceTypeFormItem = () => { }; export const useSelectedInstanceType = () => { - const form = Form.useFormInstance(); - const instanceTypeId = Form.useWatch('instance_type_id', form); + const form = Form.useFormInstance(); + const typeName = Form.useWatch('type', form) as string | undefined; + const { detailData } = useQueryInstanceTypes(); + const dataList = detailData?.items || []; return useMemo(() => { - return instanceTypeOptions.find((item) => item.id === instanceTypeId); - }, [instanceTypeId]); + return dataList.find( + (item) => (item.metadata?.name || item.name) === typeName + ); + }, [dataList, typeName]); }; export default InstanceTypeFormItem; diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index e9ee94fa..795611c1 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -1,5 +1,6 @@ import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui'; import { Button, Flex, Form, Radio } from 'antd'; +import { useEffect } from 'react'; import styled from 'styled-components'; import { mockStorageData } from '../../storage/config/mock-data'; import { StorageModeValueMap } from '../config'; @@ -9,17 +10,33 @@ const FieldBlock = styled.div` margin-bottom: 24px; `; -const StorageHeader = styled.div` - display: flex; - align-items: center; - justify-content: flex-end; - gap: 12px; - margin-bottom: 12px; -`; +type StorageHolderFields = { + storage_mode?: string; + storage_name?: string; + local_storage_size_gb?: number; +}; const StorageVolume = () => { - const form = Form.useFormInstance(); - const storageMode = Form.useWatch('storage_mode', form); + const form = Form.useFormInstance(); + const storageMode = Form.useWatch('storage_mode', form) as string | undefined; + const storageName = Form.useWatch('storage_name', form) as string | undefined; + const localSize = Form.useWatch('local_storage_size_gb', form) as + | number + | undefined; + + useEffect(() => { + if (storageMode === StorageModeValueMap.Existing) { + form.setFieldValue(['spec', 'volume'], { + persistent: { name: storageName || '' } + }); + } else if (storageMode === StorageModeValueMap.Temporary) { + form.setFieldValue(['spec', 'volume'], { + ephemeral: { + capacity: localSize ? `${localSize}Gi` : '' + } + }); + } + }, [form, storageMode, storageName, localSize]); return ( @@ -30,7 +47,7 @@ const StorageVolume = () => { marginBottom: 6 }} > - + { > @@ -56,12 +70,12 @@ const StorageVolume = () => { {storageMode === StorageModeValueMap.Existing && ( - - name="storage_id" + @@ -70,14 +84,14 @@ const StorageVolume = () => { required options={mockStorageData.map((item) => ({ label: `${item.metadata?.name} / ${item.spec?.capacity ?? '-'}`, - value: item.id + value: item.metadata?.name }))} /> )} {storageMode === StorageModeValueMap.Temporary && ( - + = ({ } return mockTemplateData.filter((item) => { - return [ - item.name, - item.image, - item.vendor, - item.volume_mount_path, - String(item.volume_size_gb ?? '') - ].some((text) => (text || '').toLowerCase().includes(currentKeyword)); + return [item.name, item.image, item.volumeMount].some((text) => + (text || '').toLowerCase().includes(currentKeyword) + ); }); }, [keyword]); @@ -73,7 +69,7 @@ const TemplateOverlay: React.FC = ({ } - placeholder="搜索模板名称、镜像、厂商或挂载路径" + placeholder="搜索模板名称、镜像或挂载路径" value={keyword} onChange={(e) => setKeyword(e.target.value)} /> diff --git a/src/pages/gpu-service/instances/forms/template-selector.tsx b/src/pages/gpu-service/instances/forms/template-selector.tsx index 9b9bd909..ce763b9e 100644 --- a/src/pages/gpu-service/instances/forms/template-selector.tsx +++ b/src/pages/gpu-service/instances/forms/template-selector.tsx @@ -81,11 +81,9 @@ const TemplateSelector: React.FC = ({
- 存储: - - - {item.volume_size_gb ?? '-'} GB {item.volume_mount_path || '-'} + 挂载: + {item.volumeMount || '-'}
diff --git a/src/pages/gpu-service/instances/forms/template.tsx b/src/pages/gpu-service/instances/forms/template.tsx index f0f85909..e38f782e 100644 --- a/src/pages/gpu-service/instances/forms/template.tsx +++ b/src/pages/gpu-service/instances/forms/template.tsx @@ -16,7 +16,6 @@ import { } from '../../templates/config/types'; import useCreateTemplate from '../../templates/hooks/use-create-template'; import { FormData } from '../config/types'; -import { useSelectedInstanceType } from './instance-type'; import TemplateOverlay from './template-overlay'; const FieldBlock = styled.div` @@ -25,7 +24,6 @@ const FieldBlock = styled.div` const SelectedCard = styled.div` display: grid; - height: 102px; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; gap: 12px; @@ -78,23 +76,12 @@ const TemplatePicker: React.FC = ({ {selected?.name || '请选择实例模板'} - {/* {selected && ( - - {selected.status === 'enabled' ? '启用' : '停用'} - - )} */} 镜像: {selected?.image || '-'} - - 存储: {selected?.volume_size_gb ?? '-'} GB /{' '} - {selected?.volume_mount_path || '-'} - + 挂载: {selected?.volumeMount || '-'} @@ -115,7 +102,6 @@ const TemplatePicker: React.FC = ({ onCancel={() => setOpen(false)} onChange={onChange} onCreate={() => { - // setOpen(false); onCreate?.(); }} /> @@ -124,10 +110,8 @@ const TemplatePicker: React.FC = ({ }; const TemplateFormItem = () => { - const form = Form.useFormInstance(); - const templateId = Form.useWatch('template_id', form); - const selectedInstanceType = useSelectedInstanceType(); - const maxGpuCount = selectedInstanceType?.gpu_count ?? 1; + const form = Form.useFormInstance(); + const templateId = Form.useWatch('template_id', form) as number | undefined; const { openTemplateModalStatus, openTemplateModal, closeTemplateModal } = useCreateTemplate(); @@ -136,13 +120,23 @@ const TemplateFormItem = () => { }, [templateId]); useEffect(() => { - if (!templateId) { - form.setFieldValue('template_id', mockTemplateData[0]?.id); + if (!templateId && mockTemplateData[0]) { + form.setFieldValue('template_id', mockTemplateData[0].id); return; } if (selectedTemplate) { - form.setFieldValue('image', selectedTemplate.image); + 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]); @@ -174,7 +168,7 @@ const TemplateFormItem = () => { return (
- + { - name="image" hidden> + name={['spec', 'image']} hidden> diff --git a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx index 844f6bfd..8e9261de 100644 --- a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx +++ b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx @@ -24,7 +24,7 @@ const useInstancesColumns = ({ return [ { title: '名称', - dataIndex: 'name', + dataIndex: ['metadata', 'name'], key: 'name', sorter: tableSorter(1), ellipsis: { @@ -54,11 +54,16 @@ const useInstancesColumns = ({ } }, { - title: '实例类型', - dataIndex: 'instance_type', - key: 'instance_type', + title: '镜像', + dataIndex: ['spec', 'image'], + key: 'image', sorter: tableSorter(3), - render: (value: string) => value ?? '-' + ellipsis: { + showTitle: false + }, + render: (value: string) => ( + {value || '-'} + ) }, { title: '创建时间', diff --git a/src/pages/gpu-service/instances/index.tsx b/src/pages/gpu-service/instances/index.tsx index c9fdc163..f667bd40 100644 --- a/src/pages/gpu-service/instances/index.tsx +++ b/src/pages/gpu-service/instances/index.tsx @@ -3,6 +3,7 @@ import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import type { PageActionType } from '@/config/types'; import useTableFetch from '@/hooks/use-table-fetch'; import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; +import { useModel } from '@@/plugin-model'; import { BaseSelect, DeleteModal, @@ -14,20 +15,52 @@ import { useIntl } from '@umijs/max'; import { useMemoizedFn } from 'ahooks'; import { ConfigProvider, Divider, Flex, message, Table } from 'antd'; import _ from 'lodash'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { PageContainerInner } from '../../_components/page-box'; import { - createGPUServiceInstance, deleteGPUServiceInstance, GPU_SERVICE_INSTANCES_API, - queryGPUServiceInstances, - updateGPUServiceInstance + queryGPUServiceInstances } from './apis'; import AddModal from './components/add-modal'; import { FormData, ListItem } from './config/types'; import useInstancesColumns from './hooks/use-instances-columns'; +import useCreateInstance from './services/use-create-instance'; +import useUpdateInstance from './services/use-update-instance'; const GPUService: React.FC = () => { + const { initialState } = useModel('@@initialState'); + const namespace = initialState?.currentUser?.org_name || 'default'; + + const deleteInstance = useCallback( + (id: number) => deleteGPUServiceInstance({ namespace, id }), + [namespace] + ); + + const fetchInstances = useCallback( + async ( + params: any, + options?: any + ): Promise> => { + const res = await queryGPUServiceInstances( + { ...params, namespace }, + options + ); + const total = res.items?.length ?? 0; + const perPage = params.perPage || 10; + return { + items: res.items ?? [], + pagination: { + total, + totalPage: Math.ceil(total / perPage), + page: params.page || 1, + perPage + } + }; + }, + [namespace] + ); + const { dataSource, rowSelection, @@ -44,12 +77,15 @@ const GPUService: React.FC = () => { handleNameChange } = useTableFetch({ key: PaginationKey.Instances, - fetchAPI: queryGPUServiceInstances, - deleteAPI: deleteGPUServiceInstance, + fetchAPI: fetchInstances, + deleteAPI: deleteInstance, watch: false, - API: GPU_SERVICE_INSTANCES_API, + API: GPU_SERVICE_INSTANCES_API(namespace), contentForDelete: 'GPU 实例' }); + + const { fetchData: createInstance } = useCreateInstance(); + const { fetchData: updateInstance } = useUpdateInstance(); const { fetchClusterList, cancelRequest: cancelClusterRequest, @@ -113,12 +149,12 @@ const GPUService: React.FC = () => { const handleModalOk = async (data: FormData) => { try { if (openAddModalStatus.action === PageAction.EDIT) { - await updateGPUServiceInstance({ + await updateInstance({ id: openAddModalStatus.currentData!.id, data }); } else { - await createGPUServiceInstance({ data }); + await createInstance({ data }); } fetchData(); @@ -133,7 +169,7 @@ const GPUService: React.FC = () => { if (val === 'edit') { handleEditInstance(row); } else if (val === 'delete') { - handleDelete({ ...row, name: row.name }); + handleDelete({ ...row, name: row.metadata?.name }); } }); diff --git a/src/pages/gpu-service/instances/services/use-create-instance.ts b/src/pages/gpu-service/instances/services/use-create-instance.ts new file mode 100644 index 00000000..a9406684 --- /dev/null +++ b/src/pages/gpu-service/instances/services/use-create-instance.ts @@ -0,0 +1,50 @@ +import { useModel } from '@@/plugin-model'; +import { useQueryData } from '@gpustack/core-ui'; +import { useCallback } from 'react'; +import { apiVersion, KindMapping } from '../../constants'; +import { createGPUServiceInstance } from '../apis'; +import { FormData, ListItem } from '../config/types'; + +interface CreateInstanceParams { + data: FormData; +} + +export default function useCreateInstance() { + const { initialState } = useModel('@@initialState'); + const namespace = initialState?.currentUser?.org_name || 'default'; + + const fetchDetail = useCallback( + (params: CreateInstanceParams, option?: any) => + createGPUServiceInstance( + { + namespace, + data: { + apiVersion, + kind: KindMapping.instance, + metadata: { + name: params.data.metadata.name, + namespace + }, + spec: params.data.spec + } + }, + option + ), + [namespace] + ); + + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + ListItem, + CreateInstanceParams + >({ + fetchDetail, + key: 'createInstance' + }); + + return { + detailData, + loading, + cancelRequest, + fetchData + }; +} diff --git a/src/pages/gpu-service/instances/services/use-query-instance-types.ts b/src/pages/gpu-service/instances/services/use-query-instance-types.ts new file mode 100644 index 00000000..ab013cc0 --- /dev/null +++ b/src/pages/gpu-service/instances/services/use-query-instance-types.ts @@ -0,0 +1,31 @@ +import { useModel } from '@@/plugin-model'; +import { useQueryData } from '@gpustack/core-ui'; +import { useCallback } from 'react'; +import { queryGPUServiceInstanceTypes } from '../apis'; +import { InstanceTypeItem } from '../config/types'; + +export default function useQueryInstanceTypes() { + const { initialState } = useModel('@@initialState'); + const namespace = initialState?.currentUser?.org_name || 'default'; + + const fetchDetail = useCallback( + (params: Global.K8sSearchParams = {}, options?: any) => + queryGPUServiceInstanceTypes({ ...params, namespace }, options), + [namespace] + ); + + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + Global.K8sPageResponse, + Global.K8sSearchParams + >({ + fetchDetail, + key: 'instanceTypes' + }); + + return { + detailData, + loading, + cancelRequest, + fetchData + }; +} diff --git a/src/pages/gpu-service/instances/services/use-update-instance.ts b/src/pages/gpu-service/instances/services/use-update-instance.ts new file mode 100644 index 00000000..d25ffd49 --- /dev/null +++ b/src/pages/gpu-service/instances/services/use-update-instance.ts @@ -0,0 +1,52 @@ +import { useModel } from '@@/plugin-model'; +import { useQueryData } from '@gpustack/core-ui'; +import { useCallback } from 'react'; +import { apiVersion, KindMapping } from '../../constants'; +import { updateGPUServiceInstance } from '../apis'; +import { FormData, ListItem } from '../config/types'; + +interface UpdateInstanceParams { + id: number; + data: FormData; +} + +export default function useUpdateInstance() { + const { initialState } = useModel('@@initialState'); + const namespace = initialState?.currentUser?.org_name || 'default'; + + const fetchDetail = useCallback( + (params: UpdateInstanceParams, option?: any) => + updateGPUServiceInstance( + { + namespace, + id: params.id, + data: { + apiVersion, + kind: KindMapping.instance, + metadata: { + name: params.data.metadata.name, + namespace + }, + spec: params.data.spec + } + }, + option + ), + [namespace] + ); + + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + ListItem, + UpdateInstanceParams + >({ + fetchDetail, + key: 'updateInstance' + }); + + return { + detailData, + loading, + cancelRequest, + fetchData + }; +} diff --git a/src/pages/gpu-service/templates/forms/basic.tsx b/src/pages/gpu-service/templates/forms/basic.tsx index d65a034f..dbb5da33 100644 --- a/src/pages/gpu-service/templates/forms/basic.tsx +++ b/src/pages/gpu-service/templates/forms/basic.tsx @@ -8,7 +8,7 @@ interface BasicProps { page?: 'template' | 'instance'; } -const Basic: React.FC = () => { +const Basic: React.FC = ({ page = 'template' }) => { const form = Form.useFormInstance(); const handleCommandChange = (e: React.ChangeEvent) => { @@ -25,17 +25,19 @@ const Basic: React.FC = () => { return ( <> - - name="name" - rules={[ - { - required: true, - message: '请输入模板名称' - } - ]} - > - - + {page === 'template' && ( + + name="name" + rules={[ + { + required: true, + message: '请输入模板名称' + } + ]} + > + + + )} name="image" rules={[