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:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user