From 35d56810085044143bce1db7ffa003371ee8b645 Mon Sep 17 00:00:00 2001 From: jialin Date: Sun, 12 Jul 2026 16:42:28 +0800 Subject: [PATCH] feat(gpu-service): add GPU Instance Types management page - Card/table list of instance types per Kubernetes cluster with delete - Create drawer with flavor picker and per-GPU resource form (CPU/RAM/storage) - Fix unit CPU to 1 for non-acceleratable flavors; N/A memory for generic types - Add routes and i18n across all locales --- config/routes.ts | 10 + src/locales/en-US/gpuservice.ts | 13 + src/locales/en-US/menu.ts | 1 + src/locales/en-US/no-result.ts | 4 + src/locales/ja-JP/gpuservice.ts | 13 + src/locales/ja-JP/menu.ts | 1 + src/locales/ja-JP/no-result.ts | 4 + src/locales/ru-RU/gpuservice.ts | 13 + src/locales/ru-RU/menu.ts | 1 + src/locales/ru-RU/no-result.ts | 4 + src/locales/tr-TR/gpuservice.ts | 13 + src/locales/tr-TR/menu.ts | 1 + src/locales/tr-TR/no-result.ts | 4 + src/locales/zh-CN/gpuservice.ts | 13 + src/locales/zh-CN/menu.ts | 1 + src/locales/zh-CN/no-result.ts | 3 + .../gpu-service/instance-types/apis/index.ts | 54 ++++ .../components/add-instance-type-modal.tsx | 165 +++++++++++++ .../instance-types/components/flavor-list.tsx | 129 ++++++++++ .../components/instance-type-card.tsx | 147 +++++++++++ .../components/instance-type-list.tsx | 31 +++ .../instance-types/config/index.ts | 47 ++++ .../instance-types/config/types.ts | 65 +++++ .../instance-types/forms/index.tsx | 228 +++++++++++++++++ .../hooks/use-create-instance-type-modal.ts | 37 +++ .../hooks/use-instance-type-columns.tsx | 224 +++++++++++++++++ .../gpu-service/instance-types/index.tsx | 231 ++++++++++++++++++ .../services/use-create-instance-type.ts | 35 +++ .../services/use-query-flavors.ts | 58 +++++ .../services/use-query-instance-types.ts | 60 +++++ .../styles/instance-types.module.less | 150 ++++++++++++ 31 files changed, 1760 insertions(+) create mode 100644 src/pages/gpu-service/instance-types/apis/index.ts create mode 100644 src/pages/gpu-service/instance-types/components/add-instance-type-modal.tsx create mode 100644 src/pages/gpu-service/instance-types/components/flavor-list.tsx create mode 100644 src/pages/gpu-service/instance-types/components/instance-type-card.tsx create mode 100644 src/pages/gpu-service/instance-types/components/instance-type-list.tsx create mode 100644 src/pages/gpu-service/instance-types/config/index.ts create mode 100644 src/pages/gpu-service/instance-types/config/types.ts create mode 100644 src/pages/gpu-service/instance-types/forms/index.tsx create mode 100644 src/pages/gpu-service/instance-types/hooks/use-create-instance-type-modal.ts create mode 100644 src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx create mode 100644 src/pages/gpu-service/instance-types/index.tsx create mode 100644 src/pages/gpu-service/instance-types/services/use-create-instance-type.ts create mode 100644 src/pages/gpu-service/instance-types/services/use-query-flavors.ts create mode 100644 src/pages/gpu-service/instance-types/services/use-query-instance-types.ts create mode 100644 src/pages/gpu-service/instance-types/styles/instance-types.module.less diff --git a/config/routes.ts b/config/routes.ts index 5b45a427..08e50efb 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -211,6 +211,16 @@ const baseRoutes = [ defaultIcon: 'icon-cloud-outlined', component: './gpu-service/instances' }, + { + name: 'instanceTypes', + path: '/gpu-service/instance-types', + key: 'gpuServiceInstanceTypes', + icon: 'icon-outline-gpu', + access: 'canSeeOrgAdmin', + selectedIcon: 'icon-filled-gpu', + defaultIcon: 'icon-outline-gpu', + component: './gpu-service/instance-types' + }, { name: 'templates', path: '/gpu-service/templates', diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index 9dc4508d..a0f0de33 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -134,6 +134,19 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'OS', 'gpuservice.instance.arch': 'Arch', +'gpuservice.instanceType': 'GPU Instance Type', + 'gpuservice.instanceType.add': 'Add Instance Type', + 'gpuservice.instanceType.flavor': 'Flavor', + 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.product': 'Product', + 'gpuservice.instanceType.unitCpu': 'Unit CPU', + 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', + 'gpuservice.instanceType.unitRam': 'Unit RAM', + 'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU', + 'gpuservice.instanceType.localStorage': 'Storage', + 'gpuservice.instanceType.localStorage.tip': 'Maximum available disk', + 'gpuservice.instanceType.notSliceable': 'Not Sliceable', + 'gpuservice.instanceType.filter.name': 'Search by name', 'gpuservice.instance.disk': 'Disk', 'gpuservice.table.count': 'Count', 'gpuservice.instance.disk.system': 'System Disk', diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 2ca56f24..14eead42 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -47,6 +47,7 @@ export default { 'menu.models.backendsList': 'Inference Backends', 'menu.gpuService': 'GPU Service', 'menu.gpuService.instances': 'GPU Instances', + 'menu.gpuService.instanceTypes': 'Instance Types', 'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.storage': 'Storage', 'menu.gpuService.storageTypes': 'Storage Types', diff --git a/src/locales/en-US/no-result.ts b/src/locales/en-US/no-result.ts index d88d7795..8b54ec54 100644 --- a/src/locales/en-US/no-result.ts +++ b/src/locales/en-US/no-result.ts @@ -68,6 +68,10 @@ export default { 'noresult.gpuservice.storage.title': 'No Storage', 'noresult.gpuservice.storage.subTitle': 'No storage has been added yet.', 'noresult.gpuservice.storage.nofound': 'No matching storage found.', + 'noresult.gpuservice.instanceType.title': 'No Instance Types', + 'noresult.gpuservice.instanceType.subTitle': + 'Create an instance type to get started', + 'noresult.gpuservice.instanceType.nofound': 'No instance types found', 'noresult.gpuservice.storageType.title': 'No Storage Types', 'noresult.gpuservice.storageType.subTitle': 'No storage types have been added yet.', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index 0244308c..26a7de58 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -133,6 +133,19 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'OS', 'gpuservice.instance.arch': 'アーキテクチャ', +'gpuservice.instanceType': 'GPU Instance Type', + 'gpuservice.instanceType.add': 'Add Instance Type', + 'gpuservice.instanceType.flavor': 'Flavor', + 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.product': 'Product', + 'gpuservice.instanceType.unitCpu': 'Unit CPU', + 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', + 'gpuservice.instanceType.unitRam': 'Unit RAM', + 'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU', + 'gpuservice.instanceType.localStorage': 'Storage', + 'gpuservice.instanceType.localStorage.tip': 'Maximum available disk', + 'gpuservice.instanceType.notSliceable': 'Not Sliceable', + 'gpuservice.instanceType.filter.name': 'Search by name', 'gpuservice.instance.disk': 'ディスク', 'gpuservice.table.count': '数量', 'gpuservice.instance.disk.system': 'システムディスク', diff --git a/src/locales/ja-JP/menu.ts b/src/locales/ja-JP/menu.ts index cfb0e12a..529e2b56 100644 --- a/src/locales/ja-JP/menu.ts +++ b/src/locales/ja-JP/menu.ts @@ -47,6 +47,7 @@ export default { 'menu.models.backendsList': 'Inference Backends', 'menu.gpuService': 'GPU Service', 'menu.gpuService.instances': 'GPU Instances', + 'menu.gpuService.instanceTypes': 'Instance Types', 'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.storage': 'Storage', 'menu.gpuService.storageTypes': 'ストレージタイプ', diff --git a/src/locales/ja-JP/no-result.ts b/src/locales/ja-JP/no-result.ts index f27b86d0..7867d7c6 100644 --- a/src/locales/ja-JP/no-result.ts +++ b/src/locales/ja-JP/no-result.ts @@ -70,6 +70,10 @@ export default { 'noresult.gpuservice.storage.subTitle': 'ストレージはまだ追加されていません。', 'noresult.gpuservice.storage.nofound': '一致するストレージが見つかりません。', + 'noresult.gpuservice.instanceType.title': 'No Instance Types', + 'noresult.gpuservice.instanceType.subTitle': + 'Create an instance type to get started', + 'noresult.gpuservice.instanceType.nofound': 'No instance types found', 'noresult.gpuservice.storageType.title': 'ストレージタイプなし', 'noresult.gpuservice.storageType.subTitle': 'ストレージタイプはまだ追加されていません。', diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index 6b7707f7..ae87f9b1 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -132,6 +132,19 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'ОС', 'gpuservice.instance.arch': 'Архитектура', +'gpuservice.instanceType': 'GPU Instance Type', + 'gpuservice.instanceType.add': 'Add Instance Type', + 'gpuservice.instanceType.flavor': 'Flavor', + 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.product': 'Product', + 'gpuservice.instanceType.unitCpu': 'Unit CPU', + 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', + 'gpuservice.instanceType.unitRam': 'Unit RAM', + 'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU', + 'gpuservice.instanceType.localStorage': 'Storage', + 'gpuservice.instanceType.localStorage.tip': 'Maximum available disk', + 'gpuservice.instanceType.notSliceable': 'Not Sliceable', + 'gpuservice.instanceType.filter.name': 'Search by name', 'gpuservice.instance.disk': 'Диск', 'gpuservice.table.count': 'Количество', 'gpuservice.instance.disk.system': 'Системный диск', diff --git a/src/locales/ru-RU/menu.ts b/src/locales/ru-RU/menu.ts index ec5c0a56..15a7d385 100644 --- a/src/locales/ru-RU/menu.ts +++ b/src/locales/ru-RU/menu.ts @@ -47,6 +47,7 @@ export default { 'menu.settings': 'Settings', 'menu.gpuService': 'GPU Service', 'menu.gpuService.instances': 'GPU Instances', + 'menu.gpuService.instanceTypes': 'Instance Types', 'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.storage': 'Storage', 'menu.gpuService.storageTypes': 'Типы хранилищ', diff --git a/src/locales/ru-RU/no-result.ts b/src/locales/ru-RU/no-result.ts index 9963f828..4a62bcf4 100644 --- a/src/locales/ru-RU/no-result.ts +++ b/src/locales/ru-RU/no-result.ts @@ -69,6 +69,10 @@ export default { 'noresult.gpuservice.storage.title': 'Нет хранилищ', 'noresult.gpuservice.storage.subTitle': 'Хранилища ещё не добавлены.', 'noresult.gpuservice.storage.nofound': 'Подходящие хранилища не найдены.', + 'noresult.gpuservice.instanceType.title': 'No Instance Types', + 'noresult.gpuservice.instanceType.subTitle': + 'Create an instance type to get started', + 'noresult.gpuservice.instanceType.nofound': 'No instance types found', 'noresult.gpuservice.storageType.title': 'Нет типов хранилищ', 'noresult.gpuservice.storageType.subTitle': 'Типы хранилищ ещё не добавлены.', 'noresult.gpuservice.storageType.nofound': diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index 520c609f..b86ef44d 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -128,6 +128,19 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'OS', 'gpuservice.instance.arch': 'Mimari', +'gpuservice.instanceType': 'GPU Instance Type', + 'gpuservice.instanceType.add': 'Add Instance Type', + 'gpuservice.instanceType.flavor': 'Flavor', + 'gpuservice.instanceType.flavor.required': 'Please select an instance type flavor', + 'gpuservice.instanceType.product': 'Product', + 'gpuservice.instanceType.unitCpu': 'Unit CPU', + 'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU', + 'gpuservice.instanceType.unitRam': 'Unit RAM', + 'gpuservice.instanceType.unitRam.tip': 'RAM allocated per GPU', + 'gpuservice.instanceType.localStorage': 'Storage', + 'gpuservice.instanceType.localStorage.tip': 'Maximum available disk', + 'gpuservice.instanceType.notSliceable': 'Not Sliceable', + 'gpuservice.instanceType.filter.name': 'Search by name', 'gpuservice.instance.disk': 'Disk', 'gpuservice.table.count': 'Sayı', 'gpuservice.instance.disk.system': 'Sistem Diski', diff --git a/src/locales/tr-TR/menu.ts b/src/locales/tr-TR/menu.ts index c744049f..8a879966 100644 --- a/src/locales/tr-TR/menu.ts +++ b/src/locales/tr-TR/menu.ts @@ -47,6 +47,7 @@ export default { 'menu.settings': 'Settings', 'menu.gpuService': 'GPU Service', 'menu.gpuService.instances': 'GPU Instances', + 'menu.gpuService.instanceTypes': 'Instance Types', 'menu.gpuService.templates': 'Instance Templates', 'menu.gpuService.storage': 'Storage', 'menu.gpuService.storageTypes': 'Depolama Türleri', diff --git a/src/locales/tr-TR/no-result.ts b/src/locales/tr-TR/no-result.ts index ec4f76df..7270f1a0 100644 --- a/src/locales/tr-TR/no-result.ts +++ b/src/locales/tr-TR/no-result.ts @@ -67,6 +67,10 @@ export default { 'noresult.gpuservice.storage.title': 'Depolama Yok', 'noresult.gpuservice.storage.subTitle': 'Henüz depolama eklenmedi.', 'noresult.gpuservice.storage.nofound': 'Eşleşen depolama bulunamadı.', + 'noresult.gpuservice.instanceType.title': 'No Instance Types', + 'noresult.gpuservice.instanceType.subTitle': + 'Create an instance type to get started', + 'noresult.gpuservice.instanceType.nofound': 'No instance types found', 'noresult.gpuservice.storageType.title': 'Depolama Türü Yok', 'noresult.gpuservice.storageType.subTitle': 'Henüz depolama türü eklenmedi.', 'noresult.gpuservice.storageType.nofound': diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index c8b965d0..cfcb7484 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -121,6 +121,19 @@ export default { 'gpuservice.instance.ram': '内存', 'gpuservice.instance.os': '系统', 'gpuservice.instance.arch': '架构', +'gpuservice.instanceType': 'GPU 实例类型', + 'gpuservice.instanceType.add': '添加实例类型', + 'gpuservice.instanceType.flavor': '规格', + 'gpuservice.instanceType.flavor.required': '请选择实例类型规格', + 'gpuservice.instanceType.product': '商品', + 'gpuservice.instanceType.unitCpu': '单位 CPU', + 'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU', + 'gpuservice.instanceType.unitRam': '单位内存', + 'gpuservice.instanceType.unitRam.tip': '每 GPU 对应多少内存', + 'gpuservice.instanceType.localStorage': '存储', + 'gpuservice.instanceType.localStorage.tip': '最大可用磁盘', + 'gpuservice.instanceType.notSliceable': '不可切分', + 'gpuservice.instanceType.filter.name': '按名称搜索', 'gpuservice.instance.disk': '磁盘', 'gpuservice.table.count': '数量', 'gpuservice.instance.disk.system': '系统盘', diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 454e9321..5b325b6f 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -47,6 +47,7 @@ export default { 'menu.models.backendsList': '推理后端', 'menu.gpuService': 'GPU 服务', 'menu.gpuService.instances': 'GPU 实例', + 'menu.gpuService.instanceTypes': '实例类型', 'menu.gpuService.templates': '实例模板', 'menu.gpuService.storage': '存储', 'menu.gpuService.storageTypes': '存储类型', diff --git a/src/locales/zh-CN/no-result.ts b/src/locales/zh-CN/no-result.ts index 856ca92d..b6bbb504 100644 --- a/src/locales/zh-CN/no-result.ts +++ b/src/locales/zh-CN/no-result.ts @@ -63,6 +63,9 @@ export default { 'noresult.gpuservice.storage.title': '暂无存储', 'noresult.gpuservice.storage.subTitle': '尚未添加任何存储。', 'noresult.gpuservice.storage.nofound': '未找到匹配的存储', + 'noresult.gpuservice.instanceType.title': '暂无实例类型', + 'noresult.gpuservice.instanceType.subTitle': '创建一个实例类型以开始使用', + 'noresult.gpuservice.instanceType.nofound': '未找到实例类型', 'noresult.gpuservice.storageType.title': '暂无存储类型', 'noresult.gpuservice.storageType.subTitle': '尚未添加任何存储类型。', 'noresult.gpuservice.storageType.nofound': '未找到匹配的存储类型', diff --git a/src/pages/gpu-service/instance-types/apis/index.ts b/src/pages/gpu-service/instance-types/apis/index.ts new file mode 100644 index 00000000..868a335d --- /dev/null +++ b/src/pages/gpu-service/instance-types/apis/index.ts @@ -0,0 +1,54 @@ +import { request } from '@umijs/max'; +import { FlavorItem, FormData, ListItem } from '../config/types'; + +export const GPU_INSTANCE_TYPES_API = '/gpu-instance-types'; + +export const GPU_INSTANCE_TYPE_FLAVORS_API = '/gpu-instance-type-flavors'; + +// GET /gpu-instance-types?cluster_id — instance types defined on a cluster. +export async function queryGPUInstanceTypes( + params: { cluster_id: number }, + options?: any +) { + return request<{ items: ListItem[] }>(GPU_INSTANCE_TYPES_API, { + method: 'GET', + params, + cancelToken: options?.token + }); +} + +// GET /gpu-instance-type-flavors?cluster_id — the hardware flavors a new +// instance type can be based on. +export async function queryGPUInstanceTypeFlavors( + params: { cluster_id: number }, + options?: any +) { + return request<{ items: FlavorItem[] }>(GPU_INSTANCE_TYPE_FLAVORS_API, { + method: 'GET', + params, + cancelToken: options?.token + }); +} + +// POST /gpu-instance-types?cluster_id (GPUInstanceTypeCreate). +export async function createGPUInstanceType(params: { + cluster_id: number; + data: FormData; +}) { + return request(GPU_INSTANCE_TYPES_API, { + method: 'POST', + params: { cluster_id: params.cluster_id }, + data: params.data + }); +} + +// DELETE /gpu-instance-types/{name}?cluster_id. +export async function deleteGPUInstanceType(params: { + name: string; + cluster_id: number; +}) { + return request(`${GPU_INSTANCE_TYPES_API}/${params.name}`, { + method: 'DELETE', + params: { cluster_id: params.cluster_id } + }); +} 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 new file mode 100644 index 00000000..4ba83da6 --- /dev/null +++ b/src/pages/gpu-service/instance-types/components/add-instance-type-modal.tsx @@ -0,0 +1,165 @@ +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 { 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; + open: boolean; + clusterId?: number; + onOk: (values: FormData) => void; + onCancel: () => void; +}; + +const ColTitle: React.FC<{ + children: React.ReactNode; + style?: React.CSSProperties; +}> = ({ children, style }) => ( + + {children} + +); + +const AddInstanceTypeModal: React.FC = ({ + title, + open, + clusterId, + onOk, + onCancel +}) => { + const intl = useIntl(); + const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); + const [selectedFlavor, setSelectedFlavor] = useState(null); + const { + dataList: flavorList, + loading: flavorLoading, + fetchFlavors + } = useQueryFlavors(); + + // Fetch flavors when the drawer opens and auto-select the first one, so the + // form's flavor-derived fields (group / acceleratable) are always set. + useEffect(() => { + if (!open) { + setSelectedFlavor(null); + return; + } + if (!clusterId) return; + const load = async () => { + const list = await fetchFlavors(clusterId); + setSelectedFlavor(list?.[0] ?? null); + }; + load(); + }, [open, clusterId]); + + const handleSubmit = () => { + if (!selectedFlavor) { + message.warning( + intl.formatMessage({ id: 'gpuservice.instanceType.flavor.required' }) + ); + return; + } + guard(() => form.current?.submit()); + }; + + const handleCancel = () => { + form.current?.resetFields(); + onCancel(); + }; + + const onFinish = async (values: FormData) => { + await run(() => onOk({ ...values })); + }; + + return ( + +
+
+ +
+
+ + {intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })} + +
+ +
+
+ +
+
+ + } + > + + {intl.formatMessage({ id: 'common.title.config' })} + + + +
+
+
+ ); +}; + +export default AddInstanceTypeModal; diff --git a/src/pages/gpu-service/instance-types/components/flavor-list.tsx b/src/pages/gpu-service/instance-types/components/flavor-list.tsx new file mode 100644 index 00000000..80be522e --- /dev/null +++ b/src/pages/gpu-service/instance-types/components/flavor-list.tsx @@ -0,0 +1,129 @@ +import { FileSkeletonRows } from '@/pages/llmodels/components/model-source/file-skeleton'; +import { + AutoTooltip, + IconFont, + TemplateCard, + ThemeTag +} from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Empty, Flex, Spin, Typography } from 'antd'; +import _ from 'lodash'; +import { formatMemoryDisplay } from '../../instances/config'; +import { manufactureColorMap } from '../../templates/config'; +import { FlavorItem } from '../config/types'; +import styles from '../styles/instance-types.module.less'; + +const { Text } = Typography; + +interface FlavorListProps { + value?: string; + dataList: FlavorItem[]; + loading?: boolean; + onChange?: (item: FlavorItem) => void; +} + +const MetaItem: React.FC<{ + icon: string; + label: string; + value?: React.ReactNode; +}> = ({ icon, label, value }) => { + return ( + + + {label}: {value ?? '-'} + + ); +}; + +const FlavorList: React.FC = ({ + value, + dataList, + loading, + onChange +}) => { + const intl = useIntl(); + + if (!dataList.length) { + if (loading) { + return ( + + + {_.times(6, (index: number) => ( + + ))} + + + ); + } + return ; + } + + return ( + + {dataList.map((item) => { + const spec = item.spec || {}; + const manufacturer = spec.manufacturer || ''; + const color = manufactureColorMap[manufacturer] ?? 'purple'; + // A generic (no product, no/`generic` manufacturer) flavor is shown as + // "CPU-only" instead of falling back to the raw flavor name. + const isCpuOnly = + !spec.acceleratable && + !spec.product && + (!manufacturer || manufacturer.toLowerCase() === 'generic'); + const title = isCpuOnly ? 'CPU-only' : spec.product || item.name || '-'; + return ( + onChange?.(item)} + > + + +
+ + {title} + +
+ {manufacturer && ( + + {manufacturer.toUpperCase()} + + )} +
+ {/* Memory / sliceable only apply to accelerator (GPU) flavors; + a non-acceleratable (generic) flavor has neither. */} + {spec.acceleratable && ( + + + + + )} +
+
+ ); + })} +
+ ); +}; + +export default FlavorList; diff --git a/src/pages/gpu-service/instance-types/components/instance-type-card.tsx b/src/pages/gpu-service/instance-types/components/instance-type-card.tsx new file mode 100644 index 00000000..df83ef19 --- /dev/null +++ b/src/pages/gpu-service/instance-types/components/instance-type-card.tsx @@ -0,0 +1,147 @@ +import { + AutoTooltip, + DropdownActions, + IconFont, + StatusTag, + TemplateCard, + ThemeTag +} from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Button } from 'antd'; +import _ from 'lodash'; +import { formatMemoryDisplay } from '../../instances/config'; +import { manufactureColorMap } from '../../templates/config'; +import { ceilMilliToCore, parseQuantityToGi } from '../../utils'; +import { + InstanceTypePhaseLabelMap, + status as phaseStatusMap, + rowActionList +} from '../config'; +import { ListItem } from '../config/types'; +import styles from '../styles/instance-types.module.less'; + +interface InstanceTypeCardProps { + data: ListItem; + onDelete?: (record: ListItem) => void; +} + +const InstanceTypeCard: React.FC = ({ + data, + onDelete +}) => { + const intl = useIntl(); + const spec = data.spec || {}; + const unit = spec.unitResources || {}; + const phase = data.status?.phase || ''; + const manufacturer = spec.manufacturer || ''; + const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple'; + + const memoryText = formatMemoryDisplay(spec.memory ?? undefined); + + // Base resources, formatted into a single "·"-separated line. Falsy parts + // (e.g. a CPU-only type without VRAM) drop out rather than showing "-". + const cpuCores = ceilMilliToCore(unit.cpu ?? null)?.cores; + const ramGi = parseQuantityToGi(unit.ram ?? null)?.value; + const storageGi = parseQuantityToGi(spec.localStorage ?? null)?.value; + const osLabel = _.capitalize(spec.os || ''); + const archLabel = _.toUpper(spec.arch || ''); + const storageWord = intl.formatMessage({ + id: 'gpuservice.instanceType.localStorage' + }); + const footerParts = [ + cpuCores != null ? `${cpuCores} vCPU` : null, + ramGi != null ? `${ramGi} GiB RAM` : null, + storageGi != null ? `${storageGi} GiB ${storageWord}` : null, + osLabel ? `${osLabel}${archLabel ? ` (${archLabel})` : ''}` : null + ].filter(Boolean) as string[]; + + const handleAction = (item: any) => { + if (item.key === 'delete') { + onDelete?.(data); + } + }; + + return ( + + + + {spec.product || data.name || '-'} + + + + e.stopPropagation()}> + +