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.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Remaining {count}', 'common.remaining': 'Remaining {count}',
'common.max': 'Max {count}', 'common.max': 'Max {count}',
'common.max.count': '{label} Count',
'common.validate.group': 'Please complete the {group} configuration', 'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences' 'common.preferences': 'Preferences'
}; };
+8
View File
@@ -108,6 +108,10 @@ export default {
'Please select at most {count} GPU card(s)', 'Please select at most {count} GPU card(s)',
'gpuservice.instance.gpuCount.min': 'gpuservice.instance.gpuCount.min':
'Please select at least {count} GPU card(s)', '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': 'gpuservice.instance.gpuCount.noAvailable':
'No available GPU resources, please choose another instance type.', 'No available GPU resources, please choose another instance type.',
'gpuservice.instance.gpuCount.zero': 'gpuservice.instance.gpuCount.zero':
@@ -117,6 +121,10 @@ export default {
'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk', '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.type.placeholder': 'Search by name',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'Search by template name, image or mount path', '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.image.limit.height': 'Image height must be {height}.',
'common.remaining': '残り {count}', 'common.remaining': '残り {count}',
'common.max': '最大 {count}', 'common.max': '最大 {count}',
'common.max.count': '{label} 数',
'common.validate.group': 'Please complete the {group} configuration', 'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences' 'common.preferences': 'Preferences'
}; };
+8
View File
@@ -108,6 +108,10 @@ export default {
'最大 {count} 枚の GPU カードを選択してください', '最大 {count} 枚の GPU カードを選択してください',
'gpuservice.instance.gpuCount.min': 'gpuservice.instance.gpuCount.min':
'少なくとも {count} 枚の GPU カードを選択してください', '少なくとも {count} 枚の GPU カードを選択してください',
'gpuservice.instance.cpuCount.max':
'最大 {count} 個の CPU コアを選択してください',
'gpuservice.instance.cpuCount.min':
'少なくとも {count} 個の CPU コアを選択してください',
'gpuservice.instance.gpuCount.noAvailable': 'gpuservice.instance.gpuCount.noAvailable':
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。', '利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。', 'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。',
@@ -116,6 +120,10 @@ export default {
'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'ディスク', '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.type.placeholder': '名前で検索',
'gpuservice.instance.search.template.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.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Остаток {count}', 'common.remaining': 'Остаток {count}',
'common.max': 'Макс. {count}', 'common.max': 'Макс. {count}',
'common.max.count': 'Количество {label}',
'common.validate.group': 'Please complete the {group} configuration', 'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Preferences' 'common.preferences': 'Preferences'
}; };
+6
View File
@@ -109,6 +109,8 @@ export default {
'gpuservice.instance.gpuCount.required': 'Введите количество GPU', 'gpuservice.instance.gpuCount.required': 'Введите количество GPU',
'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт', 'gpuservice.instance.gpuCount.max': 'Выберите максимум {count} GPU-карт',
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт', 'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
'gpuservice.instance.cpuCount.max': 'Выберите максимум {count} ядер CPU',
'gpuservice.instance.cpuCount.min': 'Выберите минимум {count} ядер CPU',
'gpuservice.instance.gpuCount.noAvailable': 'gpuservice.instance.gpuCount.noAvailable':
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.', 'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.', 'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.',
@@ -117,6 +119,10 @@ export default {
'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Диск', '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.type.placeholder': 'Поиск по имени',
'gpuservice.instance.search.template.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.image.limit.height': 'Image height must be {height}.',
'common.remaining': 'Kalan {count}', 'common.remaining': 'Kalan {count}',
'common.max': 'Maks. {count}', 'common.max': 'Maks. {count}',
'common.max.count': '{label} Sayısı',
'common.validate.group': 'Please complete the {group} configuration', 'common.validate.group': 'Please complete the {group} configuration',
'common.preferences': 'Tercihler' '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.required': 'Lütfen GPU sayısını girin',
'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin', 'gpuservice.instance.gpuCount.max': 'En fazla {count} GPU kartı seçin',
'gpuservice.instance.gpuCount.min': 'En az {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': 'gpuservice.instance.gpuCount.noAvailable':
'Kullanılabilir GPU kaynağı yok, lütfen başka bir örnek türü seçin.', '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.', 'gpuservice.instance.gpuCount.zero': 'Yalnızca CPU, ortam hazırlığı için.',
@@ -113,6 +115,10 @@ export default {
'gpuservice.instance.memory': 'VRAM', 'gpuservice.instance.memory': 'VRAM',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk', '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.type.placeholder': 'Ada göre ara',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'Şablon adına, imaja veya bağlama yoluna göre ara', 'Ş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.image.limit.height': '图片高度须为{height}。',
'common.remaining': '剩余 {count}', 'common.remaining': '剩余 {count}',
'common.max': '最大 {count}', 'common.max': '最大 {count}',
'common.max.count': '{label} 数量',
'common.validate.group': '请填写完整的{group}配置', 'common.validate.group': '请填写完整的{group}配置',
'common.preferences': '偏好设置' 'common.preferences': '偏好设置'
}; };
+6
View File
@@ -100,6 +100,8 @@ export default {
'gpuservice.instance.gpuCount.required': '请输入 GPU 数量', 'gpuservice.instance.gpuCount.required': '请输入 GPU 数量',
'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡', 'gpuservice.instance.gpuCount.max': '最多选择 {count} 张卡',
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡', 'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
'gpuservice.instance.cpuCount.max': '最多选择 {count} 核',
'gpuservice.instance.cpuCount.min': '至少选择 {count} 核',
'gpuservice.instance.gpuCount.noAvailable': 'gpuservice.instance.gpuCount.noAvailable':
'没有可用的 GPU 资源,请选择其他实例类型。', '没有可用的 GPU 资源,请选择其他实例类型。',
'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。', 'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。',
@@ -108,6 +110,10 @@ export default {
'gpuservice.instance.memory': '显存', 'gpuservice.instance.memory': '显存',
'gpuservice.instance.ram': '内存', 'gpuservice.instance.ram': '内存',
'gpuservice.instance.disk': '磁盘', '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.type.placeholder': '搜索名称',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'搜索模板名称、镜像或挂载路径', '搜索模板名称、镜像或挂载路径',
@@ -84,13 +84,6 @@ const Instance: React.FC = () => {
}), }),
children: renderParams(instanceData?.backend_parameters || []) children: renderParams(instanceData?.backend_parameters || [])
}, },
{
key: '1-1',
label: intl.formatMessage({
id: 'models.instance.params.autoInjected'
}),
children: renderParams(instanceData?.injected_backend_parameters || [])
},
{ {
key: '3', key: '3',
label: intl.formatMessage({ id: 'benchmark.detail.kvCache' }), 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 =========== // =========== Instance Types ===========
export async function queryGPUServiceInstanceTypes( export async function queryGPUServiceInstanceTypes(
@@ -132,7 +132,7 @@ const AddModal: React.FC<AddModalProps> = ({
...it, ...it,
status: { status: {
...it.status, ...it.status,
acceleratorTiers: (it.status?.acceleratorTiers ?? []) tiers: (it.status?.tiers ?? [])
.map((tier: any) => ({ .map((tier: any) => ({
...tier, ...tier,
candidates: (tier.candidates ?? []).filter((c: any) => candidates: (tier.candidates ?? []).filter((c: any) =>
@@ -142,7 +142,7 @@ const AddModal: React.FC<AddModalProps> = ({
.filter((tier: any) => (tier.candidates ?? []).length > 0) .filter((tier: any) => (tier.candidates ?? []).length > 0)
} }
})) }))
.filter((it) => (it.status?.acceleratorTiers ?? []).length > 0); .filter((it) => (it.status?.tiers ?? []).length > 0);
}; };
const ownedInstanceTypes = useMemo( const ownedInstanceTypes = useMemo(
@@ -217,7 +217,7 @@ const AddModal: React.FC<AddModalProps> = ({
): InstanceTypeItem | undefined => { ): InstanceTypeItem | undefined => {
if (!candidateName) return undefined; if (!candidateName) return undefined;
return instanceTypes.find((item) => return instanceTypes.find((item) =>
(item.status?.acceleratorTiers ?? []).some((tier) => (item.status?.tiers ?? []).some((tier) =>
(tier.candidates ?? []).some( (tier.candidates ?? []).some(
(c) => c.name === candidateName && Number(c.cluster) === clusterId (c) => c.name === candidateName && Number(c.cluster) === clusterId
) )
@@ -35,7 +35,8 @@ const InstanceTypeCell: React.FC<{
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: 16, gap: 16,
marginBottom: 6 paddingBlock: 4,
borderBottom: '1px solid rgba(255, 255, 255, 0.2)'
}} }}
> >
<div <div
@@ -19,28 +19,20 @@ const Title = styled.div`
const Meta = styled.div` const Meta = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(7, auto); grid-template-columns: repeat(7, auto);
grid-auto-rows: minmax(15px, auto);
justify-content: start; justify-content: start;
column-gap: 4px; column-gap: 4px;
row-gap: 8px; row-gap: 8px;
align-items: center; align-items: center;
color: var(--ant-color-text-secondary);
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); color: var(--ant-color-text-tertiary);
} font-size: 13px;
.dot { .dot {
width: 3px; width: 3px;
height: 3px; height: 3px;
border-radius: 50%; border-radius: 50%;
background-color: var(--ant-color-text-quaternary); background-color: var(--ant-color-text-quaternary);
margin: 0 4px; margin: 0 6px;
justify-self: center; justify-self: center;
} }
@@ -52,7 +44,10 @@ const Meta = styled.div`
interface InstanceTypeItemProps { interface InstanceTypeItemProps {
item: InstanceTypeItemModel; item: InstanceTypeItemModel;
showStatus?: boolean; }
interface MetadataSectionProps {
spec: InstanceTypeItemModel['spec'];
} }
const MetaItem: React.FC<{ const MetaItem: React.FC<{
@@ -73,76 +68,39 @@ const MetaItem: React.FC<{
); );
}; };
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => { function getInstanceDerived(item: InstanceTypeItemModel) {
const intl = useIntl(); const spec = item.spec || {};
const specData = item.spec || {}; const acceleratable = spec.acceleratable;
// false: CPU; true: GPU return {
const acceleratable = specData.acceleratable; acceleratable,
isGPU: acceleratable,
const manufacturer = acceleratable ? specData.manufacturer || '' : 'cpu'; manufacturer: acceleratable ? spec.manufacturer || '' : 'cpu',
const manufacturerColor = manufactureColorMap[manufacturer] ?? 'purple'; displayName: acceleratable ? spec.product || item.name : 'CPU',
ramUnit: spec.unitResourcesParsed?.ram?.value,
// resource once-max-request status cpuUnitCores: spec.unitResourcesParsed?.cpu?.cores
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;
}; };
}
export const InstanceMetadataSection: React.FC<MetadataSectionProps> = ({
spec
}) => {
const intl = useIntl();
const { ramUnit, cpuUnitCores, isGPU } = getInstanceDerived({
spec
} as InstanceTypeItemModel);
return ( 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> <Meta>
<span className="meta-row"> {isGPU && (
{acceleratable && (
<> <>
<MetaItem <MetaItem
show={isGPU}
showDot={false} showDot={false}
icon="icon-gpu1" icon="icon-gpu1"
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })} label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
value={ value={formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'}
formatMemoryDisplay(specData?.memory ?? undefined) ?? '-'
}
/>
<MetaItem
show={!!specData?.sliced}
icon="icon-sliced"
label={intl.formatMessage({
id: 'gpuservice.instance.sliced'
})}
value={specData?.sliced}
/> />
<MetaItem <MetaItem
icon="icon-database" icon="icon-database"
@@ -152,12 +110,8 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
}, },
{ count: '' } { count: '' }
)} )}
value={`${item.maxAccelerator || 0}`} value={`${spec.maxComputeUnitCount || 0}`}
/> />
</>
)}
</span>
<span className="meta-row">
<MetaItem <MetaItem
showDot={false} showDot={false}
icon="icon-ram-02" icon="icon-ram-02"
@@ -165,15 +119,71 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
value={ramUnit ? `${ramUnit} GB` : '-'} value={ramUnit ? `${ramUnit} GB` : '-'}
/> />
<MetaItem <MetaItem
show={!!cpuUnitCores} show={isGPU}
showDot={true} showDot={true}
icon="icon-cpu" icon="icon-cpu"
label="CPU" label="CPU"
value={cpuUnitCores || '-'} 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 { export interface FormContextValue {
action: PageActionType; action: PageActionType;
currentData: any; currentData: any;
isGPUType?: boolean;
} }
export const FormContext = createContext<FormContextValue>({ 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 { IconFont, icons } from '@gpustack/core-ui';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import { ListItem } from '../config/types';
export const InstanceStatusValueMap = { export const InstanceStatusValueMap = {
Scheduling: 'Scheduling', Scheduling: 'Scheduling',
@@ -14,20 +15,46 @@ export const InstanceStatusValueMap = {
Preparing: 'Preparing', Preparing: 'Preparing',
NotReady: 'NotReady', NotReady: 'NotReady',
Ready: 'Ready', 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> = { export const InstanceStatusLabelMap: Record<string, string> = {
[InstanceStatusValueMap.Scheduling]: 'Scheduling', // === K8s Statuses ===
[InstanceStatusValueMap.Pending]: 'Pending', ...Object.fromEntries(K8SStatuses.map((status) => [status, status])),
[InstanceStatusValueMap.Scheduled]: 'Scheduled', // === GPUStack Statuses no logs and events===
[InstanceStatusValueMap.Initializing]: 'Initializing', [InstanceStatusValueMap.Starting]: 'Starting',
[InstanceStatusValueMap.InitializeFailed]: 'InitializeFailed', [InstanceStatusValueMap.Deleting]: 'Deleting',
[InstanceStatusValueMap.Initialized]: 'Initialized', [InstanceStatusValueMap.Stopping]: 'Stopping',
[InstanceStatusValueMap.Preparing]: 'Preparing', [InstanceStatusValueMap.Stopped]: 'Stopped',
[InstanceStatusValueMap.NotReady]: 'NotReady', ...Object.fromEntries(
[InstanceStatusValueMap.Ready]: 'Ready', GPUStackFailedStatuses.map((status) => [status, status])
[InstanceStatusValueMap.Starting]: 'Starting' )
}; };
export const status: Record<string, StatusType> = { export const status: Record<string, StatusType> = {
@@ -40,61 +67,82 @@ export const status: Record<string, StatusType> = {
[InstanceStatusValueMap.Preparing]: StatusMaps.transitioning, [InstanceStatusValueMap.Preparing]: StatusMaps.transitioning,
[InstanceStatusValueMap.NotReady]: StatusMaps.error, [InstanceStatusValueMap.NotReady]: StatusMaps.error,
[InstanceStatusValueMap.Ready]: StatusMaps.success, [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 export interface InstanceRowAction {
// the new /v2/gpu-instances API does not yet expose; keep them visible but label: string;
// disabled until backend support lands. key: string;
export const rowActionList = [ 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', label: 'common.button.edit',
key: 'edit', key: 'edit',
locale: true, locale: true,
icon: icons.EditOutlined icon: icons.EditOutlined
}, },
// {
// label: 'common.button.view',
// key: 'view',
// locale: true,
// icon: icons.DetailInfo
// },
{ {
label: 'common.button.viewlog', label: 'common.button.viewlog',
key: 'viewlog', key: 'viewlog',
locale: true, 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', label: 'common.button.viewevent',
key: 'viewevent', key: 'viewevent',
locale: true, 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', label: 'common.button.delete',
key: 'delete', key: 'delete',
@@ -110,22 +158,19 @@ export const batchActionList = [
{ {
label: 'common.button.start', label: 'common.button.start',
key: 'start', key: 'start',
icon: icons.Play, locale: true,
props: { icon: icons.Play
disabled: true
}
}, },
{ {
label: 'common.button.stop', label: 'common.button.stop',
key: 'stop', key: 'stop',
icon: icons.Stop, locale: true,
props: { icon: icons.Stop
disabled: true
}
}, },
{ {
label: 'common.button.delete', label: 'common.button.delete',
key: 'delete', key: 'delete',
locale: true,
icon: icons.DeleteOutlined, icon: icons.DeleteOutlined,
props: { props: {
danger: true danger: true
@@ -198,7 +243,7 @@ const parseQuantity = (value?: string | null): number => {
}; };
// Returns the slider max for the accelerator count: the largest // 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 = ( export const getAcceleratorMax = (
tiers?: { onceMaxRequest: { accelerator?: string } }[] | null tiers?: { onceMaxRequest: { accelerator?: string } }[] | null
) => { ) => {
@@ -229,7 +274,7 @@ export const pickCandidateForAccelerator = <
}[] }[]
| undefined | undefined
| null, | null,
count: number { count, acceleratable }: { count: number; acceleratable?: boolean }
): C | null => { ): C | null => {
if (!tiers?.length) return 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. // count === 0 ? parseQuantity(tier.onceMaxRequest.accelerator) > count; this is CPU-only case.
for (const tier of sorted) { 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; if (!fits) continue;
const candidate = tier.candidates?.find(hasResources); const candidate = tier.candidates?.find(hasResources);
if (candidate) return candidate; if (candidate) return candidate;
@@ -158,6 +158,7 @@ export interface InstanceTypeSpec {
family?: string | null; family?: string | null;
computeCapability?: string | null; computeCapability?: string | null;
sliced?: string | null; sliced?: string | null;
maxComputeUnitCount?: number;
unitResources?: { unitResources?: {
cpu: QuanityCPU; cpu: QuanityCPU;
ram: QuanityMemory; ram: QuanityMemory;
@@ -178,14 +179,13 @@ export interface InstanceTypeSpec {
export interface InstanceTypeStatus { export interface InstanceTypeStatus {
onceMaxRequest: InstanceTypeOnceMaxRequestResource; onceMaxRequest: InstanceTypeOnceMaxRequestResource;
acceleratorTiers?: InstanceTypeTier[] | null; tiers?: InstanceTypeTier[] | null;
} }
export interface InstanceTypeItem { export interface InstanceTypeItem {
name: string; name: string;
spec: InstanceTypeSpec; spec: InstanceTypeSpec;
disabled?: boolean; disabled?: boolean;
maxAccelerator?: number;
status: InstanceTypeStatus; status: InstanceTypeStatus;
} }
+58 -33
View File
@@ -36,6 +36,7 @@ import TemplateBasicForm, {
BasicResourceMax BasicResourceMax
} from '../../templates/forms/basic'; } from '../../templates/forms/basic';
import { pickCandidateForAccelerator, StorageModeValueMap } from '../config'; import { pickCandidateForAccelerator, StorageModeValueMap } from '../config';
import { FormContext } from '../config/form-context';
import { FormData, InstanceTypeItem, ListItem } from '../config/types'; import { FormData, InstanceTypeItem, ListItem } from '../config/types';
import instanceStyles from '../styles/instances.module.less'; import instanceStyles from '../styles/instances.module.less';
import Basic from './basic'; import Basic from './basic';
@@ -121,6 +122,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const formAction = const formAction =
realAction === PageAction.CREATE ? PageAction.CREATE : action; realAction === PageAction.CREATE ? PageAction.CREATE : action;
const sshEnabled = Form.useWatch('enable_ssh', form); 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 // `organization_id` is owned by the create-scope picker slot; it only
// exists/changes when a platform admin retargets the form. Watch it // exists/changes when a platform admin retargets the form. Watch it
// so the parent can re-scope offerings (see onScopeChange). // 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] : [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
}); });
const isGPUType = useMemo(() => {
const spec = parseJsonSafe(description || '{}', {} as any)?.spec;
return spec?.acceleratable;
}, [description]);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchSSHData({ page: 1, perPage: 100 }); fetchSSHData({ page: 1, perPage: 100 });
@@ -236,43 +243,41 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
} }
) => { ) => {
const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed; const unitResourcesParsed = instanceType?.spec?.unitResourcesParsed;
const acceleratable = instanceType?.spec?.acceleratable;
const { count = 0 } = options; const { count = 0 } = options;
if (acceleratable) {
return { return {
accelerator: _.toString(count), accelerator: _.toString(count),
cpu: unitResourcesParsed?.cpu?.cores cpu: unitResourcesParsed?.cpu?.cores
? count * unitResourcesParsed?.cpu?.cores ? count * unitResourcesParsed?.cpu?.cores
: undefined, : null,
ram: unitResourcesParsed?.ram?.value ram: unitResourcesParsed?.ram?.value
? count * unitResourcesParsed?.ram?.value ? count * unitResourcesParsed?.ram?.value
: undefined : null
}; };
}
return {};
}; };
const buildResourcesDataForSubmit = (values: FormData) => { const buildResourcesDataForSubmit = (values: FormData) => {
const unitResourcesParsed = getUnitResources(); const unitResourcesParsed = getUnitResources();
const accelerator = _.toNumber(values.spec?.resources?.accelerator) || 0; const accelerator = _.toNumber(values.spec?.resources?.accelerator) || 0;
const cpuCount = _.toNumber(values.spec?.resources?.cpu) || 0;
const cpuNum = unitResourcesParsed?.cpu?.num; const cpuNum = unitResourcesParsed?.cpu?.num;
const ramNum = unitResourcesParsed?.ram?.num; const ramNum = unitResourcesParsed?.ram?.num;
const fallbackCpu = values.spec?.resources?.cpu; const fallbackCpu = values.spec?.resources?.cpu;
const factor = isGPUType ? accelerator : cpuCount;
return { return {
cpu: cpu: cpuNum
accelerator > 0 && cpuNum ? `${factor * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}`
? `${accelerator * cpuNum}${unitResourcesParsed?.cpu?.unit || ''}`
: // Don't stringify an unset value — `${undefined}` becomes the : // Don't stringify an unset value — `${undefined}` becomes the
// literal "undefined", which fails k8s quantity validation. // literal "undefined", which fails k8s quantity validation.
fallbackCpu != null && fallbackCpu !== '' fallbackCpu
? `${fallbackCpu}` ? `${fallbackCpu}`
: undefined, : undefined,
ram: ram: ramNum
accelerator > 0 && ramNum ? `${factor * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
? `${accelerator * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
: values.spec?.resources?.ram : values.spec?.resources?.ram
}; };
}; };
@@ -284,31 +289,36 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
if (!instanceType) { if (!instanceType) {
setSelectedInstanceType(undefined); setSelectedInstanceType(undefined);
setOnceMaxRequest({ cpu: null, memory: null, localStorage: null }); 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') || {}; const spec = form.getFieldValue('spec') || {};
form.setFieldsValue({ form.setFieldsValue({
clusterId: null, clusterId: null,
spec: { spec: {
...spec, ...spec,
type: undefined, type: null,
resources: { resources: {
...spec.resources, ...spec.resources,
accelerator: undefined, accelerator: null,
cpu: undefined, cpu: null,
ram: undefined ram: null
} }
} }
} as any); });
return; return;
} }
const candidate = pickCandidateForAccelerator(
instanceType.status?.acceleratorTiers,
count
);
setSelectedInstanceType(instanceType); setSelectedInstanceType(instanceType);
const candidate = pickCandidateForAccelerator(
instanceType.status?.tiers,
{
count,
acceleratable: instanceType.spec?.acceleratable
}
);
console.log('picked candidate', candidate, instanceType, count);
setOnceMaxRequest({ setOnceMaxRequest({
cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores, cpu: ceilMilliToCore(candidate?.cpu?.onceMaxRequest)?.cores,
memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest)?.value, memory: parseQuantityToGi(candidate?.ram?.onceMaxRequest)?.value,
@@ -344,6 +354,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}); });
const handleOnFinishFailed = (errorInfo: any) => { const handleOnFinishFailed = (errorInfo: any) => {
console.log('formvalues===', form.getFieldsValue());
const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({ const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({
...field, ...field,
name: [ name: [
@@ -371,6 +382,15 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
realAction === PageAction.CREATE realAction === PageAction.CREATE
) { ) {
console.log('currentData', currentData); 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({ form.setFieldsValue({
...currentData, ...currentData,
spec: { spec: {
@@ -380,8 +400,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
...buildResourcesData( ...buildResourcesData(
parseJsonSafe<any>(currentData?.description || '{}', {}), parseJsonSafe<any>(currentData?.description || '{}', {}),
{ {
count: count
_.toNumber(currentData?.spec?.resources?.accelerator) || 0
} }
) )
} }
@@ -456,15 +475,14 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}, },
getFieldsValue: () => form.getFieldsValue(), getFieldsValue: () => form.getFieldsValue(),
applyInstanceType: (instanceType?: InstanceTypeItem) => { applyInstanceType: (instanceType?: InstanceTypeItem) => {
console.log('applying instance type', instanceType);
if (!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); resolveAndApply(undefined, 0);
return; 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 handleOnEnableSSHChange = (e: any) => {
const checked = e.target.checked; const checked = e.target.checked;
console.log('enable ssh change', checked);
if (!checked) { if (!checked) {
form.setFieldValue(['spec', 'sshPublicKeys'], []); form.setFieldValue(['spec', 'sshPublicKeys'], []);
} }
@@ -505,6 +522,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
setActiveKey={handleActiveChange} setActiveKey={handleActiveChange}
segmentedTop={segmentedTop} segmentedTop={segmentedTop}
getScrollElementScrollableHeight={getScrollElementScrollableHeight} getScrollElementScrollableHeight={getScrollElementScrollableHeight}
>
<FormContext.Provider
value={{
action: formAction,
currentData: currentData,
isGPUType: isGPUType
}}
> >
<Form <Form
name="gpuServiceInstanceForm" name="gpuServiceInstanceForm"
@@ -664,6 +688,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
</Form.Item> </Form.Item>
</div> </div>
</Form> </Form>
</FormContext.Provider>
<PublicKeyOverlay <PublicKeyOverlay
open={sshOverlayOpen} open={sshOverlayOpen}
onCancel={() => setSshOverlayOpen(false)} onCancel={() => setSshOverlayOpen(false)}
@@ -3,14 +3,15 @@ import { PageActionType } from '@/config/types';
import NumberSelection from '@/pages/_components/number-selection'; import NumberSelection from '@/pages/_components/number-selection';
import { InputNumber } from '@gpustack/core-ui'; import { InputNumber } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Alert, Flex, Form } from 'antd'; import { Flex, Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { useMemo } from 'react'; import { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { BasicResourceMax } from '../../templates/forms/basic'; import { BasicResourceMax } from '../../templates/forms/basic';
import { parseJsonSafe } from '../../utils'; import { parseJsonSafe } from '../../utils';
import InstanceTypeItem from '../components/instance-type-item'; import InstanceTypeItem, {
import { getAcceleratorMax } from '../config'; InstanceMetadataSection
} from '../components/instance-type-item';
import { import {
FormData, FormData,
InstanceTypeItem as InstanceTypeItemModel, InstanceTypeItem as InstanceTypeItemModel,
@@ -41,7 +42,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
return ( return (
<SelectedCard> <SelectedCard>
{selectedInstanceType ? ( {selectedInstanceType ? (
<InstanceTypeItem item={selectedInstanceType} showStatus={false} /> <InstanceTypeItem item={selectedInstanceType} />
) : ( ) : (
<span style={{ color: 'var(--ant-color-text-tertiary)' }}> <span style={{ color: 'var(--ant-color-text-tertiary)' }}>
{intl.formatMessage({ {intl.formatMessage({
@@ -79,10 +80,16 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const maxGpuCount = const maxComputeUnitCount = useMemo(() => {
action === PageAction.EDIT if (action === PageAction.EDIT) {
? _.toNumber(currentData?.spec?.resources?.accelerator) || 0 const description = parseJsonSafe(
: getAcceleratorMax(selectedInstanceType?.status?.acceleratorTiers); currentData?.description || '{}',
{} as any
);
return description.spec?.maxComputeUnitCount || 0;
}
return selectedInstanceType?.spec?.maxComputeUnitCount || 0;
}, [action, currentData, selectedInstanceType]);
const isGPU = useMemo(() => { const isGPU = useMemo(() => {
if (action === PageAction.EDIT) { if (action === PageAction.EDIT) {
@@ -133,7 +140,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
color: 'var(--ant-color-text-disabled)' color: 'var(--ant-color-text-disabled)'
}} }}
> >
<Flex align="flex-start" orientation="vertical"> <Flex
align="flex-start"
orientation="vertical"
justify="space-between"
gap={16}
>
<span <span
style={{ style={{
fontWeight: 400 fontWeight: 400
@@ -143,11 +155,23 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
? `${description.product} x ${currentData?.spec?.resources?.accelerator}` ? `${description.product} x ${currentData?.spec?.resources?.accelerator}`
: 'CPU'} : 'CPU'}
</span> </span>
<InstanceMetadataSection spec={description}></InstanceMetadataSection>
</Flex> </Flex>
</SelectedCard> </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 ( return (
<div data-field="instanceType"> <div data-field="instanceType">
<FieldBlock> <FieldBlock>
@@ -171,9 +195,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
{action === PageAction.EDIT && renderInstanceType()} {action === PageAction.EDIT && renderInstanceType()}
</Form.Item> </Form.Item>
</FieldBlock> </FieldBlock>
{!noAvailableTypes && isGPU && ( {!noAvailableTypes && (
<Form.Item<FormData> <Form.Item<FormData>
name={['spec', 'resources', 'accelerator']} key={isGPU ? 'accelerator' : 'cpu'}
name={
isGPU
? ['spec', 'resources', 'accelerator']
: ['spec', 'resources', 'cpu']
}
hidden={action === PageAction.EDIT} hidden={action === PageAction.EDIT}
normalize={(value) => (value != null ? _.toString(value) : undefined)} normalize={(value) => (value != null ? _.toString(value) : undefined)}
getValueProps={(value) => ({ getValueProps={(value) => ({
@@ -184,12 +213,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
required: true, required: true,
validator: (_, value) => { validator: (_, value) => {
const num = Number(value); const num = Number(value);
if (num > maxGpuCount) { if (num > maxComputeUnitCount) {
return Promise.reject( return Promise.reject(
new Error( new Error(
intl.formatMessage( intl.formatMessage(
{ id: 'gpuservice.instance.gpuCount.max' }, { id: numberSelectionLabel.maxLabel },
{ count: maxGpuCount } { count: maxComputeUnitCount }
) )
) )
); );
@@ -198,7 +227,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
return Promise.reject( return Promise.reject(
new Error( new Error(
intl.formatMessage( intl.formatMessage(
{ id: 'gpuservice.instance.gpuCount.min' }, { id: numberSelectionLabel.minLabel },
{ count: 0 } { count: 0 }
) )
) )
@@ -212,36 +241,15 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
<NumberSelection <NumberSelection
min={1} min={1}
onChange={handleOnGPUCountChange} onChange={handleOnGPUCountChange}
max={maxGpuCount} max={maxComputeUnitCount}
step={1} step={1}
required required
tips={intl.formatMessage({
id: 'gpuservice.instance.gpuCount.zero'
})}
disabled={disabled || action === PageAction.EDIT} disabled={disabled || action === PageAction.EDIT}
labelExtra={ label={`${intl.formatMessage({ id: 'common.max.count' }, { label: numberSelectionLabel.label })} (${intl.formatMessage(
!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(
{ {
id: 'common.max' id: 'common.max'
}, },
{ count: maxGpuCount } { count: maxComputeUnitCount }
)})`} )})`}
/> />
</Form.Item> </Form.Item>
@@ -257,21 +265,23 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
})} })}
> >
<InputNumber <InputNumber
disabled={isGPU || disabled || action === PageAction.EDIT} disabled={true}
label={renderMemoryLabel()} label={intl.formatMessage({ id: 'gpuservice.template.memory' })}
max={onceMaxRequest?.memory ?? undefined} max={onceMaxRequest?.memory ?? undefined}
/> />
</Form.Item> </Form.Item>
</div> </div>
{isGPU && (
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 'resources', 'cpu']}> <Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
<InputNumber <InputNumber
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)} label={'CPU'}
max={onceMaxRequest?.cpu ?? undefined} max={onceMaxRequest?.cpu ?? undefined}
disabled={disabled || isGPU || action === PageAction.EDIT} disabled={true}
/> />
</Form.Item> </Form.Item>
</div> </div>
)}
</Flex> </Flex>
)} )}
</div> </div>
@@ -21,6 +21,23 @@ import {
} from '../config'; } from '../config';
import { InstanceTypeSpec, ListItem } from '../config/types'; 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 = ( const buildResourcesData = (
instanceType: { instanceType: {
spec: InstanceTypeSpec; spec: InstanceTypeSpec;
@@ -61,10 +78,10 @@ const formatResources = (
? `${record.spec?.resources?.cpu} C` ? `${record.spec?.resources?.cpu} C`
: '-', : '-',
ram: record.spec?.resources?.ram ram: record.spec?.resources?.ram
? `${record.spec?.resources?.ram}`.replace('Gi', 'GB') ? toGB(record.spec?.resources?.ram)
: '-', : '-',
localStorage: record.spec?.resources?.localStorage localStorage: record.spec?.resources?.localStorage
? `${record.spec?.resources?.localStorage}`.replace('Gi', 'GB') ? toGB(record.spec?.resources?.localStorage)
: '-' : '-'
}; };
} }
@@ -78,7 +95,7 @@ const formatResources = (
ram: resources.ram ? `${resources.ram} GB` : '-', ram: resources.ram ? `${resources.ram} GB` : '-',
vram, vram,
localStorage: record.spec?.resources?.localStorage localStorage: record.spec?.resources?.localStorage
? `${record.spec?.resources?.localStorage}`.replace('Gi', 'GB') ? toGB(record.spec?.resources?.localStorage)
: undefined : undefined
}; };
}; };
@@ -154,9 +171,6 @@ const useInstancesColumns = ({
const intl = useIntl(); const intl = useIntl();
const access = useAccess(); const access = useAccess();
const toGB = (v?: string) =>
v ? `${v}`.replace('Gi', 'GB').replace('Mi', 'MB') : undefined;
const renderInstanceType = (record: ListItem) => { const renderInstanceType = (record: ListItem) => {
const description = const description =
parseJsonSafe<any>(record?.description || '{}', {}).spec || {}; parseJsonSafe<any>(record?.description || '{}', {}).spec || {};
@@ -181,8 +195,14 @@ const useInstancesColumns = ({
icon: 'icon-gpu', icon: 'icon-gpu',
name: 'GPU', name: 'GPU',
rows: [ 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' }), intl.formatMessage({ id: 'gpuservice.instance.memory' }),
resources.vram resources.vram
@@ -197,24 +217,23 @@ const useInstancesColumns = ({
}); });
sections.push({ sections.push({
icon: 'icon-ram-02', icon: 'icon-ram-02',
name: 'Memory', name: intl.formatMessage({ id: 'gpuservice.instance.ram' }),
rows: [[null, resources.ram]] rows: [[null, resources.ram]]
}); });
sections.push({ sections.push({
icon: 'icon-hard-disk', icon: 'icon-hard-disk',
name: intl.formatMessage({ id: 'gpuservice.instance.disk' }), name: intl.formatMessage({ id: 'gpuservice.instance.disk' }),
rows: [ 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', intl.formatMessage({ id: 'gpuservice.instance.disk.system' }),
// Inline-created PV carries its own capacity; a referenced PV has only resources.localStorage
// 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.ephemeral' }),
toGB(volume?.ephemeral?.capacity)
],
[
intl.formatMessage({ id: 'gpuservice.instance.disk.persistent' }),
toGB(volume?.persistentTemplate?.spec?.capacity) || toGB(volume?.persistentTemplate?.spec?.capacity) ||
toGB(pvCapacityByName?.[volume?.persistent?.name]) || toGB(pvCapacityByName?.[volume?.persistent?.name]) ||
volume?.persistent?.name volume?.persistent?.name
@@ -397,12 +416,14 @@ const useInstancesColumns = ({
ellipsis: { ellipsis: {
showTitle: false showTitle: false
}, },
render: (_text, record) => ( render: (_text, record) => {
return (
<DropdownButtons <DropdownButtons
items={rowActionList} items={buildRowActions(record)}
onSelect={(val) => handleSelect(val, record)} onSelect={(val) => handleSelect(val, record)}
/> />
) );
}
} }
]; ];
}, [handleSelect, sortOrder, clusterList, intl, pvCapacityByName]); }, [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 useTableFetch from '@/hooks/use-table-fetch';
import { ProviderValueMap } from '@/pages/cluster-management/config'; import { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; 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 { useAccess, useIntl, useNavigate } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; 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 { useSetAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
@@ -16,11 +24,14 @@ import { queryGPUServiceStorage } from '../storage/apis';
import { import {
deleteGPUServiceInstance, deleteGPUServiceInstance,
GPU_SERVICE_INSTANCES_API, GPU_SERVICE_INSTANCES_API,
queryGPUServiceInstances queryGPUServiceInstances,
startGPUServiceInstance,
stopGPUServiceInstance
} from './apis'; } from './apis';
import AddModal from './components/add-modal'; import AddModal from './components/add-modal';
import ViewEventsModal from './components/view-events-modal'; import ViewEventsModal from './components/view-events-modal';
import ViewLogsModal from './components/view-logs-modal'; import ViewLogsModal from './components/view-logs-modal';
import { batchActionList } from './config';
import { FormData, ListItem } from './config/types'; import { FormData, ListItem } from './config/types';
import useCreateInstance from './hooks/use-create-instance'; import useCreateInstance from './hooks/use-create-instance';
import useInstancesColumns from './hooks/use-instances-columns'; 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) => { const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'view') { if (val === 'view') {
openViewInstanceModal(row); openViewInstanceModal(row);
} else if (val === 'edit') { } else if (val === 'edit') {
openEditInstanceModal(row); openEditInstanceModal(row);
} else if (val === 'delete') { } else if (val === 'delete') {
handleDelete({ ...row, name: row.name }); handleDelete({ ...row });
} else if (val === 'recreate') { } else if (val === 'recreate') {
openRecreateInstanceModal(row); openRecreateInstanceModal(row);
} else if (val === 'viewlog') { } else if (val === 'viewlog') {
openViewLogsModal(row); openViewLogsModal(row);
} else if (val === 'viewevent') { } else if (val === 'viewevent') {
openViewEventsModal(row); 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) => { const handleBatchActionSelect = useMemoizedFn((val: string) => {
if (val === 'delete') { if (val === 'delete') {
handleDeleteBatch(); handleDeleteBatch();
} else if (val === 'start') {
handleStartBatch();
} else if (val === 'stop') {
handleStopBatch();
} }
// start / stop are disabled until backend endpoints exist
}); });
const renderEmpty = (type?: string) => { const renderEmpty = (type?: string) => {
@@ -236,11 +330,31 @@ const GPUService: React.FC = () => {
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
rowSelection={rowSelection} rowSelection={rowSelection}
widths={{ input: 300 }} widths={{ input: 300 }}
handleClickPrimary={ right={
hasK8sCluster ? openCreateInstanceModal : undefined <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}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table
@@ -1,6 +1,6 @@
import { useQueryData } from '@gpustack/core-ui'; import { useQueryData } from '@gpustack/core-ui';
import React from 'react'; import React from 'react';
import { ceilMilliToCore, parseQuantityToGi } from '../../utils'; import { ceilMilliToCore, parseQuantity, parseQuantityToGi } from '../../utils';
import { queryGPUServiceInstanceTypes } from '../apis'; import { queryGPUServiceInstanceTypes } from '../apis';
import { getAcceleratorMax } from '../config'; import { getAcceleratorMax } from '../config';
import { InstanceTypeItem } from '../config/types'; import { InstanceTypeItem } from '../config/types';
@@ -28,12 +28,13 @@ export default function useQueryInstanceTypes() {
const isAvailable = (item: InstanceTypeItem) => { const isAvailable = (item: InstanceTypeItem) => {
if (!item.spec?.acceleratable) if (!item.spec?.acceleratable)
return { return {
maxAccelerator: 0, maxComputeUnitCount:
parseQuantity(item.status?.onceMaxRequest?.cpu || '0')?.num || 0, // CPU resource max request
available: true available: true
}; };
const max = getAcceleratorMax(item.status?.acceleratorTiers); const max = getAcceleratorMax(item.status?.tiers);
return { return {
maxAccelerator: max || 0, maxComputeUnitCount: max || 0,
available: (max || 0) > 0 available: (max || 0) > 0
}; };
}; };
@@ -54,7 +55,8 @@ export default function useQueryInstanceTypes() {
unitResourcesParsed: { unitResourcesParsed: {
cpu: ceilMilliToCore(item.spec?.unitResources?.cpu ?? null), cpu: ceilMilliToCore(item.spec?.unitResources?.cpu ?? null),
ram: parseQuantityToGi(item.spec?.unitResources?.ram ?? null) ram: parseQuantityToGi(item.spec?.unitResources?.ram ?? null)
} },
maxComputeUnitCount: remainingData.maxComputeUnitCount
}, },
status: { status: {
...item.status, ...item.status,
@@ -71,7 +73,7 @@ export default function useQueryInstanceTypes() {
: '' : ''
} }
}, },
maxAccelerator: remainingData.maxAccelerator,
disabled: !remainingData.available disabled: !remainingData.available
}; };
}); });