From 3fff1c10f257a328ca856b705da1c7d85a9ef2ca Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 13 Jul 2026 12:27:30 +0800 Subject: [PATCH] feat(gpu-service): instance-type activate/deactivate, displayName, unified flavor display - add activate/deactivate row actions (phase-driven) with new PUT APIs - add displayName field; list shows displayName || name - rename list OS column title to Platform - share a two-line flavor renderer between the create dropdown and the list - normalize 'CPU Only' label to 'CPU-only' --- src/locales/en-US/gpuservice.ts | 8 +- src/locales/ja-JP/gpuservice.ts | 8 +- src/locales/ru-RU/gpuservice.ts | 8 +- src/locales/tr-TR/gpuservice.ts | 8 +- src/locales/zh-CN/gpuservice.ts | 5 +- .../gpu-service/instance-types/apis/index.ts | 22 +++ .../components/flavor-display.tsx | 128 ++++++++++++++ .../instance-types/config/index.ts | 10 +- .../instance-types/config/types.ts | 2 + .../instance-types/forms/index.tsx | 158 +++++------------- .../hooks/use-instance-type-columns.tsx | 136 ++++++--------- .../gpu-service/instance-types/index.tsx | 26 ++- .../components/instance-type-item.tsx | 2 +- .../instances/utils/render-instance-type.tsx | 6 +- src/pages/usage/apis/resource.ts | 4 +- .../tables/use-instances-columns.tsx | 6 +- src/pages/usage/utils/format-instance-type.ts | 8 +- 17 files changed, 322 insertions(+), 223 deletions(-) create mode 100644 src/pages/gpu-service/instance-types/components/flavor-display.tsx diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index a872db0f..bdbfa9f5 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -134,12 +134,16 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'OS', 'gpuservice.instance.arch': 'Arch', -'gpuservice.instanceType': 'GPU Instance Type', + '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.flavor.required': + 'Please select an instance type flavor', 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', + 'gpuservice.instanceType.activate': 'Activate', + 'gpuservice.instanceType.deactivate': 'Deactivate', + 'gpuservice.instanceType.platform': 'Platform', '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 09d75d51..35dd6d5e 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -133,12 +133,16 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'OS', 'gpuservice.instance.arch': 'アーキテクチャ', -'gpuservice.instanceType': 'GPU Instance Type', + '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.flavor.required': + 'Please select an instance type flavor', 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', + 'gpuservice.instanceType.activate': 'Activate', + 'gpuservice.instanceType.deactivate': 'Deactivate', + 'gpuservice.instanceType.platform': 'Platform', '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 43c96d13..3600069d 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -132,12 +132,16 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'ОС', 'gpuservice.instance.arch': 'Архитектура', -'gpuservice.instanceType': 'GPU Instance Type', + '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.flavor.required': + 'Please select an instance type flavor', 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', + 'gpuservice.instanceType.activate': 'Activate', + 'gpuservice.instanceType.deactivate': 'Deactivate', + 'gpuservice.instanceType.platform': 'Platform', '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 68633202..56dc1911 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -128,12 +128,16 @@ export default { 'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.os': 'OS', 'gpuservice.instance.arch': 'Mimari', -'gpuservice.instanceType': 'GPU Instance Type', + '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.flavor.required': + 'Please select an instance type flavor', 'gpuservice.instanceType.flavor.gpuGroup': 'GPU Compute', 'gpuservice.instanceType.flavor.cpuGroup': 'CPU Compute', + 'gpuservice.instanceType.activate': 'Activate', + 'gpuservice.instanceType.deactivate': 'Deactivate', + 'gpuservice.instanceType.platform': 'Platform', '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 bc735d7e..56282404 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -121,12 +121,15 @@ export default { 'gpuservice.instance.ram': '内存', 'gpuservice.instance.os': '系统', 'gpuservice.instance.arch': '架构', -'gpuservice.instanceType': 'GPU 实例类型', + 'gpuservice.instanceType': 'GPU 实例类型', 'gpuservice.instanceType.add': '添加实例类型', 'gpuservice.instanceType.flavor': '规格', 'gpuservice.instanceType.flavor.required': '请选择实例类型规格', 'gpuservice.instanceType.flavor.gpuGroup': 'GPU 算力', 'gpuservice.instanceType.flavor.cpuGroup': 'CPU 算力', + 'gpuservice.instanceType.activate': '启用', + 'gpuservice.instanceType.deactivate': '停用', + 'gpuservice.instanceType.platform': '平台', 'gpuservice.instanceType.product': '商品', 'gpuservice.instanceType.unitCpu': '单位 CPU', 'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU', diff --git a/src/pages/gpu-service/instance-types/apis/index.ts b/src/pages/gpu-service/instance-types/apis/index.ts index 868a335d..10b413ef 100644 --- a/src/pages/gpu-service/instance-types/apis/index.ts +++ b/src/pages/gpu-service/instance-types/apis/index.ts @@ -52,3 +52,25 @@ export async function deleteGPUInstanceType(params: { params: { cluster_id: params.cluster_id } }); } + +// PUT /gpu-instance-types/{name}/enactive?cluster_id — activate an instance type. +export async function activateGPUInstanceType(params: { + name: string; + cluster_id: number; +}) { + return request(`${GPU_INSTANCE_TYPES_API}/${params.name}/activate`, { + method: 'PUT', + params: { cluster_id: params.cluster_id } + }); +} + +// PUT /gpu-instance-types/{name}/deactive?cluster_id — deactivate an instance type. +export async function deactivateGPUInstanceType(params: { + name: string; + cluster_id: number; +}) { + return request(`${GPU_INSTANCE_TYPES_API}/${params.name}/deactivate`, { + method: 'PUT', + params: { cluster_id: params.cluster_id } + }); +} diff --git a/src/pages/gpu-service/instance-types/components/flavor-display.tsx b/src/pages/gpu-service/instance-types/components/flavor-display.tsx new file mode 100644 index 00000000..0b27c814 --- /dev/null +++ b/src/pages/gpu-service/instance-types/components/flavor-display.tsx @@ -0,0 +1,128 @@ +import { AutoTooltip, ThemeTag } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Flex } from 'antd'; +import { formatMemoryDisplay } from '../../instances/config'; +import { manufactureColorMap } from '../../templates/config'; +import { formatManufacturer } from '../../utils'; + +// The subset of a flavor / instance-type spec the flavor display reads. Both +// FlavorItem.spec and InstanceTypeSpec structurally satisfy it, so the create +// drawer's dropdown and the management list share the same renderers. +interface FlavorSpecLike { + manufacturer?: string | null; + product?: string | null; + memory?: string | null; + sliceable?: boolean; + acceleratable?: boolean; +} + +// A flavor's title mirrors the flavor card: a generic (no product, no/`generic` +// manufacturer, non-acceleratable) flavor reads as "CPU-only". +export const getFlavorTitle = ( + spec: FlavorSpecLike = {}, + fallbackName?: string | null +) => { + const manufacturer = spec.manufacturer || ''; + const isCpuOnly = + !spec.acceleratable && + !spec.product && + (!manufacturer || manufacturer.toLowerCase() === 'generic'); + return isCpuOnly ? 'CPU-only' : spec.product || fallbackName || '-'; +}; + +// 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. +export const FlavorMeta: React.FC<{ spec?: FlavorSpecLike }> = ({ + spec = {} +}) => { + const intl = useIntl(); + 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 + ] + )} + + ); +}; + +// Two-line flavor display: title on top, meta row below. Shared by the create +// drawer's dropdown option and the management list's flavor cell. +export const FlavorOption: React.FC<{ + spec?: FlavorSpecLike; + fallbackName?: string | null; + maxWidth?: number | string; +}> = ({ spec = {}, fallbackName, maxWidth = '100%' }) => ( + + + {getFlavorTitle(spec, fallbackName)} + + + +); + +// Single-line flavor display: title then meta inline. Used for the collapsed +// selected value in the create drawer's Select. +export const FlavorSelected: React.FC<{ + spec?: FlavorSpecLike; + fallbackName?: string | null; +}> = ({ spec = {}, fallbackName }) => ( + + + {getFlavorTitle(spec, fallbackName)} + + + +); diff --git a/src/pages/gpu-service/instance-types/config/index.ts b/src/pages/gpu-service/instance-types/config/index.ts index 040d4c9e..8ccd9389 100644 --- a/src/pages/gpu-service/instance-types/config/index.ts +++ b/src/pages/gpu-service/instance-types/config/index.ts @@ -8,24 +8,24 @@ export const GPU_INSTANCE_TYPE_OS = 'linux'; export const InstanceTypePhaseValueMap = { Active: 'Active', Inactive: 'Inactive', - Preparing: 'Preparing' + Draining: 'Draining' }; export const InstanceTypePhaseLabelMap: Record = { [InstanceTypePhaseValueMap.Active]: 'Active', [InstanceTypePhaseValueMap.Inactive]: 'Inactive', - [InstanceTypePhaseValueMap.Preparing]: 'Preparing' + [InstanceTypePhaseValueMap.Draining]: 'Draining' }; export const status: Record = { [InstanceTypePhaseValueMap.Active]: StatusMaps.success, [InstanceTypePhaseValueMap.Inactive]: StatusMaps.inactive, - [InstanceTypePhaseValueMap.Preparing]: StatusMaps.transitioning + [InstanceTypePhaseValueMap.Draining]: StatusMaps.transitioning }; export const ArchOptions = [ - { label: 'amd64', value: 'amd64' }, - { label: 'arm64', value: 'arm64' } + { label: 'AMD64', value: 'amd64' }, + { label: 'ARM64', value: 'arm64' } ]; // ``icon`` is narrowed to ``any`` so the inferred type doesn't reach into diff --git a/src/pages/gpu-service/instance-types/config/types.ts b/src/pages/gpu-service/instance-types/config/types.ts index 3961046d..0db754b8 100644 --- a/src/pages/gpu-service/instance-types/config/types.ts +++ b/src/pages/gpu-service/instance-types/config/types.ts @@ -4,6 +4,7 @@ export interface UnitResources { } export interface InstanceTypeSpec { + displayName?: string | null; manufacturer?: string | null; product?: string | null; family?: string | null; @@ -54,6 +55,7 @@ export interface FlavorItem { export interface FormData { name: string; spec: { + displayName?: string | null; acceleratorGroup?: string | null; generalGroup?: string | null; acceleratable?: boolean; diff --git a/src/pages/gpu-service/instance-types/forms/index.tsx b/src/pages/gpu-service/instance-types/forms/index.tsx index 1e39a34f..16d5191f 100644 --- a/src/pages/gpu-service/instance-types/forms/index.tsx +++ b/src/pages/gpu-service/instance-types/forms/index.tsx @@ -1,18 +1,18 @@ 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 { Flex, Form } from 'antd'; +import { Form } from 'antd'; import { forwardRef, useEffect, useImperativeHandle } from 'react'; -import { formatMemoryDisplay } from '../../instances/config'; -import { manufactureColorMap } from '../../templates/config'; -import { formatManufacturer } from '../../utils'; +import { + FlavorOption, + FlavorSelected, + getFlavorTitle +} from '../components/flavor-display'; import { ArchOptions, GPU_INSTANCE_TYPE_OS } from '../config'; import { FlavorItem, FormData } from '../config/types'; import styles from '../styles/instance-types.module.less'; @@ -39,18 +39,6 @@ interface InstanceTypeFormProps { 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 { @@ -97,102 +85,19 @@ 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), + label: getFlavorTitle(flavor.spec, flavor.name), flavor }); - const cpuFlavors = flavorList.filter((flavor) => !flavor.spec?.acceleratable); - const gpuFlavors = flavorList.filter((flavor) => flavor.spec?.acceleratable); + const cpuFlavors = flavorList.filter( + (flavor) => !flavor.spec?.acceleratable + ); + const gpuFlavors = flavorList.filter( + (flavor) => flavor.spec?.acceleratable + ); const flavorOptions = [ cpuFlavors.length && { label: intl.formatMessage({ @@ -218,6 +123,7 @@ const GPUServiceInstanceTypeForm: React.FC = forwardRef( await onFinish({ name: values.name, spec: { + displayName: values.spec?.displayName?.trim() || null, acceleratorGroup: selectedFlavor?.spec?.acceleratorGroup ?? null, generalGroup: selectedFlavor?.spec?.generalGroup ?? null, acceleratable: selectedFlavor?.spec?.acceleratable ?? false, @@ -259,6 +165,25 @@ const GPUServiceInstanceTypeForm: React.FC = forwardRef( /> + + name={['spec', 'displayName']} + rules={[ + { + max: 63, + message: intl.formatMessage({ + id: 'gpuservice.template.displayName.max' + }) + } + ]} + > + + + = forwardRef( flavorList.find((flavor) => flavor.name === val) ?? null ) } - optionRender={(option: any) => - renderFlavorOption(option.data.flavor) - } + optionRender={(option: any) => { + const flavor: FlavorItem = option.data.flavor; + return ( + + ); + }} labelRender={({ value }) => { const flavor = flavorList.find((item) => item.name === value); - return flavor - ? renderFlavorSelected(flavor) - : ((value ?? '') as React.ReactNode); + return flavor ? ( + + ) : ( + ((value ?? '') as React.ReactNode) + ); }} /> diff --git a/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx b/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx index cc1342f2..e3fe7c0f 100644 --- a/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx +++ b/src/pages/gpu-service/instance-types/hooks/use-instance-type-columns.tsx @@ -3,22 +3,20 @@ import { AutoTooltip, DropdownButtons, icons, - StatusTag, - ThemeTag + StatusTag } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Flex, Space, Tooltip } from 'antd'; +import { Space, Tooltip } from 'antd'; import type { ColumnsType } from 'antd/lib/table'; import _ from 'lodash'; import { useMemo } from 'react'; -import { formatMemoryDisplay } from '../../instances/config'; -import { manufactureColorMap } from '../../templates/config'; +import { ceilMilliToCore, parseQuantityToGi } from '../../utils'; +import { FlavorOption } from '../components/flavor-display'; import { - ceilMilliToCore, - formatManufacturer, - parseQuantityToGi -} from '../../utils'; -import { InstanceTypePhaseLabelMap, status as phaseStatusMap } from '../config'; + InstanceTypePhaseLabelMap, + InstanceTypePhaseValueMap, + status as phaseStatusMap +} from '../config'; import { ListItem } from '../config/types'; interface ColumnsHookProps { @@ -26,16 +24,36 @@ interface ColumnsHookProps { } // DropdownButtons reads `locale` / `props` at runtime; its `items` prop is -// typed as antd's MenuProps['items'], so cast the config to satisfy it. -const rowActions = [ - { +// typed as antd's MenuProps['items'], so cast the config to satisfy it. The +// activate / deactivate action is chosen from the row's current phase: Active +// types can be deactivated, Inactive ones activated (none while Preparing). +const buildRowActions = (record: ListItem) => { + const phase = record.status?.phase; + const actions: any[] = []; + if (phase === InstanceTypePhaseValueMap.Active) { + actions.push({ + label: 'gpuservice.instanceType.deactivate', + key: 'deactivate', + locale: true, + icon: icons.Disabled + }); + } else if (phase === InstanceTypePhaseValueMap.Inactive) { + actions.push({ + label: 'gpuservice.instanceType.activate', + key: 'activate', + locale: true, + icon: icons.Charger + }); + } + actions.push({ label: 'common.button.delete', key: 'delete', locale: true, icon: icons.DeleteOutlined, props: { danger: true } - } -] as any; + }); + return actions; +}; // Column header with an info tooltip (used for the per-GPU resource columns). const TitleWithTip: React.FC<{ title: string; tip: string }> = ({ @@ -64,75 +82,31 @@ const useInstanceTypeColumns = ({ dataIndex: 'name', key: 'name', ellipsis: { showTitle: false }, - render: (text: string) => ( - - {text || '-'} - - ) + // Prefer the friendly display name, fall back to the resource name. + render: (text: string, record: ListItem) => { + const label = record.spec?.displayName || text; + return ( + + {label || '-'} + + ); + } }, { - title: intl.formatMessage({ id: 'gpuservice.instanceType.product' }), + // Flavor cell mirrors the create drawer's dropdown: product name on + // top, manufacturer · memory · sliceable on the meta line below. + title: intl.formatMessage({ id: 'gpuservice.instanceType.flavor' }), dataIndex: ['spec', 'product'], key: 'product', ellipsis: { showTitle: false }, - render: (text: string, record: ListItem) => ( - - - {text || '-'} - - {record.spec?.sliceable && ( - - {intl.formatMessage({ id: 'gpuservice.instance.sliceable' })} - - )} - + render: (_text: string, record: ListItem) => ( + ) }, - { - title: intl.formatMessage({ id: 'resources.table.vendor' }), - dataIndex: ['spec', 'manufacturer'], - key: 'manufacturer', - ellipsis: { showTitle: false }, - render: (value: string) => - value ? ( - - {formatManufacturer(value)} - - ) : ( - '-' - ) - }, - { - title: intl.formatMessage({ id: 'gpuservice.instance.memory' }), - dataIndex: ['spec', 'memory'], - key: 'memory', - ellipsis: { showTitle: false }, - // Non-acceleratable (generic) types have no VRAM concept → N/A. - render: (value: string, record: ListItem) => - record.spec?.acceleratable - ? formatMemoryDisplay(value ?? undefined) || '-' - : 'N/A' - }, - // { - // title: intl.formatMessage({ id: 'gpuservice.instance.sliceable' }), - // dataIndex: ['spec', 'sliceable'], - // key: 'sliceable', - // ellipsis: { showTitle: false }, - // render: (value: boolean) => - // value - // ? intl.formatMessage({ id: 'common.table.yes' }) - // : intl.formatMessage({ id: 'common.table.no' }) - // }, { title: ( { @@ -202,9 +176,9 @@ const useInstanceTypeColumns = ({ - {arch ? `${os} (${arch})` : os} + {arch ? `${os}/${arch}` : os} ); } @@ -234,7 +208,7 @@ const useInstanceTypeColumns = ({ ellipsis: { showTitle: false }, render: (_text, record: ListItem) => ( handleSelect(val, record)} /> ) diff --git a/src/pages/gpu-service/instance-types/index.tsx b/src/pages/gpu-service/instance-types/index.tsx index 3330ff37..8c0ecba9 100644 --- a/src/pages/gpu-service/instance-types/index.tsx +++ b/src/pages/gpu-service/instance-types/index.tsx @@ -12,7 +12,11 @@ import { useMemoizedFn } from 'ahooks'; import { ConfigProvider, Divider, Flex, Table, message } from 'antd'; import { useEffect, useMemo, useRef, useState } from 'react'; import PageBox, { HeaderLeft } from '../../_components/page-box'; -import { deleteGPUInstanceType } from './apis'; +import { + activateGPUInstanceType, + deactivateGPUInstanceType, + deleteGPUInstanceType +} from './apis'; import AddInstanceTypeModal from './components/add-instance-type-modal'; import { FormData, ListItem } from './config/types'; import useCreateInstanceTypeModal from './hooks/use-create-instance-type-modal'; @@ -120,9 +124,29 @@ const GPUServiceInstanceTypes: React.FC = () => { }); }); + const handleToggleActive = useMemoizedFn( + async (record: ListItem, activate: boolean) => { + if (clusterId == null) return; + try { + const action = activate + ? activateGPUInstanceType + : deactivateGPUInstanceType; + await action({ name: record.name, cluster_id: clusterId }); + message.success(intl.formatMessage({ id: 'common.message.success' })); + fetchInstanceTypes(clusterId); + } catch (error) { + // handled by the request interceptor + } + } + ); + const handleSelect = useMemoizedFn((val: string, record: ListItem) => { if (val === 'delete') { handleDelete(record); + } else if (val === 'activate') { + handleToggleActive(record, true); + } else if (val === 'deactivate') { + handleToggleActive(record, false); } }); diff --git a/src/pages/gpu-service/instances/components/instance-type-item.tsx b/src/pages/gpu-service/instances/components/instance-type-item.tsx index dd91b71d..23d8a471 100644 --- a/src/pages/gpu-service/instances/components/instance-type-item.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-item.tsx @@ -115,7 +115,7 @@ function getInstanceDerived(item: InstanceTypeItemModel) { acceleratable, isGPU: acceleratable, manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu', // GPU manufacturer or 'cpu' for non-acceleratable types - displayName: acceleratable ? spec.product || item.name : 'CPU Only', + displayName: acceleratable ? spec.product || item.name : 'CPU-only', ramUnit: spec.unitResourcesParsed?.ram?.value, os: _.capitalize(spec.os) || '', arch: spec.arch, diff --git a/src/pages/gpu-service/instances/utils/render-instance-type.tsx b/src/pages/gpu-service/instances/utils/render-instance-type.tsx index 533d9b16..eb170ce8 100644 --- a/src/pages/gpu-service/instances/utils/render-instance-type.tsx +++ b/src/pages/gpu-service/instances/utils/render-instance-type.tsx @@ -131,7 +131,7 @@ export const renderInstanceType = ( // Types breakdown only wants CPU + RAM, for example. categories?: SpecCategory[]; // Override the primary label (default: derived " x " / - // "CPU Only"). The Instance Types breakdown keeps its plain product name. + // "CPU-only"). The Instance Types breakdown keeps its plain product name. title?: string; } ) => { @@ -148,7 +148,7 @@ export const renderInstanceType = ( ? isSliced ? `${description.product} (${sliceMemoryPercentage}%)` : `${description.product} x ${accelerator}` - : 'CPU Only'); + : 'CPU-only'); const volume = (record.spec as any)?.volume; // Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring @@ -251,7 +251,7 @@ const mibToGiQuantity = (mib?: number): string | undefined => export interface InstanceTypeMiB { name?: string; product?: string; - // accelerator (GPU card) count; 0/undefined → CPU-only ("CPU Only"). + // accelerator (GPU card) count; 0/undefined → CPU-only. gpuCount?: number; // Per-card values. unitCpuMilli?: number; diff --git a/src/pages/usage/apis/resource.ts b/src/pages/usage/apis/resource.ts index f65e6213..412c0143 100644 --- a/src/pages/usage/apis/resource.ts +++ b/src/pages/usage/apis/resource.ts @@ -93,7 +93,7 @@ export interface ResourceBreakdownItem extends ResourceBreakdownSummary { unit_memory_mib?: number; vram_mib?: number; // Instance totals (requested cpu/ram) — the real size, so CPU instance types - // show "CPU Only · 2 vCPU · 4 GB" instead of just the per-unit spec. + // show "CPU-only · 2 vCPU · 4 GB" instead of just the per-unit spec. cpu_milli?: number; memory_mib?: number; // Per-instance rows also carry the card count + ephemeral disk so the @@ -382,7 +382,7 @@ function flattenItem( if (it.creator_deleted != null) flat.user_deleted = !!it.creator_deleted; // Instance-type grouped trend: the series label (``group``) defaults to the // raw flavor slug. Instance Types are grouped by actual shape, so label each - // series by that shape — " x " / "CPU Only · 3 vCPU · 6 GB" — + // series by that shape — " x " / "CPU-only · 3 vCPU · 6 GB" — // matching the table and keeping every shape a distinct series (#5700). // ``groupBy`` is the unmapped frontend dimension; the instance-type axis is // ``gpu_type`` (→ backend ``instance_type`` via GROUP_BY_MAP). diff --git a/src/pages/usage/instances-tab/tables/use-instances-columns.tsx b/src/pages/usage/instances-tab/tables/use-instances-columns.tsx index dc299db5..1b9a2be6 100644 --- a/src/pages/usage/instances-tab/tables/use-instances-columns.tsx +++ b/src/pages/usage/instances-tab/tables/use-instances-columns.tsx @@ -39,7 +39,7 @@ const useInstancesColumns = (groupKey: GroupKey) => { } ]; // Instance Types breakdown: the pretty product name (or flavor slug for - // older rows; "CPU Only" when no GPU cards) plus a CPU + RAM spec popover — + // older rows; "CPU-only" when no GPU cards) plus a CPU + RAM spec popover — // rendered through the canonical renderer so the formatting matches the GPU // Instances list, but limited to the CPU/RAM categories. const instanceTypeColType = { @@ -64,7 +64,7 @@ const useInstancesColumns = (groupKey: GroupKey) => { intl, categories: ['cpu', 'ram'], // Each row is one shape: GPU " x ", CPU - // "CPU Only · ". + // "CPU-only · ". title: instanceTypeSeriesLabel(row) } ); @@ -95,7 +95,7 @@ const useInstancesColumns = (groupKey: GroupKey) => { persistentMib: row.persistent_mib }), // Label by shape directly (consistent with the Instance Types - // column); avoids renderInstanceType's "CPU Only" fallback when a + // column); avoids renderInstanceType's "CPU-only" fallback when a // GPU row has vram but a missing/zero gpu_count. { intl, title: instanceTypeSeriesLabel(row) } ); diff --git a/src/pages/usage/utils/format-instance-type.ts b/src/pages/usage/utils/format-instance-type.ts index 6a55d99f..f0477ef3 100644 --- a/src/pages/usage/utils/format-instance-type.ts +++ b/src/pages/usage/utils/format-instance-type.ts @@ -32,16 +32,16 @@ export const formatCpuSpec = ( return parts.join(' · '); }; -// CPU instance-type label: "CPU Only" plus its real size when known, e.g. -// "CPU Only · 2 vCPU · 4 GB". Used by both the table column and the trend +// CPU instance-type label: "CPU-only" plus its real size when known, e.g. +// "CPU-only · 2 vCPU · 4 GB". Used by both the table column and the trend // legend so they read identically. export const cpuOnlyLabel = (row?: Partial): string => { const spec = formatCpuSpec(row?.cpu_milli, row?.memory_mib); - return spec ? `CPU Only · ${spec}` : 'CPU Only'; + return spec ? `CPU-only · ${spec}` : 'CPU-only'; }; // Instance Types are grouped by actual shape, so each row is one concrete -// type: a GPU shows " x ", a CPU shows "CPU Only · ". +// type: a GPU shows " x ", a CPU shows "CPU-only · ". // One label for the table column and the trend legend so they read the same // and each shape is a distinct series. (" x " matches the GPU Instances list.) export const instanceTypeSeriesLabel = (