fix: gpu service issues

This commit is contained in:
jialin
2026-05-26 14:21:52 +08:00
committed by jialin
parent 88ab927755
commit 95e03d11c1
20 changed files with 187 additions and 256 deletions
+1 -1
View File
@@ -284,6 +284,6 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'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': 'Maximum {count}', 'common.max': 'Max {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+3 -2
View File
@@ -16,8 +16,8 @@ export default {
'gpuservice.template.containerDisk': 'Container Disk (GB)', 'gpuservice.template.containerDisk': 'Container Disk (GB)',
'gpuservice.template.memory': 'Memory (GB)', 'gpuservice.template.memory': 'Memory (GB)',
'gpuservice.instance.containerDisk.remaining': 'gpuservice.instance.containerDisk.remaining':
'Container Disk (Remaining {count} GB)', 'Container Disk (Max {count} GB)',
'gpuservice.instance.memory.remaining': 'Memory (Remaining {count} GB)', 'gpuservice.instance.memory.remaining': 'Memory (Max {count} GB)',
'gpuservice.template.displayName': 'Display Name', 'gpuservice.template.displayName': 'Display Name',
'gpuservice.template.displayName.max': 'gpuservice.template.displayName.max':
'Display name cannot exceed 63 characters.', 'Display name cannot exceed 63 characters.',
@@ -99,6 +99,7 @@ export default {
'gpuservice.instance.sliced': 'Sliced', 'gpuservice.instance.sliced': 'Sliced',
'gpuservice.instance.memory': 'Memory', 'gpuservice.instance.memory': 'Memory',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk',
'gpuservice.instance.search.type.placeholder': 'Search by name', 'gpuservice.instance.search.type.placeholder': 'Search by name',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'Search by template name, image or mount path', 'Search by template name, image or mount path',
+1 -1
View File
@@ -284,7 +284,7 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'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': 'Maximum {count}', 'common.max': '最大 {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+3 -2
View File
@@ -16,8 +16,8 @@ export default {
'gpuservice.template.containerDisk': 'コンテナディスク (GB)', 'gpuservice.template.containerDisk': 'コンテナディスク (GB)',
'gpuservice.template.memory': 'メモリ (GB)', 'gpuservice.template.memory': 'メモリ (GB)',
'gpuservice.instance.containerDisk.remaining': 'gpuservice.instance.containerDisk.remaining':
'コンテナディスク (残り {count} GB)', 'コンテナディスク (最大 {count} GB)',
'gpuservice.instance.memory.remaining': 'メモリ (残り {count} GB)', 'gpuservice.instance.memory.remaining': 'メモリ (最大 {count} GB)',
'gpuservice.template.displayName': '表示名', 'gpuservice.template.displayName': '表示名',
'gpuservice.template.displayName.max': 'gpuservice.template.displayName.max':
'表示名は 63 文字以内で入力してください。', '表示名は 63 文字以内で入力してください。',
@@ -98,6 +98,7 @@ export default {
'gpuservice.instance.sliced': '分割', 'gpuservice.instance.sliced': '分割',
'gpuservice.instance.memory': 'Memory', 'gpuservice.instance.memory': 'Memory',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'ディスク',
'gpuservice.instance.search.type.placeholder': '名前で検索', 'gpuservice.instance.search.type.placeholder': '名前で検索',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'テンプレート名、イメージまたはマウントパスで検索', 'テンプレート名、イメージまたはマウントパスで検索',
+1 -1
View File
@@ -283,7 +283,7 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'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': 'Maximum {count}', 'common.max': 'Макс. {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+3 -2
View File
@@ -17,8 +17,8 @@ export default {
'gpuservice.template.containerDisk': 'Диск контейнера (GB)', 'gpuservice.template.containerDisk': 'Диск контейнера (GB)',
'gpuservice.template.memory': 'Память (GB)', 'gpuservice.template.memory': 'Память (GB)',
'gpuservice.instance.containerDisk.remaining': 'gpuservice.instance.containerDisk.remaining':
'Диск контейнера (Остаток {count} GB)', 'Диск контейнера (Макс. {count} GB)',
'gpuservice.instance.memory.remaining': 'Память (Остаток {count} GB)', 'gpuservice.instance.memory.remaining': 'Память (Макс. {count} GB)',
'gpuservice.template.displayName': 'Отображаемое имя', 'gpuservice.template.displayName': 'Отображаемое имя',
'gpuservice.template.displayName.max': 'gpuservice.template.displayName.max':
'Отображаемое имя не должно превышать 63 символа.', 'Отображаемое имя не должно превышать 63 символа.',
@@ -99,6 +99,7 @@ export default {
'gpuservice.instance.sliced': 'Разделено', 'gpuservice.instance.sliced': 'Разделено',
'gpuservice.instance.memory': 'Память', 'gpuservice.instance.memory': 'Память',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Диск',
'gpuservice.instance.search.type.placeholder': 'Поиск по имени', 'gpuservice.instance.search.type.placeholder': 'Поиск по имени',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'Поиск по имени шаблона, образу или пути монтирования', 'Поиск по имени шаблона, образу или пути монтирования',
+1 -1
View File
@@ -287,6 +287,6 @@ export default {
'common.image.limit.width': 'Image width must be {width}.', 'common.image.limit.width': 'Image width must be {width}.',
'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': 'Maksimum {count}', 'common.max': 'Maks. {count}',
'common.validate.group': 'Please complete the {group} configuration' 'common.validate.group': 'Please complete the {group} configuration'
}; };
+3 -2
View File
@@ -16,8 +16,8 @@ export default {
'gpuservice.template.containerDisk': 'Konteyner Diski (GB)', 'gpuservice.template.containerDisk': 'Konteyner Diski (GB)',
'gpuservice.template.memory': 'Bellek (GB)', 'gpuservice.template.memory': 'Bellek (GB)',
'gpuservice.instance.containerDisk.remaining': 'gpuservice.instance.containerDisk.remaining':
'Konteyner Diski (Kalan {count} GB)', 'Konteyner Diski (Maks. {count} GB)',
'gpuservice.instance.memory.remaining': 'Bellek (Kalan {count} GB)', 'gpuservice.instance.memory.remaining': 'Bellek (Maks. {count} GB)',
'gpuservice.template.displayName': 'Görünen Ad', 'gpuservice.template.displayName': 'Görünen Ad',
'gpuservice.template.displayName.max': 'gpuservice.template.displayName.max':
'Görünen ad 63 karakterden uzun olamaz.', 'Görünen ad 63 karakterden uzun olamaz.',
@@ -98,6 +98,7 @@ export default {
'gpuservice.instance.sliced': 'Bölünmüş', 'gpuservice.instance.sliced': 'Bölünmüş',
'gpuservice.instance.memory': 'Bellek', 'gpuservice.instance.memory': 'Bellek',
'gpuservice.instance.ram': 'RAM', 'gpuservice.instance.ram': 'RAM',
'gpuservice.instance.disk': 'Disk',
'gpuservice.instance.search.type.placeholder': 'Ada göre ara', 'gpuservice.instance.search.type.placeholder': 'Ada göre ara',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'Şablon adına, imaja veya bağlama yoluna göre ara', 'Şablon adına, imaja veya bağlama yoluna göre ara',
+1
View File
@@ -92,6 +92,7 @@ export default {
'gpuservice.instance.sliced': '切分', 'gpuservice.instance.sliced': '切分',
'gpuservice.instance.memory': '显存', 'gpuservice.instance.memory': '显存',
'gpuservice.instance.ram': '内存', 'gpuservice.instance.ram': '内存',
'gpuservice.instance.disk': '磁盘',
'gpuservice.instance.search.type.placeholder': '搜索名称', 'gpuservice.instance.search.type.placeholder': '搜索名称',
'gpuservice.instance.search.template.placeholder': 'gpuservice.instance.search.template.placeholder':
'搜索模板名称、镜像或挂载路径', '搜索模板名称、镜像或挂载路径',
@@ -46,13 +46,9 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
{ length: Math.max(0, maxCount) }, { length: Math.max(0, maxCount) },
(_, i) => i + 1 (_, i) => i + 1
); );
const computedItems = showCustomInput const items = presetItems;
? presetItems.filter((n) => n >= min && n <= max) const isItemDisabled = (num: number) =>
: Array.from( !!disabled || num > max || num < min;
{ length: Math.max(0, Math.floor((max - min) / step) + 1) },
(_, i) => min + i * step
);
const items = computedItems.length === 0 ? [0] : computedItems;
const [inputValue, setInputValue] = useState<number | null>(() => const [inputValue, setInputValue] = useState<number | null>(() =>
value !== undefined && value !== null && !presetItems.includes(value) value !== undefined && value !== null && !presetItems.includes(value)
? value ? value
@@ -70,7 +66,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
}, [value]); }, [value]);
const handleSelect = (num: number) => { const handleSelect = (num: number) => {
if (disabled || num === value) { if (isItemDisabled(num) || num === value) {
return; return;
} }
setInputValue(null); setInputValue(null);
@@ -108,15 +104,17 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
<Flex className={styles.contentWrapper} align="center"> <Flex className={styles.contentWrapper} align="center">
<div className={styles.content}> <div className={styles.content}>
{items.map((num) => { {items.map((num) => {
const itemDisabled = isItemDisabled(num);
return ( return (
<div <div
key={num} key={num}
role="radio" role="radio"
aria-checked={num === value} aria-checked={num === value}
aria-disabled={disabled} aria-disabled={itemDisabled}
tabIndex={disabled ? -1 : 0} tabIndex={itemDisabled ? -1 : 0}
className={classNames(styles.numberItem, { className={classNames(styles.numberItem, {
[styles.active]: num === value && !!value [styles.active]: num === value && !!value,
[styles.itemDisabled]: itemDisabled && !disabled
})} })}
onClick={() => handleSelect(num)} onClick={() => handleSelect(num)}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -85,6 +85,13 @@
font-weight: 500; font-weight: 500;
border-color: var(--ant-color-split); border-color: var(--ant-color-split);
} }
&.itemDisabled {
cursor: not-allowed;
color: var(--ant-color-text-disabled);
&:hover {
background-color: transparent;
}
}
} }
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
@@ -80,7 +80,6 @@ const MetaItem: React.FC<{
const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => { const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const intl = useIntl(); const intl = useIntl();
const name = item.name;
const specData = item.spec || {}; const specData = item.spec || {};
const acceleratable = specData.acceleratable; const acceleratable = specData.acceleratable;
@@ -93,11 +92,9 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
const remainingData = item.status?.remaining || {}; const remainingData = item.status?.remaining || {};
const renderName = () => { const renderName = () => {
const product = specData.product; const displayName = specData.acceleratable
const cpuCapacity = remainingData.cpu; ? specData.product || item.name
const displayName = : 'CPU';
product ||
(!acceleratable && cpuCapacity ? `${cpuCapacity} vCPUs` : name);
return displayName; return displayName;
}; };
@@ -162,7 +159,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({ item }) => {
value={toDisplayUnit(convertKiToGi(remainingData.ram)) ?? '-'} value={toDisplayUnit(convertKiToGi(remainingData.ram)) ?? '-'}
></MetaItem> ></MetaItem>
<MetaItem <MetaItem
show={acceleratable} show={!!remainingData.cpu}
showDot={true} showDot={true}
icon="icon-cpu" icon="icon-cpu"
label="vCPU" label="vCPU"
@@ -1,6 +1,6 @@
import { PageAction, validateLabelNameRegxFor63 } from '@/config'; import { PageAction, validateLabelNameRegxFor63 } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { Input as CInput } from '@gpustack/core-ui'; import { Input as CInput, useAppUtils } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field'; import OwnerPrincipalIdField from '../../../_components/owner-principal-id-field';
@@ -15,6 +15,7 @@ const Basic = ({
disabled?: boolean; disabled?: boolean;
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { getRuleMessage } = useAppUtils();
return ( return (
<> <>
<OwnerPrincipalIdField name="owner_principal_id" /> <OwnerPrincipalIdField name="owner_principal_id" />
@@ -24,9 +25,7 @@ const Basic = ({
rules={[ rules={[
{ {
required: true, required: true,
message: intl.formatMessage({ message: getRuleMessage('input', 'common.table.name')
id: 'gpuservice.instance.name.required'
})
}, },
{ {
pattern: validateLabelNameRegxFor63, pattern: validateLabelNameRegxFor63,
@@ -36,7 +35,7 @@ const Basic = ({
> >
<CInput.Input <CInput.Input
disabled={disabled || action === PageAction.EDIT} disabled={disabled || action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.instance.name' })} label={intl.formatMessage({ id: 'common.table.name' })}
required required
/> />
</Form.Item> </Form.Item>
@@ -540,10 +540,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
rules={[ rules={[
{ {
required: true, required: true,
message: getRuleMessage( message: getRuleMessage('select', 'gpuservice.publicKey')
'select',
'gpuservice.instance.ssh.assignKey'
)
} }
]} ]}
> >
@@ -97,9 +97,9 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
fontWeight: 400 fontWeight: 400
}} }}
> >
{description.manufacturer {description.acceleratable
? `${description.product} x ${currentData?.spec?.resources?.accelerator}` ? `${description.product} x ${currentData?.spec?.resources?.accelerator}`
: ''} : 'CPU'}
</span> </span>
</Flex> </Flex>
</SelectedCard> </SelectedCard>
@@ -185,7 +185,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
></Alert> ></Alert>
) )
} }
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} label={`${intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} (${intl.formatMessage(
{
id: 'common.max'
},
{ count: maxGpuCount }
)})`}
/> />
</Form.Item> </Form.Item>
)} )}
@@ -2,7 +2,6 @@ import { PageAction } from '@/config';
import { import {
Input as CInput, Input as CInput,
InputNumber as CInputNumber, InputNumber as CInputNumber,
CheckboxField,
LabelInfo, LabelInfo,
Select, Select,
useAppUtils useAppUtils
@@ -14,21 +13,17 @@ import { useEffect, useMemo, useState } from 'react';
import { FormData as StorageFormData } from '../../storage/config/types'; import { FormData as StorageFormData } from '../../storage/config/types';
import useCreateStorage from '../../storage/services/use-create-storage'; import useCreateStorage from '../../storage/services/use-create-storage';
import useQueryStorage from '../../storage/services/use-query-storage'; import useQueryStorage from '../../storage/services/use-query-storage';
import useQueryStorageTypes from '../../storage-types/services/use-query-storage-types'; import { StorageModeValueMap } from '../config';
import { DEFAULT_PV_CAPACITY_GB, StorageModeValueMap } from '../config';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import StorageOverlay from './storage-overlay'; import StorageOverlay from './storage-overlay';
const DEFAULT_TEMP_CAPACITY_GB = 50; const DEFAULT_TEMP_CAPACITY_GB = 50;
const detectMode = (volume?: FormData['spec']['volume']) => { const detectMode = (volume?: FormData['spec']['volume']) => {
if (volume?.persistentTemplate) { if (volume?.persistent || volume?.persistentTemplate) {
return { mode: StorageModeValueMap.Persistent, releaseWithInstance: true }; return StorageModeValueMap.Persistent;
} }
if (volume?.persistent) { return StorageModeValueMap.Temporary;
return { mode: StorageModeValueMap.Persistent, releaseWithInstance: false };
}
return { mode: StorageModeValueMap.Temporary, releaseWithInstance: false };
}; };
const StorageVolume = ({ const StorageVolume = ({
@@ -45,25 +40,19 @@ const StorageVolume = ({
const [storageMode, setStorageMode] = useState<string>( const [storageMode, setStorageMode] = useState<string>(
StorageModeValueMap.Temporary StorageModeValueMap.Temporary
); );
const [releaseWithInstance, setReleaseWithInstance] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (!currentVolume) return; if (!currentVolume) return;
const detected = detectMode(currentVolume); setStorageMode(detectMode(currentVolume));
setStorageMode(detected.mode);
setReleaseWithInstance(detected.releaseWithInstance);
}, [currentVolume]); }, [currentVolume]);
const { fetchData: createStorage } = useCreateStorage(); const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } = const { detailData: storageData, fetchData: fetchStorage } =
useQueryStorage(); useQueryStorage();
const { detailData: storageTypesData, fetchData: fetchStorageTypes } =
useQueryStorageTypes();
const [overlayOpen, setOverlayOpen] = useState(false); const [overlayOpen, setOverlayOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetchStorage({ page: 1, perPage: 100 }); fetchStorage({ page: 1, perPage: 100 });
fetchStorageTypes({ page: 1, perPage: 100 });
}, []); }, []);
const storageOptions = useMemo( const storageOptions = useMemo(
@@ -75,43 +64,19 @@ const StorageVolume = ({
[storageData] [storageData]
); );
const storageTypeOptions = useMemo( const applyMode = (mode: string) => {
() =>
(storageTypesData?.items || []).map((item) => ({
label: item.displayName || item.name,
value: item.name
})),
[storageTypesData]
);
const applyMode = (mode: string, release: boolean) => {
if (mode === StorageModeValueMap.Temporary) { if (mode === StorageModeValueMap.Temporary) {
form.setFieldValue(['spec', 'volume'], { form.setFieldValue(['spec', 'volume'], {
ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` } ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` }
}); });
return; return;
} }
if (release) {
form.setFieldValue(['spec', 'volume'], {
persistentTemplate: {
spec: { type: '', capacity: `${DEFAULT_PV_CAPACITY_GB}Gi` },
releaseWithInstance: true
}
});
return;
}
form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } }); form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } });
}; };
const handleModeChange = (mode: string) => { const handleModeChange = (mode: string) => {
setStorageMode(mode); setStorageMode(mode);
applyMode(mode, releaseWithInstance); applyMode(mode);
};
const handleReleaseChange = (e: any) => {
const next = !!e?.target?.checked;
setReleaseWithInstance(next);
applyMode(storageMode, next);
}; };
const handleCreateStorage = async (values: StorageFormData) => { const handleCreateStorage = async (values: StorageFormData) => {
@@ -130,48 +95,60 @@ const StorageVolume = ({
return ( return (
<> <>
<div data-field="storage"></div> <div data-field="storage"></div>
<Radio.Group <Flex align="center" justify="space-between" style={{ marginBlock: 16 }}>
disabled={disabled} <Radio.Group
style={{ marginBottom: 12 }} disabled={disabled}
value={storageMode} value={storageMode}
onChange={(e) => handleModeChange(e.target.value)} style={{ display: 'flex', gap: 12 }}
options={[ onChange={(e) => handleModeChange(e.target.value)}
{ options={[
label: ( {
<LabelInfo label: (
description={intl.formatMessage({ <LabelInfo
id: 'gpuservice.storage.temporary.tips' description={intl.formatMessage({
})} id: 'gpuservice.storage.temporary.tips'
label={ })}
<span className="text-primary"> label={
{intl.formatMessage({ <span className="text-primary">
id: 'gpuservice.storage.temporary' {intl.formatMessage({
})} id: 'gpuservice.storage.temporary'
</span> })}
} </span>
/> }
), />
value: StorageModeValueMap.Temporary ),
}, value: StorageModeValueMap.Temporary
{ },
label: ( {
<LabelInfo label: (
description={intl.formatMessage({ <LabelInfo
id: 'gpuservice.storage.persistentVolume.tips' description={intl.formatMessage({
})} id: 'gpuservice.storage.persistentVolume.tips'
label={ })}
<span className="text-primary"> label={
{intl.formatMessage({ <span className="text-primary">
id: 'gpuservice.storage.persistentVolume' {intl.formatMessage({
})} id: 'gpuservice.storage.persistentVolume'
</span> })}
} </span>
/> }
), />
value: StorageModeValueMap.Persistent ),
} value: StorageModeValueMap.Persistent
]} }
/> ]}
/>
{action === PageAction.CREATE &&
storageMode === StorageModeValueMap.Persistent && (
<Button
type="link"
size="small"
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
)}
</Flex>
{storageMode === StorageModeValueMap.Temporary && ( {storageMode === StorageModeValueMap.Temporary && (
<Form.Item <Form.Item
@@ -205,106 +182,26 @@ const StorageVolume = ({
{storageMode === StorageModeValueMap.Persistent && ( {storageMode === StorageModeValueMap.Persistent && (
<> <>
<Flex <Form.Item
style={{ name={['spec', 'volume', 'persistent', 'name']}
justifyContent: 'space-between', rules={[
alignItems: 'center', {
marginBottom: 12 required: true,
}} message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
> >
<CheckboxField <Select
disabled={disabled} disabled={disabled}
checked={releaseWithInstance}
onChange={handleReleaseChange}
label={intl.formatMessage({ label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.releaseWithInstance' id: 'gpuservice.storage.persistentVolume'
})} })}
required
options={storageOptions}
/> />
{action === PageAction.CREATE && !releaseWithInstance && ( </Form.Item>
<Button
type="link"
size="small"
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
)}
</Flex>
{releaseWithInstance ? (
<>
<Form.Item
name={['spec', 'volume', 'persistentTemplate', 'spec', 'type']}
rules={[
{
required: true,
message: getRuleMessage('select', 'gpuservice.storageType')
}
]}
>
<Select
required
disabled={disabled}
label={intl.formatMessage({ id: 'gpuservice.storageType' })}
options={storageTypeOptions}
/>
</Form.Item>
<Form.Item
name={[
'spec',
'volume',
'persistentTemplate',
'spec',
'capacity'
]}
getValueProps={(val) => ({
value: val
? Number(String(val).replace(/Gi$/i, '')) || undefined
: undefined
})}
normalize={(val) => (val ? `${val}Gi` : '')}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.capacity.required'
})
}
]}
>
<CInputNumber
required
disabled={disabled}
min={1}
precision={0}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.capacity'
})}
/>
</Form.Item>
</>
) : (
<Form.Item
name={['spec', 'volume', 'persistent', 'name']}
rules={[
{
required: true,
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
>
<Select
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
required
options={storageOptions}
/>
</Form.Item>
)}
</> </>
)} )}
@@ -13,6 +13,7 @@ import _ from 'lodash';
import { Fragment, useMemo } from 'react'; import { Fragment, useMemo } from 'react';
import { InstanceStatusLabelMap, rowActionList, status } from '../config'; import { InstanceStatusLabelMap, rowActionList, status } from '../config';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
import tableSyles from '../styles/table.module.less';
type ConnectEntry = type ConnectEntry =
| { | {
@@ -66,27 +67,6 @@ const getConnectEntries = (record: ListItem): ConnectEntry[] => {
}); });
}; };
const renderInstanceType = (record: ListItem) => {
const description = JSON.parse(record?.description || '{}').spec || {};
return (
<Flex align="flex-start" orientation="vertical">
<span
style={{
color: 'var(--ant-color-text)',
fontWeight: 400
}}
>
{description.manufacturer
? `${description.product} x ${record.spec?.resources?.accelerator}`
: ''}
</span>
<span className="text-tertiary">
{`${record.spec.resources?.cpu}C / RAM: ${record.spec.resources?.ram?.replace('Gi', 'GB')} / Disk: ${record.spec.resources?.localStorage?.replace('Gi', 'GB')}`}{' '}
</span>
</Flex>
);
};
interface ColumnsHookProps { interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void; handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[]; sortOrder: string[];
@@ -97,6 +77,36 @@ const useInstancesColumns = ({
sortOrder sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => { }: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl(); const intl = useIntl();
const renderInstanceType = (record: ListItem) => {
const description = JSON.parse(record?.description || '{}').spec || {};
return (
<Flex align="flex-start" orientation="vertical">
<span className="text-primary">
{description.acceleratable
? `${description.product} x ${record.spec?.resources?.accelerator}`
: 'CPU'}
</span>
<Flex
align="center"
style={{ fontSize: 13, color: 'var(--ant-color-text-tertiary)' }}
>
<span>{record.spec.resources?.cpu}C</span>
<span className={tableSyles.dot} />
<span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}:{' '}
{record.spec.resources?.ram?.replace('Gi', 'GB')}
</span>
<span className={tableSyles.dot} />
<span>
{intl.formatMessage({ id: 'gpuservice.instance.disk' })}:{' '}
{record.spec.resources?.localStorage?.replace('Gi', 'GB')}
</span>
</Flex>
</Flex>
);
};
return useMemo(() => { return useMemo(() => {
return [ return [
{ {
@@ -116,6 +126,9 @@ const useInstancesColumns = ({
{ {
title: intl.formatMessage({ id: 'gpuservice.instance.connect' }), title: intl.formatMessage({ id: 'gpuservice.instance.connect' }),
key: 'connect', key: 'connect',
ellipsis: {
showTitle: false
},
render: (_text, record: ListItem) => { render: (_text, record: ListItem) => {
const entries = getConnectEntries(record); const entries = getConnectEntries(record);
@@ -199,6 +212,9 @@ const useInstancesColumns = ({
dataIndex: ['status', 'phase'], dataIndex: ['status', 'phase'],
key: 'status', key: 'status',
sorter: false, sorter: false,
ellipsis: {
showTitle: false
},
render: (value: string, record: ListItem) => { render: (value: string, record: ListItem) => {
return ( return (
<StatusTag <StatusTag
@@ -212,7 +228,7 @@ const useInstancesColumns = ({
} }
}, },
{ {
title: 'Instance Type', title: intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
dataIndex: ['spec', 'type'], dataIndex: ['spec', 'type'],
key: 'type', key: 'type',
sorter: false, sorter: false,
@@ -220,12 +236,9 @@ const useInstancesColumns = ({
showTitle: false showTitle: false
}, },
render: (_text: string, record: ListItem) => ( render: (_text: string, record: ListItem) => (
<AutoTooltip <div style={{ width: 'max-content' }}>
ghost <AutoTooltip ghost>{renderInstanceType(record)}</AutoTooltip>
style={{ maxWidth: 360, color: 'var(--ant-color-text) !important' }} </div>
>
{renderInstanceType(record)}
</AutoTooltip>
) )
}, },
{ {
@@ -246,6 +259,9 @@ const useInstancesColumns = ({
title: intl.formatMessage({ id: 'common.table.operation' }), title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation', key: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
ellipsis: {
showTitle: false
},
render: (_text, record) => ( render: (_text, record) => (
<DropdownButtons <DropdownButtons
items={rowActionList} items={rowActionList}
@@ -0,0 +1,9 @@
.dot {
display: flex;
width: 3px;
height: 3px;
margin-inline: 5px;
border-radius: 50%;
background-color: var(--ant-color-text-quaternary);
flex: none;
}
@@ -150,6 +150,7 @@ const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({
<div className={styles.port}> <div className={styles.port}>
<CInputNumber <CInputNumber
min={PORT_MIN} min={PORT_MIN}
controls={false}
disabled={disabled} disabled={disabled}
precision={0} precision={0}
value={item.port} value={item.port}
@@ -2,17 +2,17 @@
display: flex; display: flex;
gap: 12px; gap: 12px;
width: 100%; width: 100%;
flex-wrap: wrap; // flex-wrap: wrap;
.select { .select {
flex: 1 1 120px; flex: 1 1 100px;
min-width: 120px; min-width: 100px;
} }
.port { .port {
flex: 1 1 120px; flex: 1 1 100px;
min-width: 120px; min-width: 100px;
} }
.name { .name {
flex: 100 1 180px; flex: 100 1 160px;
min-width: 180px; min-width: 160px;
} }
} }