From 2763594ae3089701d32d7d6030b461959b210d55 Mon Sep 17 00:00:00 2001 From: jialin Date: Sun, 12 Jul 2026 18:56:18 +0800 Subject: [PATCH] feat(gpu-service): use grouped flavor select in instance type create drawer --- src/locales/en-US/gpuservice.ts | 2 + src/locales/ja-JP/gpuservice.ts | 2 + src/locales/ru-RU/gpuservice.ts | 2 + src/locales/tr-TR/gpuservice.ts | 2 + src/locales/zh-CN/gpuservice.ts | 2 + .../components/add-instance-type-modal.tsx | 107 +++-------- .../instance-types/forms/index.tsx | 175 +++++++++++++++++- .../styles/instance-types.module.less | 18 ++ 8 files changed, 228 insertions(+), 82 deletions(-) diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index a0f0de33..a872db0f 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -138,6 +138,8 @@ export default { 'gpuservice.instanceType.add': 'Add Instance Type', 'gpuservice.instanceType.flavor': 'Flavor', 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', + 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', 'gpuservice.instanceType.product': 'Product', 'gpuservice.instanceType.unitCpu': 'Unit CPU', 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index 26a7de58..09d75d51 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -137,6 +137,8 @@ export default { 'gpuservice.instanceType.add': 'Add Instance Type', 'gpuservice.instanceType.flavor': 'Flavor', 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', + 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', 'gpuservice.instanceType.product': 'Product', 'gpuservice.instanceType.unitCpu': 'Unit CPU', 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index ae87f9b1..43c96d13 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -136,6 +136,8 @@ export default { 'gpuservice.instanceType.add': 'Add Instance Type', 'gpuservice.instanceType.flavor': 'Flavor', 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', + 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', 'gpuservice.instanceType.product': 'Product', 'gpuservice.instanceType.unitCpu': 'Unit CPU', 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index b86ef44d..68633202 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -132,6 +132,8 @@ export default { 'gpuservice.instanceType.add': 'Add Instance Type', 'gpuservice.instanceType.flavor': 'Flavor', 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', + 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', 'gpuservice.instanceType.product': 'Product', 'gpuservice.instanceType.unitCpu': 'Unit CPU', 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index cfcb7484..bc735d7e 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -125,6 +125,8 @@ export default { 'gpuservice.instanceType.add': '添加实例类型', 'gpuservice.instanceType.flavor': '规格', 'gpuservice.instanceType.flavor.required': '请选择实例类型规格', + 'gpuservice.instanceType.flavor.gpuGroup': 'GPU 算力', + 'gpuservice.instanceType.flavor.cpuGroup': 'CPU 算力', 'gpuservice.instanceType.product': '商品', 'gpuservice.instanceType.unitCpu': '单位 CPU', 'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU', diff --git a/src/pages/gpu-service/instance-types/components/add-instance-type-modal.tsx b/src/pages/gpu-service/instance-types/components/add-instance-type-modal.tsx index 4ba83da6..8542d2c5 100644 --- a/src/pages/gpu-service/instance-types/components/add-instance-type-modal.tsx +++ b/src/pages/gpu-service/instance-types/components/add-instance-type-modal.tsx @@ -1,14 +1,11 @@ import useSubmitLock from '@/hooks/use-submit-lock'; -import Separator from '@/pages/llmodels/components/separator'; import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Typography, message } from 'antd'; +import { message } from 'antd'; import { useEffect, useRef, useState } from 'react'; import { FlavorItem, FormData } from '../config/types'; import GPUServiceInstanceTypeForm from '../forms'; import useQueryFlavors from '../services/use-query-flavors'; -import styles from '../styles/instance-types.module.less'; -import FlavorList from './flavor-list'; type AddInstanceTypeModalProps = { title: string; @@ -18,28 +15,6 @@ type AddInstanceTypeModalProps = { onCancel: () => void; }; -const ColTitle: React.FC<{ - children: React.ReactNode; - style?: React.CSSProperties; -}> = ({ children, style }) => ( - - {children} - -); - const AddInstanceTypeModal: React.FC = ({ title, open, @@ -101,63 +76,37 @@ const AddInstanceTypeModal: React.FC = ({ mask={{ closable: false }} keyboard={false} styles={{ - wrapper: { width: 'min(900px, calc(100vw - 220px))' }, + wrapper: { width: 'min(600px, calc(100vw - 220px))' }, body: { overflowY: 'hidden' } }} footer={false} > -
-
- -
-
- - {intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })} - -
- -
-
- -
-
- - } - > - - {intl.formatMessage({ id: 'common.title.config' })} - - - -
-
+ + } + > + + ); }; diff --git a/src/pages/gpu-service/instance-types/forms/index.tsx b/src/pages/gpu-service/instance-types/forms/index.tsx index 0c3e18fd..1e39a34f 100644 --- a/src/pages/gpu-service/instance-types/forms/index.tsx +++ b/src/pages/gpu-service/instance-types/forms/index.tsx @@ -1,15 +1,21 @@ import { validateLabelNameRegxFor63 } from '@/config'; import { + AutoTooltip, Input as CInput, InputNumber, Select as SealSelect, + ThemeTag, useAppUtils } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; +import { Flex, Form } from 'antd'; import { forwardRef, useEffect, useImperativeHandle } from 'react'; +import { formatMemoryDisplay } from '../../instances/config'; +import { manufactureColorMap } from '../../templates/config'; +import { formatManufacturer } from '../../utils'; import { ArchOptions, GPU_INSTANCE_TYPE_OS } from '../config'; import { FlavorItem, FormData } from '../config/types'; +import styles from '../styles/instance-types.module.less'; // RAM / storage are entered as a plain number in GB but stored/submitted as a // "Gi" quantity string. These drive the FormItem's submit (`normalize`) and @@ -23,16 +29,39 @@ const giValueProps = (value?: string | null) => ({ interface InstanceTypeFormProps { ref?: any; open: boolean; - // The flavor picked in the drawer's first column. Its acceleratorGroup / + // The flavor picked from the flavor Select. Its acceleratorGroup / // generalGroup / acceleratable are copied into the created instance type. selectedFlavor?: FlavorItem | null; + flavorList: FlavorItem[]; + flavorLoading?: boolean; + onFlavorChange: (flavor: FlavorItem | null) => void; onFinish: (values: FormData) => Promise; onFinishFailed?: (errorInfo: any) => void; } +// A flavor's title mirrors the flavor card: a generic (no product, no/`generic` +// manufacturer, non-acceleratable) flavor reads as "CPU-only". +const getFlavorTitle = (flavor: FlavorItem) => { + const spec = flavor.spec || {}; + const manufacturer = spec.manufacturer || ''; + const isCpuOnly = + !spec.acceleratable && + !spec.product && + (!manufacturer || manufacturer.toLowerCase() === 'generic'); + return isCpuOnly ? 'CPU-only' : spec.product || flavor.name || '-'; +}; + const GPUServiceInstanceTypeForm: React.FC = forwardRef( (props, ref) => { - const { open, selectedFlavor, onFinish, onFinishFailed } = props; + const { + open, + selectedFlavor, + flavorList, + flavorLoading, + onFlavorChange, + onFinish, + onFinishFailed + } = props; const intl = useIntl(); const { getRuleMessage } = useAppUtils(); const [form] = Form.useForm(); @@ -68,6 +97,119 @@ const GPUServiceInstanceTypeForm: React.FC = forwardRef( } })); + // A flavor's dropdown / selected label shows the same info as the flavor + // Secondary line, dot-separated: manufacturer · memory · sliceable. memory + // and sliceable apply to accelerator (GPU) flavors only; sliceable stays a + // tag. Returns null when a (generic) flavor has nothing to show. + const renderFlavorMeta = (flavor: FlavorItem) => { + const spec = flavor.spec || {}; + const manufacturer = spec.manufacturer || ''; + const color = manufactureColorMap[manufacturer] ?? 'purple'; + const memory = spec.acceleratable + ? formatMemoryDisplay(spec.memory ?? undefined) + : ''; + + const pieces: React.ReactNode[] = []; + if (manufacturer) { + pieces.push( + + {formatManufacturer(manufacturer)} + + ); + } + if (memory) { + pieces.push({memory}); + } + if (spec.acceleratable && spec.sliceable) { + pieces.push( + + {intl.formatMessage({ id: 'gpuservice.instance.sliceable' })} + + ); + } + if (!pieces.length) return null; + + return ( + + {pieces.flatMap((piece, index) => + index === 0 + ? [piece] + : [ + + · + , + piece + ] + )} + + ); + }; + + // Dropdown item: two lines — name on top, meta row below. + const renderFlavorOption = (flavor: FlavorItem) => ( + + + {getFlavorTitle(flavor)} + + {renderFlavorMeta(flavor)} + + ); + + // Collapsed selected value: single line — name then meta inline. + const renderFlavorSelected = (flavor: FlavorItem) => ( + + + {getFlavorTitle(flavor)} + + {renderFlavorMeta(flavor)} + + ); + + // Split flavors into two groups: CPU compute (generic) and GPU compute + // (accelerator). Groups render as labeled sections in the dropdown. + const toFlavorOption = (flavor: FlavorItem) => ({ + value: flavor.name, + label: getFlavorTitle(flavor), + flavor + }); + const cpuFlavors = flavorList.filter((flavor) => !flavor.spec?.acceleratable); + const gpuFlavors = flavorList.filter((flavor) => flavor.spec?.acceleratable); + const flavorOptions = [ + cpuFlavors.length && { + label: intl.formatMessage({ + id: 'gpuservice.instanceType.flavor.cpuGroup' + }), + title: 'cpu', + options: cpuFlavors.map(toFlavorOption) + }, + gpuFlavors.length && { + label: intl.formatMessage({ + id: 'gpuservice.instanceType.flavor.gpuGroup' + }), + title: 'gpu', + options: gpuFlavors.map(toFlavorOption) + } + ].filter(Boolean) as any; + const handleFinish = async (values: FormData) => { // The hardware group / acceleratable flags are not user-editable; they // come from the chosen flavor. os is fixed to lowercase "linux". ram / @@ -117,6 +259,33 @@ const GPUServiceInstanceTypeForm: React.FC = forwardRef( /> + + + onFlavorChange( + flavorList.find((flavor) => flavor.name === val) ?? null + ) + } + optionRender={(option: any) => + renderFlavorOption(option.data.flavor) + } + labelRender={({ value }) => { + const flavor = flavorList.find((item) => item.name === value); + return flavor + ? renderFlavorSelected(flavor) + : ((value ?? '') as React.ReactNode); + }} + /> + +