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'
This commit is contained in:
jialin
2026-07-22 15:52:17 +08:00
parent 2763594ae3
commit 3fff1c10f2
17 changed files with 322 additions and 223 deletions
+6 -2
View File
@@ -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',
+6 -2
View File
@@ -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',
+6 -2
View File
@@ -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',
+6 -2
View File
@@ -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',
+4 -1
View File
@@ -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',
@@ -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 }
});
}
@@ -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(
<ThemeTag
key="vendor"
color={color}
style={{ fontWeight: 400, marginInlineEnd: 0 }}
>
{formatManufacturer(manufacturer)}
</ThemeTag>
);
}
if (memory) {
pieces.push(<span key="memory">{memory}</span>);
}
if (spec.acceleratable && spec.sliceable) {
pieces.push(
<ThemeTag
key="sliceable"
color="geekblue"
style={{ fontWeight: 400, marginInlineEnd: 0 }}
>
{intl.formatMessage({ id: 'gpuservice.instance.sliceable' })}
</ThemeTag>
);
}
if (!pieces.length) return null;
return (
<Flex
align="center"
gap={8}
style={{
minWidth: 0,
color: 'var(--ant-color-text-tertiary)',
fontSize: 12
}}
>
{pieces.flatMap((piece, index) =>
index === 0
? [piece]
: [
<span
key={`dot-${index}`}
style={{ color: 'var(--ant-color-text-quaternary)' }}
>
·
</span>,
piece
]
)}
</Flex>
);
};
// 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%' }) => (
<Flex vertical gap={4} style={{ minWidth: 0, padding: '2px 0' }}>
<AutoTooltip ghost minWidth={20} maxWidth={maxWidth}>
{getFlavorTitle(spec, fallbackName)}
</AutoTooltip>
<FlavorMeta spec={spec} />
</Flex>
);
// 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 }) => (
<Flex align="center" gap={8} style={{ minWidth: 0 }}>
<AutoTooltip ghost minWidth={20} maxWidth={200}>
{getFlavorTitle(spec, fallbackName)}
</AutoTooltip>
<FlavorMeta spec={spec} />
</Flex>
);
@@ -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<string, string> = {
[InstanceTypePhaseValueMap.Active]: 'Active',
[InstanceTypePhaseValueMap.Inactive]: 'Inactive',
[InstanceTypePhaseValueMap.Preparing]: 'Preparing'
[InstanceTypePhaseValueMap.Draining]: 'Draining'
};
export const status: Record<string, StatusType> = {
[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
@@ -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;
@@ -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<InstanceTypeFormProps> = forwardRef(
(props, ref) => {
const {
@@ -97,102 +85,19 @@ const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = 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(
<ThemeTag
key="vendor"
color={color}
style={{ fontWeight: 400, marginInlineEnd: 0 }}
>
{formatManufacturer(manufacturer)}
</ThemeTag>
);
}
if (memory) {
pieces.push(<span key="memory">{memory}</span>);
}
if (spec.acceleratable && spec.sliceable) {
pieces.push(
<ThemeTag
key="sliceable"
color="geekblue"
style={{ fontWeight: 400, marginInlineEnd: 0 }}
>
{intl.formatMessage({ id: 'gpuservice.instance.sliceable' })}
</ThemeTag>
);
}
if (!pieces.length) return null;
return (
<Flex
align="center"
gap={8}
style={{
minWidth: 0,
color: 'var(--ant-color-text-tertiary)',
fontSize: 12
}}
>
{pieces.flatMap((piece, index) =>
index === 0
? [piece]
: [
<span
key={`dot-${index}`}
style={{ color: 'var(--ant-color-text-quaternary)' }}
>
·
</span>,
piece
]
)}
</Flex>
);
};
// Dropdown item: two lines — name on top, meta row below.
const renderFlavorOption = (flavor: FlavorItem) => (
<Flex vertical gap={4} style={{ minWidth: 0, padding: '2px 0' }}>
<AutoTooltip ghost minWidth={20} maxWidth={'100%'}>
{getFlavorTitle(flavor)}
</AutoTooltip>
{renderFlavorMeta(flavor)}
</Flex>
);
// Collapsed selected value: single line — name then meta inline.
const renderFlavorSelected = (flavor: FlavorItem) => (
<Flex align="center" gap={8} style={{ minWidth: 0 }}>
<AutoTooltip ghost minWidth={20} maxWidth={200}>
{getFlavorTitle(flavor)}
</AutoTooltip>
{renderFlavorMeta(flavor)}
</Flex>
);
// 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<InstanceTypeFormProps> = 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<InstanceTypeFormProps> = forwardRef(
/>
</Form.Item>
<Form.Item<FormData>
name={['spec', 'displayName']}
rules={[
{
max: 63,
message: intl.formatMessage({
id: 'gpuservice.template.displayName.max'
})
}
]}
>
<CInput.Input
trim={false}
label={intl.formatMessage({
id: 'gpuservice.template.displayName'
})}
/>
</Form.Item>
<Form.Item>
<SealSelect
label={intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })}
@@ -274,14 +199,19 @@ const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = 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 (
<FlavorOption spec={flavor.spec} fallbackName={flavor.name} />
);
}}
labelRender={({ value }) => {
const flavor = flavorList.find((item) => item.name === value);
return flavor
? renderFlavorSelected(flavor)
: ((value ?? '') as React.ReactNode);
return flavor ? (
<FlavorSelected spec={flavor.spec} fallbackName={flavor.name} />
) : (
((value ?? '') as React.ReactNode)
);
}}
/>
</Form.Item>
@@ -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) => (
<AutoTooltip ghost minWidth={20} maxWidth={200} title={text}>
<span className="text-primary">{text || '-'}</span>
</AutoTooltip>
)
// Prefer the friendly display name, fall back to the resource name.
render: (text: string, record: ListItem) => {
const label = record.spec?.displayName || text;
return (
<AutoTooltip ghost minWidth={20} maxWidth={200} title={label}>
<span className="text-primary">{label || '-'}</span>
</AutoTooltip>
);
}
},
{
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) => (
<Flex
gap={4}
align="center"
style={{ maxWidth: '100%', minWidth: 0 }}
>
<AutoTooltip ghost minWidth={20} maxWidth={200}>
{text || '-'}
</AutoTooltip>
{record.spec?.sliceable && (
<ThemeTag
color="geekblue"
style={{ fontWeight: 400, flexShrink: 0 }}
>
{intl.formatMessage({ id: 'gpuservice.instance.sliceable' })}
</ThemeTag>
)}
</Flex>
render: (_text: string, record: ListItem) => (
<FlavorOption
spec={record.spec}
fallbackName={record.name}
maxWidth={200}
/>
)
},
{
title: intl.formatMessage({ id: 'resources.table.vendor' }),
dataIndex: ['spec', 'manufacturer'],
key: 'manufacturer',
ellipsis: { showTitle: false },
render: (value: string) =>
value ? (
<ThemeTag
color={manufactureColorMap[value] ?? 'purple'}
style={{ fontWeight: 400, width: 'fit-content' }}
>
{formatManufacturer(value)}
</ThemeTag>
) : (
'-'
)
},
{
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: (
<TitleWithTip
@@ -191,7 +165,7 @@ const useInstanceTypeColumns = ({
}
},
{
title: intl.formatMessage({ id: 'gpuservice.instance.os' }),
title: intl.formatMessage({ id: 'gpuservice.instanceType.platform' }),
key: 'os',
ellipsis: { showTitle: false },
render: (_text, record: ListItem) => {
@@ -202,9 +176,9 @@ const useInstanceTypeColumns = ({
<AutoTooltip
ghost
maxWidth={240}
title={arch ? `${os} (${arch})` : os}
title={arch ? `${os}/${arch}` : os}
>
{arch ? `${os} (${arch})` : os}
{arch ? `${os}/${arch}` : os}
</AutoTooltip>
);
}
@@ -234,7 +208,7 @@ const useInstanceTypeColumns = ({
ellipsis: { showTitle: false },
render: (_text, record: ListItem) => (
<DropdownButtons
items={rowActions}
items={buildRowActions(record)}
onSelect={(val: string) => handleSelect(val, record)}
/>
)
+25 -1
View File
@@ -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);
}
});
@@ -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,
@@ -131,7 +131,7 @@ export const renderInstanceType = (
// Types breakdown only wants CPU + RAM, for example.
categories?: SpecCategory[];
// Override the primary label (default: derived "<product> x <count>" /
// "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;
+2 -2
View File
@@ -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 — "<product> x <cards>" / "CPU Only · 3 vCPU · 6 GB" —
// series by that shape — "<product> x <cards>" / "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).
@@ -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 "<product> x <cards>", CPU
// "CPU Only · <spec>".
// "CPU-only · <spec>".
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) }
);
@@ -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<ResourceBreakdownItem>): 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 "<product> x <cards>", a CPU shows "CPU Only · <spec>".
// type: a GPU shows "<product> x <cards>", a CPU shows "CPU-only · <spec>".
// 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 = (