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.ram': 'RAM',
|
||||||
'gpuservice.instance.os': 'OS',
|
'gpuservice.instance.os': 'OS',
|
||||||
'gpuservice.instance.arch': 'Arch',
|
'gpuservice.instance.arch': 'Arch',
|
||||||
'gpuservice.instanceType': 'GPU Instance Type',
|
'gpuservice.instanceType': 'GPU Instance Type',
|
||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'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.gpuGroup': 'GPU Compute',
|
||||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 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.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -133,12 +133,16 @@ export default {
|
|||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.os': 'OS',
|
'gpuservice.instance.os': 'OS',
|
||||||
'gpuservice.instance.arch': 'アーキテクチャ',
|
'gpuservice.instance.arch': 'アーキテクチャ',
|
||||||
'gpuservice.instanceType': 'GPU Instance Type',
|
'gpuservice.instanceType': 'GPU Instance Type',
|
||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'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.gpuGroup': 'GPU Compute',
|
||||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 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.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -132,12 +132,16 @@ export default {
|
|||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.os': 'ОС',
|
'gpuservice.instance.os': 'ОС',
|
||||||
'gpuservice.instance.arch': 'Архитектура',
|
'gpuservice.instance.arch': 'Архитектура',
|
||||||
'gpuservice.instanceType': 'GPU Instance Type',
|
'gpuservice.instanceType': 'GPU Instance Type',
|
||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'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.gpuGroup': 'GPU Compute',
|
||||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 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.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -128,12 +128,16 @@ export default {
|
|||||||
'gpuservice.instance.ram': 'RAM',
|
'gpuservice.instance.ram': 'RAM',
|
||||||
'gpuservice.instance.os': 'OS',
|
'gpuservice.instance.os': 'OS',
|
||||||
'gpuservice.instance.arch': 'Mimari',
|
'gpuservice.instance.arch': 'Mimari',
|
||||||
'gpuservice.instanceType': 'GPU Instance Type',
|
'gpuservice.instanceType': 'GPU Instance Type',
|
||||||
'gpuservice.instanceType.add': 'Add Instance Type',
|
'gpuservice.instanceType.add': 'Add Instance Type',
|
||||||
'gpuservice.instanceType.flavor': 'Flavor',
|
'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.gpuGroup': 'GPU Compute',
|
||||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 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.product': 'Product',
|
||||||
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
'gpuservice.instanceType.unitCpu': 'Unit CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
'gpuservice.instanceType.unitCpu.tip': 'CPU allocated per GPU',
|
||||||
|
|||||||
@@ -121,12 +121,15 @@ export default {
|
|||||||
'gpuservice.instance.ram': '内存',
|
'gpuservice.instance.ram': '内存',
|
||||||
'gpuservice.instance.os': '系统',
|
'gpuservice.instance.os': '系统',
|
||||||
'gpuservice.instance.arch': '架构',
|
'gpuservice.instance.arch': '架构',
|
||||||
'gpuservice.instanceType': 'GPU 实例类型',
|
'gpuservice.instanceType': 'GPU 实例类型',
|
||||||
'gpuservice.instanceType.add': '添加实例类型',
|
'gpuservice.instanceType.add': '添加实例类型',
|
||||||
'gpuservice.instanceType.flavor': '规格',
|
'gpuservice.instanceType.flavor': '规格',
|
||||||
'gpuservice.instanceType.flavor.required': '请选择实例类型规格',
|
'gpuservice.instanceType.flavor.required': '请选择实例类型规格',
|
||||||
'gpuservice.instanceType.flavor.gpuGroup': 'GPU 算力',
|
'gpuservice.instanceType.flavor.gpuGroup': 'GPU 算力',
|
||||||
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 算力',
|
'gpuservice.instanceType.flavor.cpuGroup': 'CPU 算力',
|
||||||
|
'gpuservice.instanceType.activate': '启用',
|
||||||
|
'gpuservice.instanceType.deactivate': '停用',
|
||||||
|
'gpuservice.instanceType.platform': '平台',
|
||||||
'gpuservice.instanceType.product': '商品',
|
'gpuservice.instanceType.product': '商品',
|
||||||
'gpuservice.instanceType.unitCpu': '单位 CPU',
|
'gpuservice.instanceType.unitCpu': '单位 CPU',
|
||||||
'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU',
|
'gpuservice.instanceType.unitCpu.tip': '每 GPU 对应多少 CPU',
|
||||||
|
|||||||
@@ -52,3 +52,25 @@ export async function deleteGPUInstanceType(params: {
|
|||||||
params: { cluster_id: params.cluster_id }
|
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 = {
|
export const InstanceTypePhaseValueMap = {
|
||||||
Active: 'Active',
|
Active: 'Active',
|
||||||
Inactive: 'Inactive',
|
Inactive: 'Inactive',
|
||||||
Preparing: 'Preparing'
|
Draining: 'Draining'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstanceTypePhaseLabelMap: Record<string, string> = {
|
export const InstanceTypePhaseLabelMap: Record<string, string> = {
|
||||||
[InstanceTypePhaseValueMap.Active]: 'Active',
|
[InstanceTypePhaseValueMap.Active]: 'Active',
|
||||||
[InstanceTypePhaseValueMap.Inactive]: 'Inactive',
|
[InstanceTypePhaseValueMap.Inactive]: 'Inactive',
|
||||||
[InstanceTypePhaseValueMap.Preparing]: 'Preparing'
|
[InstanceTypePhaseValueMap.Draining]: 'Draining'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const status: Record<string, StatusType> = {
|
export const status: Record<string, StatusType> = {
|
||||||
[InstanceTypePhaseValueMap.Active]: StatusMaps.success,
|
[InstanceTypePhaseValueMap.Active]: StatusMaps.success,
|
||||||
[InstanceTypePhaseValueMap.Inactive]: StatusMaps.inactive,
|
[InstanceTypePhaseValueMap.Inactive]: StatusMaps.inactive,
|
||||||
[InstanceTypePhaseValueMap.Preparing]: StatusMaps.transitioning
|
[InstanceTypePhaseValueMap.Draining]: StatusMaps.transitioning
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ArchOptions = [
|
export const ArchOptions = [
|
||||||
{ label: 'amd64', value: 'amd64' },
|
{ label: 'AMD64', value: 'amd64' },
|
||||||
{ label: 'arm64', value: 'arm64' }
|
{ label: 'ARM64', value: 'arm64' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// ``icon`` is narrowed to ``any`` so the inferred type doesn't reach into
|
// ``icon`` is narrowed to ``any`` so the inferred type doesn't reach into
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface UnitResources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface InstanceTypeSpec {
|
export interface InstanceTypeSpec {
|
||||||
|
displayName?: string | null;
|
||||||
manufacturer?: string | null;
|
manufacturer?: string | null;
|
||||||
product?: string | null;
|
product?: string | null;
|
||||||
family?: string | null;
|
family?: string | null;
|
||||||
@@ -54,6 +55,7 @@ export interface FlavorItem {
|
|||||||
export interface FormData {
|
export interface FormData {
|
||||||
name: string;
|
name: string;
|
||||||
spec: {
|
spec: {
|
||||||
|
displayName?: string | null;
|
||||||
acceleratorGroup?: string | null;
|
acceleratorGroup?: string | null;
|
||||||
generalGroup?: string | null;
|
generalGroup?: string | null;
|
||||||
acceleratable?: boolean;
|
acceleratable?: boolean;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { validateLabelNameRegxFor63 } from '@/config';
|
import { validateLabelNameRegxFor63 } from '@/config';
|
||||||
import {
|
import {
|
||||||
AutoTooltip,
|
|
||||||
Input as CInput,
|
Input as CInput,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select as SealSelect,
|
Select as SealSelect,
|
||||||
ThemeTag,
|
|
||||||
useAppUtils
|
useAppUtils
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Flex, Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||||
import { formatMemoryDisplay } from '../../instances/config';
|
import {
|
||||||
import { manufactureColorMap } from '../../templates/config';
|
FlavorOption,
|
||||||
import { formatManufacturer } from '../../utils';
|
FlavorSelected,
|
||||||
|
getFlavorTitle
|
||||||
|
} from '../components/flavor-display';
|
||||||
import { ArchOptions, GPU_INSTANCE_TYPE_OS } from '../config';
|
import { ArchOptions, GPU_INSTANCE_TYPE_OS } from '../config';
|
||||||
import { FlavorItem, FormData } from '../config/types';
|
import { FlavorItem, FormData } from '../config/types';
|
||||||
import styles from '../styles/instance-types.module.less';
|
import styles from '../styles/instance-types.module.less';
|
||||||
@@ -39,18 +39,6 @@ interface InstanceTypeFormProps {
|
|||||||
onFinishFailed?: (errorInfo: any) => void;
|
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(
|
const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
const {
|
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
|
// Split flavors into two groups: CPU compute (generic) and GPU compute
|
||||||
// (accelerator). Groups render as labeled sections in the dropdown.
|
// (accelerator). Groups render as labeled sections in the dropdown.
|
||||||
const toFlavorOption = (flavor: FlavorItem) => ({
|
const toFlavorOption = (flavor: FlavorItem) => ({
|
||||||
value: flavor.name,
|
value: flavor.name,
|
||||||
label: getFlavorTitle(flavor),
|
label: getFlavorTitle(flavor.spec, flavor.name),
|
||||||
flavor
|
flavor
|
||||||
});
|
});
|
||||||
const cpuFlavors = flavorList.filter((flavor) => !flavor.spec?.acceleratable);
|
const cpuFlavors = flavorList.filter(
|
||||||
const gpuFlavors = flavorList.filter((flavor) => flavor.spec?.acceleratable);
|
(flavor) => !flavor.spec?.acceleratable
|
||||||
|
);
|
||||||
|
const gpuFlavors = flavorList.filter(
|
||||||
|
(flavor) => flavor.spec?.acceleratable
|
||||||
|
);
|
||||||
const flavorOptions = [
|
const flavorOptions = [
|
||||||
cpuFlavors.length && {
|
cpuFlavors.length && {
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
@@ -218,6 +123,7 @@ const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
|||||||
await onFinish({
|
await onFinish({
|
||||||
name: values.name,
|
name: values.name,
|
||||||
spec: {
|
spec: {
|
||||||
|
displayName: values.spec?.displayName?.trim() || null,
|
||||||
acceleratorGroup: selectedFlavor?.spec?.acceleratorGroup ?? null,
|
acceleratorGroup: selectedFlavor?.spec?.acceleratorGroup ?? null,
|
||||||
generalGroup: selectedFlavor?.spec?.generalGroup ?? null,
|
generalGroup: selectedFlavor?.spec?.generalGroup ?? null,
|
||||||
acceleratable: selectedFlavor?.spec?.acceleratable ?? false,
|
acceleratable: selectedFlavor?.spec?.acceleratable ?? false,
|
||||||
@@ -259,6 +165,25 @@ const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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>
|
<Form.Item>
|
||||||
<SealSelect
|
<SealSelect
|
||||||
label={intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })}
|
label={intl.formatMessage({ id: 'gpuservice.instanceType.flavor' })}
|
||||||
@@ -274,14 +199,19 @@ const GPUServiceInstanceTypeForm: React.FC<InstanceTypeFormProps> = forwardRef(
|
|||||||
flavorList.find((flavor) => flavor.name === val) ?? null
|
flavorList.find((flavor) => flavor.name === val) ?? null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
optionRender={(option: any) =>
|
optionRender={(option: any) => {
|
||||||
renderFlavorOption(option.data.flavor)
|
const flavor: FlavorItem = option.data.flavor;
|
||||||
}
|
return (
|
||||||
|
<FlavorOption spec={flavor.spec} fallbackName={flavor.name} />
|
||||||
|
);
|
||||||
|
}}
|
||||||
labelRender={({ value }) => {
|
labelRender={({ value }) => {
|
||||||
const flavor = flavorList.find((item) => item.name === value);
|
const flavor = flavorList.find((item) => item.name === value);
|
||||||
return flavor
|
return flavor ? (
|
||||||
? renderFlavorSelected(flavor)
|
<FlavorSelected spec={flavor.spec} fallbackName={flavor.name} />
|
||||||
: ((value ?? '') as React.ReactNode);
|
) : (
|
||||||
|
((value ?? '') as React.ReactNode)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -3,22 +3,20 @@ import {
|
|||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
DropdownButtons,
|
DropdownButtons,
|
||||||
icons,
|
icons,
|
||||||
StatusTag,
|
StatusTag
|
||||||
ThemeTag
|
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Flex, Space, Tooltip } from 'antd';
|
import { Space, Tooltip } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/lib/table';
|
import type { ColumnsType } from 'antd/lib/table';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { formatMemoryDisplay } from '../../instances/config';
|
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
|
||||||
import { manufactureColorMap } from '../../templates/config';
|
import { FlavorOption } from '../components/flavor-display';
|
||||||
import {
|
import {
|
||||||
ceilMilliToCore,
|
InstanceTypePhaseLabelMap,
|
||||||
formatManufacturer,
|
InstanceTypePhaseValueMap,
|
||||||
parseQuantityToGi
|
status as phaseStatusMap
|
||||||
} from '../../utils';
|
} from '../config';
|
||||||
import { InstanceTypePhaseLabelMap, status as phaseStatusMap } from '../config';
|
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
interface ColumnsHookProps {
|
interface ColumnsHookProps {
|
||||||
@@ -26,16 +24,36 @@ interface ColumnsHookProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DropdownButtons reads `locale` / `props` at runtime; its `items` prop is
|
// DropdownButtons reads `locale` / `props` at runtime; its `items` prop is
|
||||||
// typed as antd's MenuProps['items'], so cast the config to satisfy it.
|
// typed as antd's MenuProps['items'], so cast the config to satisfy it. The
|
||||||
const rowActions = [
|
// 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',
|
label: 'common.button.delete',
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
locale: true,
|
locale: true,
|
||||||
icon: icons.DeleteOutlined,
|
icon: icons.DeleteOutlined,
|
||||||
props: { danger: true }
|
props: { danger: true }
|
||||||
}
|
});
|
||||||
] as any;
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
// Column header with an info tooltip (used for the per-GPU resource columns).
|
// Column header with an info tooltip (used for the per-GPU resource columns).
|
||||||
const TitleWithTip: React.FC<{ title: string; tip: string }> = ({
|
const TitleWithTip: React.FC<{ title: string; tip: string }> = ({
|
||||||
@@ -64,75 +82,31 @@ const useInstanceTypeColumns = ({
|
|||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
ellipsis: { showTitle: false },
|
ellipsis: { showTitle: false },
|
||||||
render: (text: string) => (
|
// Prefer the friendly display name, fall back to the resource name.
|
||||||
<AutoTooltip ghost minWidth={20} maxWidth={200} title={text}>
|
render: (text: string, record: ListItem) => {
|
||||||
<span className="text-primary">{text || '-'}</span>
|
const label = record.spec?.displayName || text;
|
||||||
</AutoTooltip>
|
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'],
|
dataIndex: ['spec', 'product'],
|
||||||
key: 'product',
|
key: 'product',
|
||||||
ellipsis: { showTitle: false },
|
ellipsis: { showTitle: false },
|
||||||
render: (text: string, record: ListItem) => (
|
render: (_text: string, record: ListItem) => (
|
||||||
<Flex
|
<FlavorOption
|
||||||
gap={4}
|
spec={record.spec}
|
||||||
align="center"
|
fallbackName={record.name}
|
||||||
style={{ maxWidth: '100%', minWidth: 0 }}
|
maxWidth={200}
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
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: (
|
title: (
|
||||||
<TitleWithTip
|
<TitleWithTip
|
||||||
@@ -191,7 +165,7 @@ const useInstanceTypeColumns = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'gpuservice.instance.os' }),
|
title: intl.formatMessage({ id: 'gpuservice.instanceType.platform' }),
|
||||||
key: 'os',
|
key: 'os',
|
||||||
ellipsis: { showTitle: false },
|
ellipsis: { showTitle: false },
|
||||||
render: (_text, record: ListItem) => {
|
render: (_text, record: ListItem) => {
|
||||||
@@ -202,9 +176,9 @@ const useInstanceTypeColumns = ({
|
|||||||
<AutoTooltip
|
<AutoTooltip
|
||||||
ghost
|
ghost
|
||||||
maxWidth={240}
|
maxWidth={240}
|
||||||
title={arch ? `${os} (${arch})` : os}
|
title={arch ? `${os}/${arch}` : os}
|
||||||
>
|
>
|
||||||
{arch ? `${os} (${arch})` : os}
|
{arch ? `${os}/${arch}` : os}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -234,7 +208,7 @@ const useInstanceTypeColumns = ({
|
|||||||
ellipsis: { showTitle: false },
|
ellipsis: { showTitle: false },
|
||||||
render: (_text, record: ListItem) => (
|
render: (_text, record: ListItem) => (
|
||||||
<DropdownButtons
|
<DropdownButtons
|
||||||
items={rowActions}
|
items={buildRowActions(record)}
|
||||||
onSelect={(val: string) => handleSelect(val, record)}
|
onSelect={(val: string) => handleSelect(val, record)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import { useMemoizedFn } from 'ahooks';
|
|||||||
import { ConfigProvider, Divider, Flex, Table, message } from 'antd';
|
import { ConfigProvider, Divider, Flex, Table, message } from 'antd';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import PageBox, { HeaderLeft } from '../../_components/page-box';
|
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 AddInstanceTypeModal from './components/add-instance-type-modal';
|
||||||
import { FormData, ListItem } from './config/types';
|
import { FormData, ListItem } from './config/types';
|
||||||
import useCreateInstanceTypeModal from './hooks/use-create-instance-type-modal';
|
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) => {
|
const handleSelect = useMemoizedFn((val: string, record: ListItem) => {
|
||||||
if (val === 'delete') {
|
if (val === 'delete') {
|
||||||
handleDelete(record);
|
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,
|
acceleratable,
|
||||||
isGPU: acceleratable,
|
isGPU: acceleratable,
|
||||||
manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu', // GPU manufacturer or 'cpu' for non-acceleratable types
|
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,
|
ramUnit: spec.unitResourcesParsed?.ram?.value,
|
||||||
os: _.capitalize(spec.os) || '',
|
os: _.capitalize(spec.os) || '',
|
||||||
arch: spec.arch,
|
arch: spec.arch,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const renderInstanceType = (
|
|||||||
// Types breakdown only wants CPU + RAM, for example.
|
// Types breakdown only wants CPU + RAM, for example.
|
||||||
categories?: SpecCategory[];
|
categories?: SpecCategory[];
|
||||||
// Override the primary label (default: derived "<product> x <count>" /
|
// 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;
|
title?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
@@ -148,7 +148,7 @@ export const renderInstanceType = (
|
|||||||
? isSliced
|
? isSliced
|
||||||
? `${description.product} (${sliceMemoryPercentage}%)`
|
? `${description.product} (${sliceMemoryPercentage}%)`
|
||||||
: `${description.product} x ${accelerator}`
|
: `${description.product} x ${accelerator}`
|
||||||
: 'CPU Only');
|
: 'CPU-only');
|
||||||
|
|
||||||
const volume = (record.spec as any)?.volume;
|
const volume = (record.spec as any)?.volume;
|
||||||
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
|
// Spec popover grouped by category (GPU / CPU / Memory / Disk), mirroring
|
||||||
@@ -251,7 +251,7 @@ const mibToGiQuantity = (mib?: number): string | undefined =>
|
|||||||
export interface InstanceTypeMiB {
|
export interface InstanceTypeMiB {
|
||||||
name?: string;
|
name?: string;
|
||||||
product?: string;
|
product?: string;
|
||||||
// accelerator (GPU card) count; 0/undefined → CPU-only ("CPU Only").
|
// accelerator (GPU card) count; 0/undefined → CPU-only.
|
||||||
gpuCount?: number;
|
gpuCount?: number;
|
||||||
// Per-card values.
|
// Per-card values.
|
||||||
unitCpuMilli?: number;
|
unitCpuMilli?: number;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export interface ResourceBreakdownItem extends ResourceBreakdownSummary {
|
|||||||
unit_memory_mib?: number;
|
unit_memory_mib?: number;
|
||||||
vram_mib?: number;
|
vram_mib?: number;
|
||||||
// Instance totals (requested cpu/ram) — the real size, so CPU instance types
|
// 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;
|
cpu_milli?: number;
|
||||||
memory_mib?: number;
|
memory_mib?: number;
|
||||||
// Per-instance rows also carry the card count + ephemeral disk so the
|
// 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;
|
if (it.creator_deleted != null) flat.user_deleted = !!it.creator_deleted;
|
||||||
// Instance-type grouped trend: the series label (``group``) defaults to the
|
// Instance-type grouped trend: the series label (``group``) defaults to the
|
||||||
// raw flavor slug. Instance Types are grouped by actual shape, so label each
|
// 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).
|
// matching the table and keeping every shape a distinct series (#5700).
|
||||||
// ``groupBy`` is the unmapped frontend dimension; the instance-type axis is
|
// ``groupBy`` is the unmapped frontend dimension; the instance-type axis is
|
||||||
// ``gpu_type`` (→ backend ``instance_type`` via GROUP_BY_MAP).
|
// ``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
|
// 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
|
// rendered through the canonical renderer so the formatting matches the GPU
|
||||||
// Instances list, but limited to the CPU/RAM categories.
|
// Instances list, but limited to the CPU/RAM categories.
|
||||||
const instanceTypeColType = {
|
const instanceTypeColType = {
|
||||||
@@ -64,7 +64,7 @@ const useInstancesColumns = (groupKey: GroupKey) => {
|
|||||||
intl,
|
intl,
|
||||||
categories: ['cpu', 'ram'],
|
categories: ['cpu', 'ram'],
|
||||||
// Each row is one shape: GPU "<product> x <cards>", CPU
|
// Each row is one shape: GPU "<product> x <cards>", CPU
|
||||||
// "CPU Only · <spec>".
|
// "CPU-only · <spec>".
|
||||||
title: instanceTypeSeriesLabel(row)
|
title: instanceTypeSeriesLabel(row)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -95,7 +95,7 @@ const useInstancesColumns = (groupKey: GroupKey) => {
|
|||||||
persistentMib: row.persistent_mib
|
persistentMib: row.persistent_mib
|
||||||
}),
|
}),
|
||||||
// Label by shape directly (consistent with the Instance Types
|
// 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.
|
// GPU row has vram but a missing/zero gpu_count.
|
||||||
{ intl, title: instanceTypeSeriesLabel(row) }
|
{ intl, title: instanceTypeSeriesLabel(row) }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ export const formatCpuSpec = (
|
|||||||
return parts.join(' · ');
|
return parts.join(' · ');
|
||||||
};
|
};
|
||||||
|
|
||||||
// CPU instance-type label: "CPU Only" plus its real size when known, e.g.
|
// 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-only · 2 vCPU · 4 GB". Used by both the table column and the trend
|
||||||
// legend so they read identically.
|
// legend so they read identically.
|
||||||
export const cpuOnlyLabel = (row?: Partial<ResourceBreakdownItem>): string => {
|
export const cpuOnlyLabel = (row?: Partial<ResourceBreakdownItem>): string => {
|
||||||
const spec = formatCpuSpec(row?.cpu_milli, row?.memory_mib);
|
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
|
// 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
|
// 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.)
|
// and each shape is a distinct series. (" x " matches the GPU Instances list.)
|
||||||
export const instanceTypeSeriesLabel = (
|
export const instanceTypeSeriesLabel = (
|
||||||
|
|||||||
Reference in New Issue
Block a user