diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index d69c15c1..3bb94a99 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -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' }; diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index 607d8bcf..51555bd2 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -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', diff --git a/src/locales/ja-JP/common.ts b/src/locales/ja-JP/common.ts index 5fd4608e..08faa04e 100644 --- a/src/locales/ja-JP/common.ts +++ b/src/locales/ja-JP/common.ts @@ -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' }; diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index f98bdafd..fce356d3 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -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': 'テンプレート名、イメージまたはマウントパスで検索', diff --git a/src/locales/ru-RU/common.ts b/src/locales/ru-RU/common.ts index b1106944..709ed946 100644 --- a/src/locales/ru-RU/common.ts +++ b/src/locales/ru-RU/common.ts @@ -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' }; diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index dadb1c2b..3d09c1c6 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -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': 'Поиск по имени шаблона, образу или пути монтирования', diff --git a/src/locales/tr-TR/common.ts b/src/locales/tr-TR/common.ts index 7fadf6e5..d587f51c 100644 --- a/src/locales/tr-TR/common.ts +++ b/src/locales/tr-TR/common.ts @@ -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' }; diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index 81c6fc78..b11a4132 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -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', diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 49a11b89..ae0e48aa 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -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': '偏好设置' }; diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index 4305d73b..9da31bca 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -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': '搜索模板名称、镜像或挂载路径', diff --git a/src/pages/benchmark/components/summary/instance.tsx b/src/pages/benchmark/components/summary/instance.tsx index d4b7c55f..33d23f21 100644 --- a/src/pages/benchmark/components/summary/instance.tsx +++ b/src/pages/benchmark/components/summary/instance.tsx @@ -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' }), diff --git a/src/pages/gpu-service/instances/apis/index.ts b/src/pages/gpu-service/instances/apis/index.ts index 5d47c98b..a9159c4d 100644 --- a/src/pages/gpu-service/instances/apis/index.ts +++ b/src/pages/gpu-service/instances/apis/index.ts @@ -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( diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index dcc08b80..dc3162a4 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -132,7 +132,7 @@ const AddModal: React.FC = ({ ...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 = ({ .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 = ({ ): 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 ) diff --git a/src/pages/gpu-service/instances/components/instance-type-cell.tsx b/src/pages/gpu-service/instances/components/instance-type-cell.tsx index a670e451..e8d3e271 100644 --- a/src/pages/gpu-service/instances/components/instance-type-cell.tsx +++ b/src/pages/gpu-service/instances/components/instance-type-cell.tsx @@ -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)' }} >
= ({ 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 = ({ + spec +}) => { + const intl = useIntl(); + + const { ramUnit, cpuUnitCores, isGPU } = getInstanceDerived({ + spec + } as InstanceTypeItemModel); return ( - <> - - <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> - - - - {acceleratable && ( - <> - - - - - )} - - + + {isGPU && ( + <> + + = ({ item }) => { value={ramUnit ? `${ramUnit} GB` : '-'} /> - - - + + )} + + {!isGPU && ( + <> + + + + )} + + ); +}; + +const InstanceTypeItem: React.FC = ({ item }) => { + const specData = item.spec || {}; + + const { acceleratable, manufacturer, displayName } = getInstanceDerived(item); + + const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple'; + const showManufacturerTag = acceleratable && manufacturer; + + return ( + + + <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> + + + ); }; diff --git a/src/pages/gpu-service/instances/config/form-context.ts b/src/pages/gpu-service/instances/config/form-context.ts index 9798e4e6..d339901e 100644 --- a/src/pages/gpu-service/instances/config/form-context.ts +++ b/src/pages/gpu-service/instances/config/form-context.ts @@ -4,6 +4,7 @@ import { createContext } from 'react'; export interface FormContextValue { action: PageActionType; currentData: any; + isGPUType?: boolean; } export const FormContext = createContext({ diff --git a/src/pages/gpu-service/instances/config/index.ts b/src/pages/gpu-service/instances/config/index.ts index 99255301..2a28e962 100644 --- a/src/pages/gpu-service/instances/config/index.ts +++ b/src/pages/gpu-service/instances/config/index.ts @@ -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 = { - [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 = { @@ -40,61 +67,82 @@ export const status: Record = { [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; + 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; diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts index 93347b85..0e7ab6d9 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -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; } diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 79a49556..dd057ceb 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -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 = 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 = 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 = 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 = 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 = 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 = 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 = forwardRef( ...buildResourcesData( parseJsonSafe(currentData?.description || '{}', {}), { - count: - _.toNumber(currentData?.spec?.resources?.accelerator) || 0 + count } ) } @@ -456,15 +475,14 @@ const GPUServiceInstanceForm: React.FC = 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 = 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 = forwardRef( segmentedTop={segmentedTop} getScrollElementScrollableHeight={getScrollElementScrollableHeight} > -
- - - - ) + - ) - }, - { - key: TABKeysMap.STORAGE, - label: intl.formatMessage({ - id: 'gpuservice.instance.section.storage' - }), - forceRender: true, - children: ( - - ) - } - ]} - /> - - - - name="enable_ssh" - valuePropName="checked" - style={{ marginBottom: 8 }} - > - + enable_ssh: false, + storageMode: StorageModeValueMap.Temporary + }} + > + + - - - -
- - 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={[ + + ) + }, + { + key: TABKeysMap.TEMPLATE, + label: intl.formatMessage({ + id: 'gpuservice.instance.section.template' + }), + forceRender: true, + children: ( + + ) + }, + { + key: TABKeysMap.STORAGE, + label: intl.formatMessage({ + id: 'gpuservice.instance.section.storage' + }), + forceRender: true, + children: ( + + ) } ]} - > - + + + + name="enable_ssh" + valuePropName="checked" + style={{ marginBottom: 8 }} + > + + + + + +
+ + name={['spec', 'sshPublicKeys']} + style={{ + marginBottom: 12 }} - options={sshkeyOptions} - > - -
- + 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') + } + ]} + > + + +
+ + setSshOverlayOpen(false)} diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index 1fdc85b6..c05683c8 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -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 = ({ return ( {selectedInstanceType ? ( - + ) : ( {intl.formatMessage({ @@ -79,10 +80,16 @@ const InstanceTypeFormItem: React.FC = ({ }) => { 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 = ({ color: 'var(--ant-color-text-disabled)' }} > - + = ({ ? `${description.product} x ${currentData?.spec?.resources?.accelerator}` : 'CPU'} + ); }; + 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 (
@@ -171,9 +195,14 @@ const InstanceTypeFormItem: React.FC = ({ {action === PageAction.EDIT && renderInstanceType()} - {!noAvailableTypes && isGPU && ( + {!noAvailableTypes && ( - 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 = ({ 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 = ({ 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 = ({ - ) - } - 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 } )})`} /> @@ -257,21 +265,23 @@ const InstanceTypeFormItem: React.FC = ({ })} >
-
- name={['spec', 'resources', 'cpu']}> - - -
+ {isGPU && ( +
+ name={['spec', 'resources', 'cpu']}> + + +
+ )} )}
diff --git a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx index fd43b10e..59e0be23 100644 --- a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx +++ b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx @@ -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(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) => ( - handleSelect(val, record)} - /> - ) + render: (_text, record) => { + return ( + handleSelect(val, record)} + /> + ); + } } ]; }, [handleSelect, sortOrder, clusterList, intl, pvCapacityByName]); diff --git a/src/pages/gpu-service/instances/index.tsx b/src/pages/gpu-service/instances/index.tsx index 5eda2c4e..86c7cf55 100644 --- a/src/pages/gpu-service/instances/index.tsx +++ b/src/pages/gpu-service/instances/index.tsx @@ -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={ + + {hasK8sCluster && ( + + )} + 0 && ( + ({rowSelection.selectedRowKeys.length}) + ) + } + /> + } - buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })} - handleDeleteByBatch={handleDeleteBatch} /> { 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 }; });