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.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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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':
|
||||||
'テンプレート名、イメージまたはマウントパスで検索',
|
'テンプレート名、イメージまたはマウントパスで検索',
|
||||||
|
|||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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':
|
||||||
'Поиск по имени шаблона, образу или пути монтирования',
|
'Поиск по имени шаблона, образу или пути монтирования',
|
||||||
|
|||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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': '偏好设置'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
color: var(--ant-color-text-tertiary);
|
||||||
font-size: 13px;
|
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 {
|
.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,91 +68,50 @@ 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 (
|
||||||
<>
|
<Meta>
|
||||||
<Title>
|
{isGPU && (
|
||||||
<Flex gap={8} align="center">
|
<>
|
||||||
<AutoTooltip ghost minWidth={20} maxWidth={200}>
|
<MetaItem
|
||||||
{renderName() || '-'}
|
show={isGPU}
|
||||||
</AutoTooltip>
|
showDot={false}
|
||||||
{acceleratable && manufacturer && (
|
icon="icon-gpu1"
|
||||||
<span
|
label={intl.formatMessage({ id: 'gpuservice.instance.memory' })}
|
||||||
style={{
|
value={formatMemoryDisplay(spec?.memory ?? undefined) ?? '-'}
|
||||||
color: 'var(--ant-color-text-tertiary)',
|
/>
|
||||||
fontWeight: 400
|
<MetaItem
|
||||||
}}
|
icon="icon-database"
|
||||||
>
|
label={intl.formatMessage(
|
||||||
<ThemeTag color={manufacturerColor} disabled={false}>
|
{
|
||||||
{manufacturer?.toUpperCase()}
|
id: 'common.max'
|
||||||
</ThemeTag>
|
},
|
||||||
</span>
|
{ count: '' }
|
||||||
)}
|
)}
|
||||||
</Flex>
|
value={`${spec.maxComputeUnitCount || 0}`}
|
||||||
</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">
|
|
||||||
<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>({
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,44 +243,42 @@ 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
|
: null,
|
||||||
: undefined,
|
ram: unitResourcesParsed?.ram?.value
|
||||||
ram: unitResourcesParsed?.ram?.value
|
? count * unitResourcesParsed?.ram?.value
|
||||||
? count * unitResourcesParsed?.ram?.value
|
: null
|
||||||
: undefined
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
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
|
||||||
fallbackCpu != null && fallbackCpu !== ''
|
? `${fallbackCpu}`
|
||||||
? `${fallbackCpu}`
|
: undefined,
|
||||||
: undefined,
|
ram: ramNum
|
||||||
ram:
|
? `${factor * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
|
||||||
accelerator > 0 && ramNum
|
: values.spec?.resources?.ram
|
||||||
? `${accelerator * ramNum}${unitResourcesParsed?.ram?.unit || ''}`
|
|
||||||
: 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'], []);
|
||||||
}
|
}
|
||||||
@@ -506,164 +523,172 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
segmentedTop={segmentedTop}
|
segmentedTop={segmentedTop}
|
||||||
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
|
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
|
||||||
>
|
>
|
||||||
<Form
|
<FormContext.Provider
|
||||||
name="gpuServiceInstanceForm"
|
value={{
|
||||||
form={form}
|
action: formAction,
|
||||||
disabled={disabled}
|
currentData: currentData,
|
||||||
onFinish={handleFinish}
|
isGPUType: isGPUType
|
||||||
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
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Basic action={formAction} disabled={disabled} />
|
<Form
|
||||||
<Form.Item name="clusterId" hidden>
|
name="gpuServiceInstanceForm"
|
||||||
<CInput.Input />
|
form={form}
|
||||||
</Form.Item>
|
disabled={disabled}
|
||||||
<CollapsePanel
|
onFinish={handleFinish}
|
||||||
activeKey={collapseKeys}
|
onFinishFailed={handleOnFinishFailed}
|
||||||
accordion={false}
|
preserve={false}
|
||||||
onChange={handleOnCollapseChange}
|
initialValues={{
|
||||||
items={[
|
spec: {
|
||||||
{
|
description: '',
|
||||||
key: TABKeysMap.INSTANCE_TYPE,
|
displayName: '',
|
||||||
label: intl.formatMessage({
|
image: '',
|
||||||
id: 'gpuservice.instance.section.type'
|
imagePullPolicy: DefaultImagePullPolicy,
|
||||||
}),
|
command: [],
|
||||||
forceRender: true,
|
ports: [],
|
||||||
children: (
|
env: [],
|
||||||
<InstanceTypeFormItem
|
volumeMount: '',
|
||||||
action={formAction}
|
resources: {
|
||||||
disabled={disabled}
|
accelerator: 1
|
||||||
selectedInstanceType={selectedInstanceType}
|
},
|
||||||
currentData={currentData as any}
|
volume: {
|
||||||
onceMaxRequest={onceMaxRequest}
|
ephemeral: {
|
||||||
noAvailableTypes={noAvailableInstanceTypes}
|
capacity: '50Gi'
|
||||||
onGPUCountChange={handleAcceleratorChange}
|
},
|
||||||
/>
|
persistent: {
|
||||||
)
|
name: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
enable_ssh: false,
|
||||||
key: TABKeysMap.TEMPLATE,
|
storageMode: StorageModeValueMap.Temporary
|
||||||
label: intl.formatMessage({
|
}}
|
||||||
id: 'gpuservice.instance.section.template'
|
>
|
||||||
}),
|
<Basic action={formAction} disabled={disabled} />
|
||||||
forceRender: true,
|
<Form.Item name="clusterId" hidden>
|
||||||
children: (
|
<CInput.Input />
|
||||||
<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>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button
|
<CollapsePanel
|
||||||
size="small"
|
activeKey={collapseKeys}
|
||||||
type="link"
|
accordion={false}
|
||||||
icon={<PlusOutlined />}
|
onChange={handleOnCollapseChange}
|
||||||
onClick={handleAddSSHKey}
|
items={[
|
||||||
>
|
|
||||||
{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={[
|
|
||||||
{
|
{
|
||||||
required: sshEnabled,
|
key: TABKeysMap.INSTANCE_TYPE,
|
||||||
message: getRuleMessage('select', 'gpuservice.publicKey')
|
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
|
<Flex align="center" justify="space-between">
|
||||||
disabled={disabled}
|
<Form.Item<FormData>
|
||||||
mode="multiple"
|
name="enable_ssh"
|
||||||
maxTagCount={'responsive'}
|
valuePropName="checked"
|
||||||
label={intl.formatMessage({
|
style={{ marginBottom: 8 }}
|
||||||
id: 'gpuservice.instance.ssh.assignKey'
|
>
|
||||||
|
<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={{
|
</Button>
|
||||||
wrapper: {
|
</Flex>
|
||||||
width: '100%'
|
|
||||||
}
|
<div className={instanceStyles.sshkeySelection}>
|
||||||
|
<Form.Item<FormData>
|
||||||
|
name={['spec', 'sshPublicKeys']}
|
||||||
|
style={{
|
||||||
|
marginBottom: 12
|
||||||
}}
|
}}
|
||||||
options={sshkeyOptions}
|
hidden={!sshEnabled}
|
||||||
></MultipleSelect>
|
normalize={(value) =>
|
||||||
</Form.Item>
|
Array.isArray(value)
|
||||||
</div>
|
? value?.map((item) => ({ name: item }))
|
||||||
</Form>
|
: []
|
||||||
|
}
|
||||||
|
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
|
<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>
|
||||||
<div style={{ flex: 1 }}>
|
{isGPU && (
|
||||||
<Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
|
<div style={{ flex: 1 }}>
|
||||||
<InputNumber
|
<Form.Item<FormData> name={['spec', 'resources', 'cpu']}>
|
||||||
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
|
<InputNumber
|
||||||
max={onceMaxRequest?.cpu ?? undefined}
|
label={'CPU'}
|
||||||
disabled={disabled || isGPU || action === PageAction.EDIT}
|
max={onceMaxRequest?.cpu ?? undefined}
|
||||||
/>
|
disabled={true}
|
||||||
</Form.Item>
|
/>
|
||||||
</div>
|
</Form.Item>
|
||||||
|
</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;
|
||||||
@@ -58,13 +75,13 @@ const formatResources = (
|
|||||||
if (!record.spec?.resources?.accelerator) {
|
if (!record.spec?.resources?.accelerator) {
|
||||||
return {
|
return {
|
||||||
cpu: record.spec?.resources?.cpu
|
cpu: record.spec?.resources?.cpu
|
||||||
? `${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)
|
||||||
: '-'
|
: '-'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -74,11 +91,11 @@ const formatResources = (
|
|||||||
const vram = formatMemoryDisplay((instanceTypeSpec.spec as any)?.memory);
|
const vram = formatMemoryDisplay((instanceTypeSpec.spec as any)?.memory);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cpu: resources.cpu ? `${resources.cpu}C` : '-',
|
cpu: resources.cpu ? `${resources.cpu} C` : '-',
|
||||||
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) => {
|
||||||
<DropdownButtons
|
return (
|
||||||
items={rowActionList}
|
<DropdownButtons
|
||||||
onSelect={(val) => handleSelect(val, record)}
|
items={buildRowActions(record)}
|
||||||
/>
|
onSelect={(val) => handleSelect(val, record)}
|
||||||
)
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [handleSelect, sortOrder, clusterList, intl, pvCapacityByName]);
|
}, [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 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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user