import { StatusMaps } from '@/config'; 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', Pending: 'Pending', Scheduled: 'Scheduled', Initializing: 'Initializing', InitializeFailed: 'InitializeFailed', Initialized: 'Initialized', Preparing: 'Preparing', NotReady: 'NotReady', Ready: 'Ready', Starting: 'Starting', Deleting: 'Deleting', Stopping: 'Stopping', Stopped: 'Stopped', CreateFailed: 'CreateFailed', SSHPublicKeyCreateFailed: 'SSHPublicKeyCreateFailed', PersistentVolumeTypeCreateFailed: 'PersistentVolumeTypeCreateFailed', PersistentVolumeCreateFailed: 'PersistentVolumeCreateFailed', Unknown: 'Unknown' }; 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 = { // === K8s Statuses === ...Object.fromEntries(K8SStatuses.map((status) => [status, status])), // === MesaStack Statuses no logs and events=== [InstanceStatusValueMap.Deleting]: 'Deleting', [InstanceStatusValueMap.Stopping]: 'Stopping', [InstanceStatusValueMap.Stopped]: 'Stopped', [InstanceStatusValueMap.Unknown]: 'Unknown', [InstanceStatusValueMap.Starting]: 'Starting', ...Object.fromEntries( GPUStackFailedStatuses.map((status) => [status, status]) ) }; export const status: Record = { [InstanceStatusValueMap.Scheduling]: StatusMaps.transitioning, [InstanceStatusValueMap.Pending]: StatusMaps.transitioning, [InstanceStatusValueMap.Scheduled]: StatusMaps.transitioning, [InstanceStatusValueMap.Initializing]: StatusMaps.transitioning, [InstanceStatusValueMap.InitializeFailed]: StatusMaps.error, [InstanceStatusValueMap.Initialized]: StatusMaps.transitioning, [InstanceStatusValueMap.Preparing]: StatusMaps.transitioning, [InstanceStatusValueMap.NotReady]: StatusMaps.error, [InstanceStatusValueMap.Ready]: StatusMaps.success, [InstanceStatusValueMap.Starting]: StatusMaps.transitioning, [InstanceStatusValueMap.Deleting]: StatusMaps.warning, [InstanceStatusValueMap.Stopping]: StatusMaps.transitioning, [InstanceStatusValueMap.Stopped]: StatusMaps.inactive, [InstanceStatusValueMap.CreateFailed]: StatusMaps.error, [InstanceStatusValueMap.SSHPublicKeyCreateFailed]: StatusMaps.error, [InstanceStatusValueMap.PersistentVolumeTypeCreateFailed]: StatusMaps.error, [InstanceStatusValueMap.PersistentVolumeCreateFailed]: StatusMaps.error, [InstanceStatusValueMap.Unknown]: StatusMaps.error }; 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.viewlog', key: 'viewlog', locale: true, 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, show: (record: ListItem) => { const phase = record.status?.phase; return [...K8SStatuses, InstanceStatusValueMap.Starting].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 [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 [InstanceStatusValueMap.Ready].includes(phase as string); } }, { label: 'common.button.delete', key: 'delete', locale: true, icon: icons.DeleteOutlined, show: (record: ListItem) => { const phase = record.status?.phase; return true; }, props: { danger: true } } ]; export const batchActionList = [ { label: 'common.button.start', key: 'start', locale: true, icon: icons.Play }, { label: 'common.button.stop', key: 'stop', locale: true, icon: icons.Stop }, { label: 'common.button.delete', key: 'delete', locale: true, icon: icons.DeleteOutlined, props: { danger: true } } ]; export const InstanceTypePhaseValueMap = { PreParing: 'Preparing', Inactive: 'Inactive', Active: 'Active' }; export const InstanceTypePhaseLabelMap: Record = { [InstanceTypePhaseValueMap.PreParing]: 'Preparing', [InstanceTypePhaseValueMap.Inactive]: 'Inactive', [InstanceTypePhaseValueMap.Active]: 'Active' }; export const InstanceTypePhaseStatus: Record = { [InstanceTypePhaseValueMap.PreParing]: StatusMaps.transitioning, [InstanceTypePhaseValueMap.Inactive]: StatusMaps.inactive, [InstanceTypePhaseValueMap.Active]: StatusMaps.success }; export const StorageModeValueMap = { Temporary: 'temporary', Persistent: 'persistent' }; export const DEFAULT_PV_CAPACITY_GB = 20; // Constant SSH public key resource name used when SSH is enabled export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default'; const GI_DIVISOR: Record = { Ki: 1024 * 1024, Mi: 1024, Gi: 1 }; export const convertKiToGi = (value?: string): string | undefined => { if (!value) return value; const match = /^(-?\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti)$/.exec(value); if (!match) return value; const [, num, unit] = match; if (unit === 'Ti') return `${_.floor(Number(num), 0)} Ti`; return `${_.floor(Number(num) / GI_DIVISOR[unit], 0)} Gi`; }; // Memory quantity → display string, flooring to whole Gi. Accepts a k8s // quantity string ("16Gi" / "32607Mi") or a raw MiB number (as the Usage // breakdown carries it). Centralizes the conversion so the GPU Instances list // and the Usage tab render identical sizes (e.g. both "31GB", not 31 vs 32). export const formatMemoryDisplay = ( value?: string | number ): string | undefined => { if (!value) return undefined; const quantity = typeof value === 'number' ? `${value}Mi` : value; return ( convertKiToGi(quantity)?.replace(/Gi$/, 'GB').replace(/Ti$/, 'TB') || undefined ); }; const parseQuantity = (value?: string | null): number => { if (!value) return 0; const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value)); return match ? Number(match[1]) || 0 : 0; }; // Returns the slider max for the accelerator count: the largest // tier.onceMaxRequest.accelerator across all tiers (not from candidates). export const getAcceleratorMax = ( tiers?: { onceMaxRequest: { accelerator?: string } }[] | null ) => { if (!tiers?.length) return 0; return tiers.reduce((acc, tier) => { const n = parseQuantity(tier.onceMaxRequest?.accelerator); return n > acc ? n : acc; }, 0); }; // Picks the candidate (cluster + type name) that should fulfill a requested // accelerator count: the first candidate of the smallest tier whose // onceMaxRequest.accelerator is >= the requested count and whose cpu/ram/localStorage // remaining are all > 0. export const pickCandidateForAccelerator = < C extends { cluster: string; name: string; cpu?: { remaining?: string | null } | null; ram?: { remaining?: string | null } | null; localStorage?: { remaining?: string | null } | null; } >( tiers: | { onceMaxRequest: { accelerator?: string }; candidates?: C[] | null; }[] | undefined | null, { count, acceleratable }: { count: number; acceleratable?: boolean } ): C | null => { if (!tiers?.length) return null; const hasResources = (c: C) => parseQuantity(c.cpu?.remaining) > 0 && parseQuantity(c.ram?.remaining) > 0 && parseQuantity(c.localStorage?.remaining) > 0; const sorted = [...tiers].sort( (a, b) => parseQuantity(a.onceMaxRequest?.accelerator) - parseQuantity(b.onceMaxRequest?.accelerator) ); // count === 0 ? parseQuantity(tier.onceMaxRequest.accelerator) > count; this is CPU-only case. for (const tier of sorted) { 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; } return null; };