fix: align instance type fields to api

This commit is contained in:
jialin
2026-06-05 23:08:45 +08:00
committed by Lawrence Li
parent 0cabbeb81c
commit 9f5d95f565
23 changed files with 733 additions and 457 deletions
+1
View File
@@ -288,6 +288,7 @@ export default {
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Remaining {count}',
'common.max': 'Max {count}',
'common.max.count': '{label} Count',
'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences'
};
+8
View File
@@ -108,6 +108,10 @@ export default {
'Please select at most {count} GPU card(s)',
'gpuservice.instance.gpuCount.min':
'Please select at least {count} GPU card(s)',
'gpuservice.instance.cpuCount.max':
'Please select at most {count} CPU core(s)',
'gpuservice.instance.cpuCount.min':
'Please select at least {count} CPU core(s)',
'gpuservice.instance.gpuCount.noAvailable':
'No available GPU resources, please choose another instance type.',
'gpuservice.instance.gpuCount.zero':
@@ -117,6 +121,10 @@ export default {
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk',
'gpuservice.table.count': 'Count',
'gpuservice.instance.disk.system': 'System Disk',
'gpuservice.instance.disk.ephemeral': 'Ephemeral Storage',
'gpuservice.instance.disk.persistent': 'Persistent Storage',
'gpuservice.instance.search.type.placeholder': 'Search by name',
'gpuservice.instance.search.template.placeholder':
'Search by template name, image or mount path',
+1
View File
@@ -288,6 +288,7 @@ export default {
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': '残り {count}',
'common.max': '最大 {count}',
'common.max.count': '{label} 数',
'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences'
};
+8
View File
@@ -108,6 +108,10 @@ export default {
'最大 {count} 枚の GPU カードを選択してください',
'gpuservice.instance.gpuCount.min':
'少なくとも {count} 枚の GPU カードを選択してください',
'gpuservice.instance.cpuCount.max':
'最大 {count} 個の CPU コアを選択してください',
'gpuservice.instance.cpuCount.min':
'少なくとも {count} 個の CPU コアを選択してください',
'gpuservice.instance.gpuCount.noAvailable':
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。',
@@ -116,6 +120,10 @@ export default {
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'ディスク',
'gpuservice.table.count': '数量',
'gpuservice.instance.disk.system': 'システムディスク',
'gpuservice.instance.disk.ephemeral': '一時ストレージ',
'gpuservice.instance.disk.persistent': '永続ストレージ',
'gpuservice.instance.search.type.placeholder': '名前で検索',
'gpuservice.instance.search.template.placeholder':
'テンプレート名、イメージまたはマウントパスで検索',
+1
View File
@@ -287,6 +287,7 @@ export default {
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Остаток {count}',
'common.max': 'Макс. {count}',
'common.max.count': 'Количество {label}',
'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences'
};
+6
View File
@@ -109,6 +109,8 @@ export default {
'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт',
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
'gpuservice.instance.cpuCount.max': 'Выберите максимум {count} ядер CPU',
'gpuservice.instance.cpuCount.min': 'Выберите минимум {count} ядер CPU',
'gpuservice.instance.gpuCount.noAvailable':
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.',
@@ -117,6 +119,10 @@ export default {
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Диск',
'gpuservice.table.count': 'Количество',
'gpuservice.instance.disk.system': 'Системный диск',
'gpuservice.instance.disk.ephemeral': 'Временное хранилище',
'gpuservice.instance.disk.persistent': 'Постоянное хранилище',
'gpuservice.instance.search.type.placeholder': 'Поиск по имени',
'gpuservice.instance.search.template.placeholder':
'Поиск по имени шаблона, образу или пути монтирования',
+1
View File
@@ -291,6 +291,7 @@ export default {
'common.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Kalan {count}',
'common.max': 'Maks. {count}',
'common.max.count': '{label} Sayısı',
'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Tercihler'
};
+6
View File
@@ -105,6 +105,8 @@ export default {
'gpuservice.instance.gpuCount.required': 'Lütfen GPU sayısını girin',
'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin',
'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı seçin',
'gpuservice.instance.cpuCount.max': 'En fazla {count} CPU çekirdeği seçin',
'gpuservice.instance.cpuCount.min': 'En az {count} CPU çekirdeği seçin',
'gpuservice.instance.gpuCount.noAvailable':
'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.',
'gpuservice.instance.gpuCount.zero': 'Yalnızca CPU, ortam hazırlığı için.',
@@ -113,6 +115,10 @@ export default {
'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk',
'gpuservice.table.count': 'Sayı',
'gpuservice.instance.disk.system': 'Sistem Diski',
'gpuservice.instance.disk.ephemeral': 'Geçici Depolama',
'gpuservice.instance.disk.persistent': 'Kalıcı Depolama',
'gpuservice.instance.search.type.placeholder': 'Ada göre ara',
'gpuservice.instance.search.template.placeholder':
'Şablon adına, imaja veya bağlama yoluna göre ara',
+1
View File
@@ -280,6 +280,7 @@ export default {
'common.image.limit.height': '图片高度须为{height}。',
'common.remaining': '剩余 {count}',
'common.max': '最大 {count}',
'common.max.count': '{label} 数量',
'common.validate.group': '请填写完整的{group}配置',
'common.preferences': '偏好设置'
};
+6
View File
@@ -100,6 +100,8 @@ export default {
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡',
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
'gpuservice.instance.cpuCount.max': '最多选择 {count} 核',
'gpuservice.instance.cpuCount.min': '至少选择 {count} 核',
'gpuservice.instance.gpuCount.noAvailable':
'没有可用的 GPU 资源,请选择其他实例类型。',
'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。',
@@ -108,6 +110,10 @@ export default {
'gpuservice.instance.memory': '显存',
'gpuservice.instance.ram': '内存',
'gpuservice.instance.disk': '磁盘',
'gpuservice.table.count': '数量',
'gpuservice.instance.disk.system': '系统盘',
'gpuservice.instance.disk.ephemeral': '临时存储',
'gpuservice.instance.disk.persistent': '持久存储',
'gpuservice.instance.search.type.placeholder': '搜索名称',
'gpuservice.instance.search.template.placeholder':
'搜索模板名称、镜像或挂载路径',
@@ -84,13 +84,6 @@ const Instance: React.FC = () => {
}),
children: renderParams(instanceData?.backend_parameters || [])
},
{
key: '1-1',
label: intl.formatMessage({
id: 'models.instance.params.autoInjected'
}),
children: renderParams(instanceData?.injected_backend_parameters || [])
},
{
key: '3',
label: intl.formatMessage({ id: 'benchmark.detail.kvCache' }),
@@ -75,6 +75,18 @@ export async function deleteGPUServiceInstance(id: number) {
});
}
export async function stopGPUServiceInstance(id: number) {
return request(`${GPU_SERVICE_INSTANCES_API}/${id}/stop`, {
method: 'PUT'
});
}
export async function startGPUServiceInstance(id: number) {
return request(`${GPU_SERVICE_INSTANCES_API}/${id}/start`, {
method: 'PUT'
});
}
// =========== Instance Types ===========
export async function queryGPUServiceInstanceTypes(
@@ -132,7 +132,7 @@ const AddModal: React.FC<AddModalProps> = ({
...it,
status: {
...it.status,
acceleratorTiers: (it.status?.acceleratorTiers ?? [])
tiers: (it.status?.tiers ?? [])
.map((tier: any) => ({
...tier,
candidates: (tier.candidates ?? []).filter((c: any) =>
@@ -142,7 +142,7 @@ const AddModal: React.FC<AddModalProps> = ({
.filter((tier: any) => (tier.candidates ?? []).length > 0)
}
}))
.filter((it) => (it.status?.acceleratorTiers ?? []).length > 0);
.filter((it) => (it.status?.tiers ?? []).length > 0);
};
const ownedInstanceTypes = useMemo(
@@ -217,7 +217,7 @@ const AddModal: React.FC<AddModalProps> = ({
): InstanceTypeItem | undefined => {
if (!candidateName) return undefined;
return instanceTypes.find((item) =>
(item.status?.acceleratorTiers ?? []).some((tier) =>
(item.status?.tiers ?? []).some((tier) =>
(tier.candidates ?? []).some(
(c) => c.name === candidateName && Number(c.cluster) === clusterId
)
@@ -35,7 +35,8 @@ const InstanceTypeCell: React.FC<{
display: 'flex',
alignItems: 'flex-start',
gap: 16,
marginBottom: 6
paddingBlock: 4,
borderBottom: '1px solid rgba(255, 255, 255, 0.2)'
}}
>
<div
@@ -19,28 +19,20 @@ const Title = styled.div`
const Meta = styled.div`
display: grid;
grid-template-columns: repeat(7, auto);
grid-auto-rows: minmax(15px, auto);
justify-content: start;
column-gap: 4px;
row-gap: 8px;
align-items: center;
color: var(--ant-color-text-secondary);
color: var(--ant-color-text-tertiary);
font-size: 13px;
.meta-row {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
align-items: center;
min-height: 15px;
color: var(--ant-color-text-tertiary);
}
.dot {
width: 3px;
height: 3px;
border-radius: 50%;
background-color: var(--ant-color-text-quaternary);
margin: 0 4px;
margin: 0 6px;
justify-self: center;
}
@@ -52,7 +44,10 @@ const Meta = styled.div`
interface InstanceTypeItemProps {
item: InstanceTypeItemModel;
showStatus?: boolean;
}
interface MetadataSectionProps {
spec: InstanceTypeItemModel['spec'];
}
const MetaItem: React.FC<{
@@ -73,91 +68,50 @@ const MetaItem: React.FC<{
);
};
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const intl = useIntl();
const specData = item.spec || {};
function getInstanceDerived(item: InstanceTypeItemModel) {
const spec = item.spec || {};
const acceleratable = spec.acceleratable;
// false: CPU; true: GPU
const acceleratable = specData.acceleratable;
const manufacturer = acceleratable ? specData.manufacturer || '' : 'cpu';
const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple';
// resource once-max-request status
const onceMaxRequestData = item.status?.onceMaxRequest || {};
// RAM resource
const ramUnit = acceleratable
? specData.unitResourcesParsed?.ram?.value
: onceMaxRequestData.ram;
// CPU resource
const cpuUnitCores = acceleratable
? specData.unitResourcesParsed?.cpu?.cores
: onceMaxRequestData.cpu;
const renderName = () => {
const displayName = specData.acceleratable
? specData.product || item.name
: 'CPU';
return displayName;
return {
acceleratable,
isGPU: acceleratable,
manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu',
displayName: acceleratable ? spec.product || item.name : 'CPU',
ramUnit: spec.unitResourcesParsed?.ram?.value,
cpuUnitCores: spec.unitResourcesParsed?.cpu?.cores
};
}
export const InstanceMetadataSection: React.FC<MetadataSectionProps> = ({
spec
}) => {
const intl = useIntl();
const { ramUnit, cpuUnitCores, isGPU } = getInstanceDerived({
spec
} as InstanceTypeItemModel);
return (
<>
<Title>
<Flex gap={8} align="center">
<AutoTooltip ghost minWidth={20} maxWidth={200}>
{renderName() || '-'}
</AutoTooltip>
{acceleratable && manufacturer && (
<span
style={{
color: 'var(--ant-color-text-tertiary)',
fontWeight: 400
}}
>
<ThemeTag color={manufacturerColor} disabled={false}>
{manufacturer?.toUpperCase()}
</ThemeTag>
</span>
)}
</Flex>
</Title>
<Meta>
<span className="meta-row">
{acceleratable && (
<>
<MetaItem
showDot={false}
icon="icon-gpu1"
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
value={
formatMemoryDisplay(specData?.memory ?? undefined) ?? '-'
}
/>
<MetaItem
show={!!specData?.sliced}
icon="icon-sliced"
label={intl.formatMessage({
id: 'gpuservice.instance.sliced'
})}
value={specData?.sliced}
/>
<MetaItem
icon="icon-database"
label={intl.formatMessage(
{
id: 'common.max'
},
{ count: '' }
)}
value={`${item.maxAccelerator || 0}`}
/>
</>
)}
</span>
<span className="meta-row">
<Meta>
{isGPU && (
<>
<MetaItem
show={isGPU}
showDot={false}
icon="icon-gpu1"
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
value={formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'}
/>
<MetaItem
icon="icon-database"
label={intl.formatMessage(
{
id: 'common.max'
},
{ count: '' }
)}
value={`${spec.maxComputeUnitCount || 0}`}
/>
<MetaItem
showDot={false}
icon="icon-ram-02"
@@ -165,15 +119,71 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
value={ramUnit ? `${ramUnit} GB` : '-'}
/>
<MetaItem
show={!!cpuUnitCores}
show={isGPU}
showDot={true}
icon="icon-cpu"
label="CPU"
value={cpuUnitCores || '-'}
/>
</span>
</Meta>
</>
</>
)}
{!isGPU && (
<>
<MetaItem
showDot={false}
icon="icon-ram-02"
label={intl.formatMessage({ id: 'gpuservice.instance.ram' })}
value={ramUnit ? `${ramUnit} GB` : '-'}
/>
<MetaItem
icon="icon-database"
label={intl.formatMessage(
{
id: 'common.max'
},
{ count: '' }
)}
value={`${spec.maxComputeUnitCount || 0}`}
/>
</>
)}
</Meta>
);
};
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const specData = item.spec || {};
const { acceleratable, manufacturer, displayName } = getInstanceDerived(item);
const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple';
const showManufacturerTag = acceleratable && manufacturer;
return (
<Flex
orientation="vertical"
justify="space-between"
style={{ height: '100%' }}
>
<Title>
<Flex gap={8} align="center">
<AutoTooltip ghost minWidth={20} maxWidth={200}>
{displayName || '-'}
</AutoTooltip>
{showManufacturerTag && (
<ThemeTag
color={manufacturerColor}
disabled={false}
style={{ fontWeight: 400 }}
>
{manufacturer?.toUpperCase()}
</ThemeTag>
)}
</Flex>
</Title>
<InstanceMetadataSection spec={specData}></InstanceMetadataSection>
</Flex>
);
};
@@ -4,6 +4,7 @@ import { createContext } from 'react';
export interface FormContextValue {
action: PageActionType;
currentData: any;
isGPUType?: boolean;
}
export const FormContext = createContext<FormContextValue>({
+107 -59
View File
@@ -3,6 +3,7 @@ import { StatusType } from '@/config/types';
import { IconFont, icons } from '@gpustack/core-ui';
import _ from 'lodash';
import React from 'react';
import { ListItem } from '../config/types';
export const InstanceStatusValueMap = {
Scheduling: 'Scheduling',
@@ -14,20 +15,46 @@ export const InstanceStatusValueMap = {
Preparing: 'Preparing',
NotReady: 'NotReady',
Ready: 'Ready',
Starting: 'Starting'
Starting: 'Starting',
Deleting: 'Deleting',
Stopping: 'Stopping',
Stopped: 'Stopped',
CreateFailed: 'CreateFailed',
SSHPublicKeyCreateFailed: 'SSHPublicKeyCreateFailed',
PersistentVolumeTypeCreateFailed: 'PersistentVolumeTypeCreateFailed',
PersistentVolumeCreateFailed: 'PersistentVolumeCreateFailed'
};
export const K8SStatuses = [
InstanceStatusValueMap.Scheduling,
InstanceStatusValueMap.Pending,
InstanceStatusValueMap.Scheduled,
InstanceStatusValueMap.Initializing,
InstanceStatusValueMap.InitializeFailed,
InstanceStatusValueMap.Initialized,
InstanceStatusValueMap.Preparing,
InstanceStatusValueMap.NotReady,
InstanceStatusValueMap.Ready
];
export const GPUStackFailedStatuses = [
InstanceStatusValueMap.CreateFailed,
InstanceStatusValueMap.SSHPublicKeyCreateFailed,
InstanceStatusValueMap.PersistentVolumeTypeCreateFailed,
InstanceStatusValueMap.PersistentVolumeCreateFailed
];
export const InstanceStatusLabelMap: Record<string, string> = {
[InstanceStatusValueMap.Scheduling]: 'Scheduling',
[InstanceStatusValueMap.Pending]: 'Pending',
[InstanceStatusValueMap.Scheduled]: 'Scheduled',
[InstanceStatusValueMap.Initializing]: 'Initializing',
[InstanceStatusValueMap.InitializeFailed]: 'InitializeFailed',
[InstanceStatusValueMap.Initialized]: 'Initialized',
[InstanceStatusValueMap.Preparing]: 'Preparing',
[InstanceStatusValueMap.NotReady]: 'NotReady',
[InstanceStatusValueMap.Ready]: 'Ready',
[InstanceStatusValueMap.Starting]: 'Starting'
// === K8s Statuses ===
...Object.fromEntries(K8SStatuses.map((status) => [status, status])),
// === GPUStack Statuses no logs and events===
[InstanceStatusValueMap.Starting]: 'Starting',
[InstanceStatusValueMap.Deleting]: 'Deleting',
[InstanceStatusValueMap.Stopping]: 'Stopping',
[InstanceStatusValueMap.Stopped]: 'Stopped',
...Object.fromEntries(
GPUStackFailedStatuses.map((status) => [status, status])
)
};
export const status: Record<string, StatusType> = {
@@ -40,61 +67,82 @@ export const status: Record<string, StatusType> = {
[InstanceStatusValueMap.Preparing]: StatusMaps.transitioning,
[InstanceStatusValueMap.NotReady]: StatusMaps.error,
[InstanceStatusValueMap.Ready]: StatusMaps.success,
[InstanceStatusValueMap.Starting]: StatusMaps.transitioning
[InstanceStatusValueMap.Starting]: StatusMaps.transitioning,
[InstanceStatusValueMap.Deleting]: StatusMaps.error,
[InstanceStatusValueMap.Stopping]: StatusMaps.error,
[InstanceStatusValueMap.Stopped]: StatusMaps.warning,
[InstanceStatusValueMap.CreateFailed]: StatusMaps.error,
[InstanceStatusValueMap.SSHPublicKeyCreateFailed]: StatusMaps.error,
[InstanceStatusValueMap.PersistentVolumeTypeCreateFailed]: StatusMaps.error,
[InstanceStatusValueMap.PersistentVolumeCreateFailed]: StatusMaps.error
};
// Lifecycle actions (logs/events/start/stop) require K8s proxy endpoints that
// the new /v2/gpu-instances API does not yet expose; keep them visible but
// disabled until backend support lands.
export const rowActionList = [
export interface InstanceRowAction {
label: string;
key: string;
locale?: boolean;
icon?: React.ReactNode;
props?: Record<string, any>;
show?: (record: ListItem) => boolean;
disabled?: (record: ListItem) => boolean;
}
export const rowActionList: InstanceRowAction[] = [
{
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
// {
// label: 'common.button.view',
// key: 'view',
// locale: true,
// icon: icons.DetailInfo
// },
{
label: 'common.button.viewlog',
key: 'viewlog',
locale: true,
icon: React.createElement(IconFont, { type: 'icon-logs' })
icon: React.createElement(IconFont, { type: 'icon-logs' }),
show: (record: ListItem) => {
const phase = record.status?.phase;
return [InstanceStatusValueMap.Ready].includes(phase as string);
}
},
{
label: 'common.button.viewevent',
key: 'viewevent',
locale: true,
icon: icons.ProfileOutlined
icon: icons.ProfileOutlined,
show: (record: ListItem) => {
const phase = record.status?.phase;
return K8SStatuses.includes(phase as string);
}
},
{
label: 'common.button.start',
key: 'start',
locale: true,
icon: icons.Play,
props: {
disabled: false
},
show: (record: ListItem) => {
const phase = record.status?.phase;
return [
...GPUStackFailedStatuses,
InstanceStatusValueMap.Stopped
].includes(phase as string);
}
},
{
label: 'common.button.stop',
key: 'stop',
locale: true,
icon: icons.Stop,
props: {
disabled: false
},
show: (record: ListItem) => {
const phase = record.status?.phase;
return K8SStatuses.includes(phase as string);
}
},
// {
// label: 'common.button.start',
// key: 'start',
// locale: true,
// icon: icons.Play,
// props: {
// disabled: true
// }
// },
// {
// label: 'common.button.stop',
// key: 'stop',
// locale: true,
// icon: icons.Stop,
// props: {
// disabled: true
// }
// },
// {
// label: 'common.button.recreate',
// key: 'recreate',
// locale: true,
// icon: icons.RetweetOutlined
// },
{
label: 'common.button.delete',
key: 'delete',
@@ -110,22 +158,19 @@ export const batchActionList = [
{
label: 'common.button.start',
key: 'start',
icon: icons.Play,
props: {
disabled: true
}
locale: true,
icon: icons.Play
},
{
label: 'common.button.stop',
key: 'stop',
icon: icons.Stop,
props: {
disabled: true
}
locale: true,
icon: icons.Stop
},
{
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
@@ -198,7 +243,7 @@ const parseQuantity = (value?: string | null): number => {
};
// Returns the slider max for the accelerator count: the largest
// tier.onceMaxRequest.accelerator across all acceleratorTiers (not from candidates).
// tier.onceMaxRequest.accelerator across all tiers (not from candidates).
export const getAcceleratorMax = (
tiers?: { onceMaxRequest: { accelerator?: string } }[] | null
) => {
@@ -229,7 +274,7 @@ export const pickCandidateForAccelerator = <
}[]
| undefined
| null,
count: number
{ count, acceleratable }: { count: number; acceleratable?: boolean }
): C | null => {
if (!tiers?.length) return null;
@@ -246,7 +291,10 @@ export const pickCandidateForAccelerator = <
// count === 0 ? parseQuantity(tier.onceMaxRequest.accelerator) > count; this is CPU-only case.
for (const tier of sorted) {
const fits = parseQuantity(tier.onceMaxRequest?.accelerator) >= count;
const acceleratorCount = parseQuantity(tier.onceMaxRequest?.accelerator);
const fits = acceleratable
? acceleratorCount >= count
: acceleratorCount === 0;
if (!fits) continue;
const candidate = tier.candidates?.find(hasResources);
if (candidate) return candidate;
@@ -158,6 +158,7 @@ export interface InstanceTypeSpec {
family?: string | null;
computeCapability?: string | null;
sliced?: string | null;
maxComputeUnitCount?: number;
unitResources?: {
cpu: QuanityCPU;
ram: QuanityMemory;
@@ -178,14 +179,13 @@ export interface InstanceTypeSpec {
export interface InstanceTypeStatus {
onceMaxRequest: InstanceTypeOnceMaxRequestResource;
acceleratorTiers?: InstanceTypeTier[] | null;
tiers?: InstanceTypeTier[] | null;
}
export interface InstanceTypeItem {
name: string;
spec: InstanceTypeSpec;
disabled?: boolean;
maxAccelerator?: number;
status: InstanceTypeStatus;
}
+219 -194
View File
@@ -36,6 +36,7 @@ import TemplateBasicForm, {
BasicResourceMax
} from '../../templates/forms/basic';
import { pickCandidateForAccelerator, StorageModeValueMap } from '../config';
import { FormContext } from '../config/form-context';
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
import instanceStyles from '../styles/instances.module.less';
import Basic from './basic';
@@ -121,6 +122,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const formAction =
realAction === PageAction.CREATE ? PageAction.CREATE : action;
const sshEnabled = Form.useWatch('enable_ssh', form);
const description = Form.useWatch(['spec', 'description'], form);
// `organization_id` is owned by the create-scope picker slot; it only
// exists/changes when a platform admin retargets the form. Watch it
// so the parent can re-scope offerings (see onScopeChange).
@@ -147,6 +149,11 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
: [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
});
const isGPUType = useMemo(() => {
const spec = parseJsonSafe(description || '{}', {} as any)?.spec;
return spec?.acceleratable;
}, [description]);
useEffect(() => {
if (open) {
fetchSSHData({ page: 1, perPage: 100 });
@@ -236,44 +243,42 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}
) => {
const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed;
const acceleratable = instanceType?.spec?.acceleratable;
const { count = 0 } = options;
if (acceleratable) {
return {
accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores
: undefined,
ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value
: undefined
};
}
return {};
return {
accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores
: null,
ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value
: null
};
};
const buildResourcesDataForSubmit = (values: FormData) => {
const unitResourcesParsed = getUnitResources();
const accelerator = _.toNumber(values.spec?.resources?.accelerator) || 0;
const cpuCount = _.toNumber(values.spec?.resources?.cpu) || 0;
const cpuNum = unitResourcesParsed?.cpu?.num;
const ramNum = unitResourcesParsed?.ram?.num;
const fallbackCpu = values.spec?.resources?.cpu;
const factor = isGPUType ? accelerator : cpuCount;
return {
cpu:
accelerator > 0 && cpuNum
? `${accelerator * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}`
: // Don't stringify an unset value — `${undefined}` becomes the
// literal "undefined", which fails k8s quantity validation.
fallbackCpu != null && fallbackCpu !== ''
? `${fallbackCpu}`
: undefined,
ram:
accelerator > 0 && ramNum
? `${accelerator * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
: values.spec?.resources?.ram
cpu: cpuNum
? `${factor * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}`
: // Don't stringify an unset value — `${undefined}` becomes the
// literal "undefined", which fails k8s quantity validation.
fallbackCpu
? `${fallbackCpu}`
: undefined,
ram: ramNum
? `${factor * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
: values.spec?.resources?.ram
};
};
@@ -284,31 +289,36 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
if (!instanceType) {
setSelectedInstanceType(undefined);
setOnceMaxRequest({ cpu: null, memory: null, localStorage: null });
// Also reset the instance-type-derived form fields (cluster, type and
// the CPU / memory / GPU-count resources) so the previous scope's
// numbers don't linger when the new org has no clusters.
const spec = form.getFieldValue('spec') || {};
form.setFieldsValue({
clusterId: null,
spec: {
...spec,
type: undefined,
type: null,
resources: {
...spec.resources,
accelerator: undefined,
cpu: undefined,
ram: undefined
accelerator: null,
cpu: null,
ram: null
}
}
} as any);
});
return;
}
const candidate = pickCandidateForAccelerator(
instanceType.status?.acceleratorTiers,
count
);
setSelectedInstanceType(instanceType);
const candidate = pickCandidateForAccelerator(
instanceType.status?.tiers,
{
count,
acceleratable: instanceType.spec?.acceleratable
}
);
console.log('picked candidate', candidate, instanceType, count);
setOnceMaxRequest({
cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores,
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest)?.value,
@@ -344,6 +354,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
});
const handleOnFinishFailed = (errorInfo: any) => {
console.log('formvalues===', form.getFieldsValue());
const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({
...field,
name: [
@@ -371,6 +382,15 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
realAction === PageAction.CREATE
) {
console.log('currentData', currentData);
const currentSpec = parseJsonSafe(
currentData?.description || '{}',
{} as any
)?.spec;
const count = currentSpec?.acceleratable
? _.toNumber(currentData?.spec?.resources?.accelerator)
: _.toNumber(currentData?.spec?.resources?.cpu) || 0;
form.setFieldsValue({
...currentData,
spec: {
@@ -380,8 +400,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
...buildResourcesData(
parseJsonSafe<any>(currentData?.description || '{}', {}),
{
count:
_.toNumber(currentData?.spec?.resources?.accelerator) || 0
count
}
)
}
@@ -456,15 +475,14 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
},
getFieldsValue: () => form.getFieldsValue(),
applyInstanceType: (instanceType?: InstanceTypeItem) => {
console.log('applying instance type', instanceType);
if (!instanceType) {
// Clear the current instance-type selection (e.g. the create scope
// switched to an org with no clusters) so its card/limits don't
// linger.
resolveAndApply(undefined, 0);
return;
}
const count = instanceType.spec?.acceleratable ? 1 : 0;
resolveAndApply(instanceType, count);
// set default to 1, for all instance types: GPU or non-GPU
resolveAndApply(instanceType, 1);
}
}));
@@ -490,7 +508,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const handleOnEnableSSHChange = (e: any) => {
const checked = e.target.checked;
console.log('enable ssh change', checked);
if (!checked) {
form.setFieldValue(['spec', 'sshPublicKeys'], []);
}
@@ -506,164 +523,172 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
segmentedTop={segmentedTop}
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
>
<Form
name="gpuServiceInstanceForm"
form={form}
disabled={disabled}
onFinish={handleFinish}
onFinishFailed={handleOnFinishFailed}
preserve={false}
initialValues={{
spec: {
description: '',
displayName: '',
image: '',
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [],
env: [],
volumeMount: '',
resources: {
accelerator: 1
},
volume: {
ephemeral: {
capacity: '50Gi'
},
persistent: {
name: ''
}
}
},
enable_ssh: false,
storageMode: StorageModeValueMap.Temporary
<FormContext.Provider
value={{
action: formAction,
currentData: currentData,
isGPUType: isGPUType
}}
>
<Basic action={formAction} disabled={disabled} />
<Form.Item name="clusterId" hidden>
<CInput.Input />
</Form.Item>
<CollapsePanel
activeKey={collapseKeys}
accordion={false}
onChange={handleOnCollapseChange}
items={[
{
key: TABKeysMap.INSTANCE_TYPE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.type'
}),
forceRender: true,
children: (
<InstanceTypeFormItem
action={formAction}
disabled={disabled}
selectedInstanceType={selectedInstanceType}
currentData={currentData as any}
onceMaxRequest={onceMaxRequest}
noAvailableTypes={noAvailableInstanceTypes}
onGPUCountChange={handleAcceleratorChange}
/>
)
<Form
name="gpuServiceInstanceForm"
form={form}
disabled={disabled}
onFinish={handleFinish}
onFinishFailed={handleOnFinishFailed}
preserve={false}
initialValues={{
spec: {
description: '',
displayName: '',
image: '',
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [],
env: [],
volumeMount: '',
resources: {
accelerator: 1
},
volume: {
ephemeral: {
capacity: '50Gi'
},
persistent: {
name: ''
}
}
},
{
key: TABKeysMap.TEMPLATE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
forceRender: true,
children: (
<TemplateBasicForm
page="instance"
disabled={disabled || formAction === PageAction.EDIT}
onceMaxRequest={onceMaxRequest}
/>
)
},
{
key: TABKeysMap.STORAGE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
forceRender: true,
children: (
<StorageVolume
disabled={disabled || formAction === PageAction.EDIT}
action={formAction}
/>
)
}
]}
/>
<Flex align="center" justify="space-between">
<Form.Item<FormData>
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
onChange={handleOnEnableSSHChange}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.enable'
})}
></CheckboxField>
enable_ssh: false,
storageMode: StorageModeValueMap.Temporary
}}
>
<Basic action={formAction} disabled={disabled} />
<Form.Item name="clusterId" hidden>
<CInput.Input />
</Form.Item>
<Button
size="small"
type="link"
icon={<PlusOutlined />}
onClick={handleAddSSHKey}
>
{intl.formatMessage({
id: 'gpuservice.instance.ssh.addKey'
})}
</Button>
</Flex>
<div className={instanceStyles.sshkeySelection}>
<Form.Item<FormData>
name={['spec', 'sshPublicKeys']}
style={{
marginBottom: 12
}}
hidden={!sshEnabled}
normalize={(value) =>
Array.isArray(value)
? value?.map((item) => ({ name: item }))
: []
}
getValueProps={(value) => ({
value: Array.isArray(value)
? value.map((item) => item?.name ?? item)
: []
})}
rules={[
<CollapsePanel
activeKey={collapseKeys}
accordion={false}
onChange={handleOnCollapseChange}
items={[
{
required: sshEnabled,
message: getRuleMessage('select', 'gpuservice.publicKey')
key: TABKeysMap.INSTANCE_TYPE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.type'
}),
forceRender: true,
children: (
<InstanceTypeFormItem
action={formAction}
disabled={disabled}
selectedInstanceType={selectedInstanceType}
currentData={currentData as any}
onceMaxRequest={onceMaxRequest}
noAvailableTypes={noAvailableInstanceTypes}
onGPUCountChange={handleAcceleratorChange}
/>
)
},
{
key: TABKeysMap.TEMPLATE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
forceRender: true,
children: (
<TemplateBasicForm
page="instance"
disabled={disabled || formAction === PageAction.EDIT}
onceMaxRequest={onceMaxRequest}
/>
)
},
{
key: TABKeysMap.STORAGE,
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
forceRender: true,
children: (
<StorageVolume
disabled={disabled || formAction === PageAction.EDIT}
action={formAction}
/>
)
}
]}
>
<MultipleSelect
required
disabled={disabled}
mode="multiple"
maxTagCount={'responsive'}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.assignKey'
/>
<Flex align="center" justify="space-between">
<Form.Item<FormData>
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
onChange={handleOnEnableSSHChange}
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.enable'
})}
></CheckboxField>
</Form.Item>
<Button
size="small"
type="link"
icon={<PlusOutlined />}
onClick={handleAddSSHKey}
>
{intl.formatMessage({
id: 'gpuservice.instance.ssh.addKey'
})}
styles={{
wrapper: {
width: '100%'
}
</Button>
</Flex>
<div className={instanceStyles.sshkeySelection}>
<Form.Item<FormData>
name={['spec', 'sshPublicKeys']}
style={{
marginBottom: 12
}}
options={sshkeyOptions}
></MultipleSelect>
</Form.Item>
</div>
</Form>
hidden={!sshEnabled}
normalize={(value) =>
Array.isArray(value)
? value?.map((item) => ({ name: item }))
: []
}
getValueProps={(value) => ({
value: Array.isArray(value)
? value.map((item) => item?.name ?? item)
: []
})}
rules={[
{
required: sshEnabled,
message: getRuleMessage('select', 'gpuservice.publicKey')
}
]}
>
<MultipleSelect
required
disabled={disabled}
mode="multiple"
maxTagCount={'responsive'}
label={intl.formatMessage({
id: 'gpuservice.instance.ssh.assignKey'
})}
styles={{
wrapper: {
width: '100%'
}
}}
options={sshkeyOptions}
></MultipleSelect>
</Form.Item>
</div>
</Form>
</FormContext.Provider>
<PublicKeyOverlay
open={sshOverlayOpen}
onCancel={() => setSshOverlayOpen(false)}
@@ -3,14 +3,15 @@ import { PageActionType } from '@/config/types';
import NumberSelection from '@/pages/_components/number-selection';
import { InputNumber } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Alert, Flex, Form } from 'antd';
import { Flex, Form } from 'antd';
import _ from 'lodash';
import { useMemo } from 'react';
import styled from 'styled-components';
import { BasicResourceMax } from '../../templates/forms/basic';
import { parseJsonSafe } from '../../utils';
import InstanceTypeItem from '../components/instance-type-item';
import { getAcceleratorMax } from '../config';
import InstanceTypeItem, {
InstanceMetadataSection
} from '../components/instance-type-item';
import {
FormData,
InstanceTypeItem as InstanceTypeItemModel,
@@ -41,7 +42,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
return (
<SelectedCard>
{selectedInstanceType ? (
<InstanceTypeItem item={selectedInstanceType} showStatus={false} />
<InstanceTypeItem item={selectedInstanceType} />
) : (
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
{intl.formatMessage({
@@ -79,10 +80,16 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
}) => {
const intl = useIntl();
const maxGpuCount =
action === PageAction.EDIT
? _.toNumber(currentData?.spec?.resources?.accelerator) || 0
: getAcceleratorMax(selectedInstanceType?.status?.acceleratorTiers);
const maxComputeUnitCount = useMemo(() => {
if (action === PageAction.EDIT) {
const description = parseJsonSafe(
currentData?.description || '{}',
{} as any
);
return description.spec?.maxComputeUnitCount || 0;
}
return selectedInstanceType?.spec?.maxComputeUnitCount || 0;
}, [action, currentData, selectedInstanceType]);
const isGPU = useMemo(() => {
if (action === PageAction.EDIT) {
@@ -133,7 +140,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
color: 'var(--ant-color-text-disabled)'
}}
>
<Flex align="flex-start" orientation="vertical">
<Flex
align="flex-start"
orientation="vertical"
justify="space-between"
gap={16}
>
<span
style={{
fontWeight: 400
@@ -143,11 +155,23 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
? `${description.product} x ${currentData?.spec?.resources?.accelerator}`
: 'CPU'}
</span>
<InstanceMetadataSection spec={description}></InstanceMetadataSection>
</Flex>
</SelectedCard>
);
};
const numberSelectionLabel = isGPU
? {
label: 'GPU',
maxLabel: 'gpuservice.instance.gpuCount.max',
minLabel: 'gpuservice.instance.gpuCount.min'
}
: {
label: 'CPU',
maxLabel: 'gpuservice.instance.cpuCount.max',
minLabel: 'gpuservice.instance.cpuCount.min'
};
return (
<div data-field="instanceType">
<FieldBlock>
@@ -171,9 +195,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
{action === PageAction.EDIT && renderInstanceType()}
</Form.Item>
</FieldBlock>
{!noAvailableTypes && isGPU && (
{!noAvailableTypes && (
<Form.Item<FormData>
name={['spec', 'resources', 'accelerator']}
key={isGPU ? 'accelerator' : 'cpu'}
name={
isGPU
? ['spec', 'resources', 'accelerator']
: ['spec', 'resources', 'cpu']
}
hidden={action === PageAction.EDIT}
normalize={(value) => (value != null ? _.toString(value) : undefined)}
getValueProps={(value) => ({
@@ -184,12 +213,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
required: true,
validator: (_, value) => {
const num = Number(value);
if (num > maxGpuCount) {
if (num > maxComputeUnitCount) {
return Promise.reject(
new Error(
intl.formatMessage(
{ id: 'gpuservice.instance.gpuCount.max' },
{ count: maxGpuCount }
{ id: numberSelectionLabel.maxLabel },
{ count: maxComputeUnitCount }
)
)
);
@@ -198,7 +227,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
return Promise.reject(
new Error(
intl.formatMessage(
{ id: 'gpuservice.instance.gpuCount.min' },
{ id: numberSelectionLabel.minLabel },
{ count: 0 }
)
)
@@ -212,36 +241,15 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
<NumberSelection
min={1}
onChange={handleOnGPUCountChange}
max={maxGpuCount}
max={maxComputeUnitCount}
step={1}
required
tips={intl.formatMessage({
id: 'gpuservice.instance.gpuCount.zero'
})}
disabled={disabled || action === PageAction.EDIT}
labelExtra={
!maxGpuCount &&
action !== PageAction.EDIT && (
<Alert
showIcon
type="warning"
title={intl.formatMessage({
id: 'gpuservice.instance.gpuCount.noAvailable'
})}
styles={{
root: {
marginLeft: 4,
paddingBlock: 0
}
}}
></Alert>
)
}
label={`${intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} (${intl.formatMessage(
label={`${intl.formatMessage({ id: 'common.max.count' }, { label: numberSelectionLabel.label })} (${intl.formatMessage(
{
id: 'common.max'
},
{ count: maxGpuCount }
{ count: maxComputeUnitCount }
)})`}
/>
</Form.Item>
@@ -257,21 +265,23 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
})}
>
<InputNumber
disabled={isGPU || disabled || action === PageAction.EDIT}
label={renderMemoryLabel()}
disabled={true}
label={intl.formatMessage({ id: 'gpuservice.template.memory' })}
max={onceMaxRequest?.memory ?? undefined}
/>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
<InputNumber
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
max={onceMaxRequest?.cpu ?? undefined}
disabled={disabled || isGPU || action === PageAction.EDIT}
/>
</Form.Item>
</div>
{isGPU && (
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
<InputNumber
label={'CPU'}
max={onceMaxRequest?.cpu ?? undefined}
disabled={true}
/>
</Form.Item>
</div>
)}
</Flex>
)}
</div>
@@ -21,6 +21,23 @@ import {
} from '../config';
import { InstanceTypeSpec, ListItem } from '../config/types';
const buildRowActions = (record: ListItem) => {
return rowActionList
.filter((action) => (action.show ? action.show(record) : true))
.map(({ show, disabled, ...action }) => ({
...action,
props: {
...action.props,
disabled: disabled
? disabled(record)
: (action.props?.disabled ?? false)
}
}));
};
const toGB = (v?: string | number) =>
v ? `${v}`.replace('Gi', ' GB').replace('Mi', ' MB') : '-';
const buildResourcesData = (
instanceType: {
spec: InstanceTypeSpec;
@@ -58,13 +75,13 @@ const formatResources = (
if (!record.spec?.resources?.accelerator) {
return {
cpu: record.spec?.resources?.cpu
? `${record.spec?.resources?.cpu}C`
? `${record.spec?.resources?.cpu} C`
: '-',
ram: record.spec?.resources?.ram
? `${record.spec?.resources?.ram}`.replace('Gi', 'GB')
? toGB(record.spec?.resources?.ram)
: '-',
localStorage: record.spec?.resources?.localStorage
? `${record.spec?.resources?.localStorage}`.replace('Gi', 'GB')
? toGB(record.spec?.resources?.localStorage)
: '-'
};
}
@@ -74,11 +91,11 @@ const formatResources = (
const vram = formatMemoryDisplay((instanceTypeSpec.spec as any)?.memory);
return {
cpu: resources.cpu ? `${resources.cpu}C` : '-',
ram: resources.ram ? `${resources.ram}GB` : '-',
cpu: resources.cpu ? `${resources.cpu} C` : '-',
ram: resources.ram ? `${resources.ram} GB` : '-',
vram,
localStorage: record.spec?.resources?.localStorage
? `${record.spec?.resources?.localStorage}`.replace('Gi', 'GB')
? toGB(record.spec?.resources?.localStorage)
: undefined
};
};
@@ -154,9 +171,6 @@ const useInstancesColumns = ({
const intl = useIntl();
const access = useAccess();
const toGB = (v?: string) =>
v ? `${v}`.replace('Gi', 'GB').replace('Mi', 'MB') : undefined;
const renderInstanceType = (record: ListItem) => {
const description =
parseJsonSafe<any>(record?.description || '{}', {}).spec || {};
@@ -181,8 +195,14 @@ const useInstancesColumns = ({
icon: 'icon-gpu',
name: 'GPU',
rows: [
['Count', accelerator ? `${accelerator}` : undefined],
['Instance Type', description.product],
[
intl.formatMessage({ id: 'gpuservice.table.count' }),
accelerator ? `${accelerator}` : undefined
],
[
intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
description.product
],
[
intl.formatMessage({ id: 'gpuservice.instance.memory' }),
resources.vram
@@ -197,24 +217,23 @@ const useInstancesColumns = ({
});
sections.push({
icon: 'icon-ram-02',
name: 'Memory',
name: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
rows: [[null, resources.ram]]
});
sections.push({
icon: 'icon-hard-disk',
name: intl.formatMessage({ id: 'gpuservice.instance.disk' }),
rows: [
['System', resources.localStorage],
// Data disk is either ephemeral OR persistent (mutually exclusive at
// create time, but each shown if present): ephemeral.capacity for a
// temp disk; persistentTemplate.spec.capacity / persistent.name for a
// persistent one (a referenced PV carries only a name, no capacity).
['Data', toGB(volume?.ephemeral?.capacity)],
[
'Persistent',
// Inline-created PV carries its own capacity; a referenced PV has only
// a name, so resolve its size from the fetched PV map (fall back to
// the name if it can't be resolved).
intl.formatMessage({ id: 'gpuservice.instance.disk.system' }),
resources.localStorage
],
[
intl.formatMessage({ id: 'gpuservice.instance.disk.ephemeral' }),
toGB(volume?.ephemeral?.capacity)
],
[
intl.formatMessage({ id: 'gpuservice.instance.disk.persistent' }),
toGB(volume?.persistentTemplate?.spec?.capacity) ||
toGB(pvCapacityByName?.[volume?.persistent?.name]) ||
volume?.persistent?.name
@@ -397,12 +416,14 @@ const useInstancesColumns = ({
ellipsis: {
showTitle: false
},
render: (_text, record) => (
<DropdownButtons
items={rowActionList}
onSelect={(val) => handleSelect(val, record)}
/>
)
render: (_text, record) => {
return (
<DropdownButtons
items={buildRowActions(record)}
onSelect={(val) => handleSelect(val, record)}
/>
);
}
}
];
}, [handleSelect, sortOrder, clusterList, intl, pvCapacityByName]);
+124 -10
View File
@@ -4,10 +4,18 @@ import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useTableFetch from '@/hooks/use-table-fetch';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
import { handleBatchRequest } from '@/utils';
import { PlusOutlined } from '@ant-design/icons';
import {
DeleteModal,
DropdownButtons,
FilterBar,
IconFont,
NoResult
} from '@gpustack/core-ui';
import { useAccess, useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, message, Modal, Table } from 'antd';
import { Button, ConfigProvider, message, Modal, Space, Table } from 'antd';
import { useSetAtom } from 'jotai';
import _ from 'lodash';
import { useEffect, useMemo, useState } from 'react';
@@ -16,11 +24,14 @@ import { queryGPUServiceStorage } from '../storage/apis';
import {
deleteGPUServiceInstance,
GPU_SERVICE_INSTANCES_API,
queryGPUServiceInstances
queryGPUServiceInstances,
startGPUServiceInstance,
stopGPUServiceInstance
} from './apis';
import AddModal from './components/add-modal';
import ViewEventsModal from './components/view-events-modal';
import ViewLogsModal from './components/view-logs-modal';
import { batchActionList } from './config';
import { FormData, ListItem } from './config/types';
import useCreateInstance from './hooks/use-create-instance';
import useInstancesColumns from './hooks/use-instances-columns';
@@ -146,28 +157,111 @@ const GPUService: React.FC = () => {
}
};
const handleStart = useMemoizedFn((row: ListItem) => {
modalRef.current?.show({
content: 'gpuservice.instance',
title: 'common.title.start.confirm',
okText: 'common.button.start',
operation: 'common.start.single.confirm',
name: row.name,
async onOk() {
await startGPUServiceInstance(row.id);
rowSelection.removeSelectedKeys([row.id]);
message.success(intl.formatMessage({ id: 'common.message.success' }));
fetchData();
}
});
});
const handleStop = useMemoizedFn((row: ListItem) => {
modalRef.current?.show({
content: 'gpuservice.instance',
title: 'common.title.stop.confirm',
okText: 'common.button.stop',
operation: 'common.stop.single.confirm',
name: row.name,
async onOk() {
await stopGPUServiceInstance(row.id);
rowSelection.removeSelectedKeys([row.id]);
fetchData();
}
});
});
const handleStartBatch = useMemoizedFn(() => {
modalRef.current?.show({
content: 'gpuservice.instance',
title: 'common.title.start.confirm',
okText: 'common.button.start',
operation: 'common.start.confirm',
selection: true,
async onOk() {
const successIds: number[] = [];
const res = await handleBatchRequest(
rowSelection.selectedRowKeys,
async (id: number) => {
await startGPUServiceInstance(id);
successIds.push(id);
}
);
rowSelection.removeSelectedKeys(successIds);
fetchData();
return res;
}
});
});
const handleStopBatch = useMemoizedFn(() => {
modalRef.current?.show({
content: 'gpuservice.instance',
title: 'common.title.stop.confirm',
okText: 'common.button.stop',
operation: 'common.stop.confirm',
selection: true,
async onOk() {
const successIds: number[] = [];
const res = await handleBatchRequest(
rowSelection.selectedRowKeys,
async (id: number) => {
await stopGPUServiceInstance(id);
successIds.push(id);
}
);
rowSelection.removeSelectedKeys(successIds);
fetchData();
return res;
}
});
});
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'view') {
openViewInstanceModal(row);
} else if (val === 'edit') {
openEditInstanceModal(row);
} else if (val === 'delete') {
handleDelete({ ...row, name: row.name });
handleDelete({ ...row });
} else if (val === 'recreate') {
openRecreateInstanceModal(row);
} else if (val === 'viewlog') {
openViewLogsModal(row);
} else if (val === 'viewevent') {
openViewEventsModal(row);
} else if (val === 'start') {
handleStart(row);
} else if (val === 'stop') {
handleStop(row);
}
// start / stop are disabled until backend endpoints exist
});
const handleBatchActionSelect = useMemoizedFn((val: string) => {
if (val === 'delete') {
handleDeleteBatch();
} else if (val === 'start') {
handleStartBatch();
} else if (val === 'stop') {
handleStopBatch();
}
// start / stop are disabled until backend endpoints exist
});
const renderEmpty = (type?: string) => {
@@ -236,11 +330,31 @@ const GPUService: React.FC = () => {
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
handleClickPrimary={
hasK8sCluster ? openCreateInstanceModal : undefined
right={
<Space size={16}>
{hasK8sCluster && (
<Button
icon={<PlusOutlined />}
type="primary"
onClick={openCreateInstanceModal}
>
{intl.formatMessage({ id: 'gpuservice.instance.add' })}
</Button>
)}
<DropdownButtons
items={batchActionList}
onSelect={handleBatchActionSelect}
disabled={!rowSelection.selectedRowKeys.length}
size="large"
showText={true}
extra={
rowSelection.selectedRowKeys.length > 0 && (
<span>({rowSelection.selectedRowKeys.length})</span>
)
}
/>
</Space>
}
buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })}
handleDeleteByBatch={handleDeleteBatch}
/>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
@@ -1,6 +1,6 @@
import { useQueryData } from '@gpustack/core-ui';
import React from 'react';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils';
import { ceilMilliToCore, parseQuantity, parseQuantityToGi } from '../../utils';
import { queryGPUServiceInstanceTypes } from '../apis';
import { getAcceleratorMax } from '../config';
import { InstanceTypeItem } from '../config/types';
@@ -28,12 +28,13 @@ export default function useQueryInstanceTypes() {
const isAvailable = (item: InstanceTypeItem) => {
if (!item.spec?.acceleratable)
return {
maxAccelerator: 0,
maxComputeUnitCount:
parseQuantity(item.status?.onceMaxRequest?.cpu || '0')?.num || 0, // CPU resource max request
available: true
};
const max = getAcceleratorMax(item.status?.acceleratorTiers);
const max = getAcceleratorMax(item.status?.tiers);
return {
maxAccelerator: max || 0,
maxComputeUnitCount: max || 0,
available: (max || 0) > 0
};
};
@@ -54,7 +55,8 @@ export default function useQueryInstanceTypes() {
unitResourcesParsed: {
cpu: ceilMilliToCore(item.spec?.unitResources?.cpu ?? null),
ram: parseQuantityToGi(item.spec?.unitResources?.ram ?? null)
}
},
maxComputeUnitCount: remainingData.maxComputeUnitCount
},
status: {
...item.status,
@@ -71,7 +73,7 @@ export default function useQueryInstanceTypes() {
: ''
}
},
maxAccelerator: remainingData.maxAccelerator,
disabled: !remainingData.available
};
});