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