Applied via scripts/rebrand across UI labels, page title, and all locale strings (en/zh/ja/ru/tr). Functional references untouched: npm package, URLs, paths, JS identifiers, GPUSTACK_ constants, and the X-GPUStack-Model header. The backend-contract startsWith string in llmodels/hooks keeps "GPUStack server" to stay matchable. Logo images intentionally left unchanged for now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
310 lines
9.3 KiB
TypeScript
310 lines
9.3 KiB
TypeScript
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<string, string> = {
|
|
// === 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<string, StatusType> = {
|
|
[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<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.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<string, string> = {
|
|
[InstanceTypePhaseValueMap.PreParing]: 'Preparing',
|
|
[InstanceTypePhaseValueMap.Inactive]: 'Inactive',
|
|
[InstanceTypePhaseValueMap.Active]: 'Active'
|
|
};
|
|
|
|
export const InstanceTypePhaseStatus: Record<string, StatusType> = {
|
|
[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<string, number> = {
|
|
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;
|
|
};
|