fix: default instance type, storageMode
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -105,6 +105,8 @@ export default {
|
|||||||
'Please select at least {count} GPU card(s)',
|
'Please select at least {count} GPU card(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':
|
||||||
|
'CPU-only setup for environment preparation.',
|
||||||
'gpuservice.instance.stock': 'Stock',
|
'gpuservice.instance.stock': 'Stock',
|
||||||
'gpuservice.instance.sliced': 'Sliced',
|
'gpuservice.instance.sliced': 'Sliced',
|
||||||
'gpuservice.instance.memory': 'Memory',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export default {
|
|||||||
'少なくとも {count} 枚の GPU カードを選択してください',
|
'少なくとも {count} 枚の GPU カードを選択してください',
|
||||||
'gpuservice.instance.gpuCount.noAvailable':
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
|
'利用可能な GPU リソースがありません。別のインスタンスタイプを選択してください。',
|
||||||
|
'gpuservice.instance.gpuCount.zero': 'CPU のみを使用し、環境準備用です。',
|
||||||
'gpuservice.instance.stock': '在庫',
|
'gpuservice.instance.stock': '在庫',
|
||||||
'gpuservice.instance.sliced': '分割',
|
'gpuservice.instance.sliced': '分割',
|
||||||
'gpuservice.instance.memory': 'Memory',
|
'gpuservice.instance.memory': 'Memory',
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
|
'gpuservice.instance.gpuCount.min': 'Выберите минимум {count} GPU-карт',
|
||||||
'gpuservice.instance.gpuCount.noAvailable':
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
|
'Нет доступных ресурсов GPU, выберите другой тип экземпляра.',
|
||||||
|
'gpuservice.instance.gpuCount.zero': 'Только CPU, для подготовки окружения.',
|
||||||
'gpuservice.instance.stock': 'Остаток',
|
'gpuservice.instance.stock': 'Остаток',
|
||||||
'gpuservice.instance.sliced': 'Разделено',
|
'gpuservice.instance.sliced': 'Разделено',
|
||||||
'gpuservice.instance.memory': 'Память',
|
'gpuservice.instance.memory': 'Память',
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı seçin',
|
'gpuservice.instance.gpuCount.min': 'En az {count} GPU kartı 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.stock': 'Stok',
|
'gpuservice.instance.stock': 'Stok',
|
||||||
'gpuservice.instance.sliced': 'Bölünmüş',
|
'gpuservice.instance.sliced': 'Bölünmüş',
|
||||||
'gpuservice.instance.memory': 'Bellek',
|
'gpuservice.instance.memory': 'Bellek',
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default {
|
|||||||
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
|
'gpuservice.instance.gpuCount.min': '至少选择 {count} 张卡',
|
||||||
'gpuservice.instance.gpuCount.noAvailable':
|
'gpuservice.instance.gpuCount.noAvailable':
|
||||||
'没有可用的 GPU 资源,请选择其他实例类型。',
|
'没有可用的 GPU 资源,请选择其他实例类型。',
|
||||||
|
'gpuservice.instance.gpuCount.zero': '仅使用 CPU,用于环境准备。',
|
||||||
'gpuservice.instance.stock': '库存',
|
'gpuservice.instance.stock': '库存',
|
||||||
'gpuservice.instance.sliced': '切分',
|
'gpuservice.instance.sliced': '切分',
|
||||||
'gpuservice.instance.memory': '显存',
|
'gpuservice.instance.memory': '显存',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { LabelInfo } from '@gpustack/core-ui';
|
import { LabelInfo } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Flex, InputNumber } from 'antd';
|
import { Flex, InputNumber, Tooltip } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import styles from './styles.less';
|
import styles from './styles.less';
|
||||||
@@ -21,6 +22,7 @@ interface NumberSelectionProps {
|
|||||||
};
|
};
|
||||||
labelExtra?: React.ReactNode;
|
labelExtra?: React.ReactNode;
|
||||||
maxCount?: number;
|
maxCount?: number;
|
||||||
|
tips?: string;
|
||||||
onChange?: (value: number) => void;
|
onChange?: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
|||||||
labelExtra,
|
labelExtra,
|
||||||
className,
|
className,
|
||||||
maxCount = 8,
|
maxCount = 8,
|
||||||
|
tips,
|
||||||
style,
|
style,
|
||||||
onChange
|
onChange
|
||||||
}) => {
|
}) => {
|
||||||
@@ -108,25 +111,38 @@ const NumberSelection: React.FC<NumberSelectionProps> = ({
|
|||||||
{items.map((num) => {
|
{items.map((num) => {
|
||||||
const itemDisabled = isItemDisabled(num);
|
const itemDisabled = isItemDisabled(num);
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={num} style={{ flex: 1 }}>
|
||||||
key={num}
|
<Tooltip title={num === 0 ? tips : false}>
|
||||||
role="radio"
|
<div
|
||||||
aria-checked={num === value}
|
key={num}
|
||||||
aria-disabled={itemDisabled}
|
role="radio"
|
||||||
tabIndex={itemDisabled ? -1 : 0}
|
aria-checked={num === value}
|
||||||
className={classNames(styles.numberItem, {
|
aria-disabled={itemDisabled}
|
||||||
[styles.active]: num === value && value != null,
|
tabIndex={itemDisabled ? -1 : 0}
|
||||||
[styles.itemDisabled]: itemDisabled && !disabled
|
className={classNames(styles.numberItem, {
|
||||||
})}
|
[styles.active]: num === value && value != null,
|
||||||
onClick={() => handleSelect(num)}
|
[styles.itemDisabled]: itemDisabled && !disabled
|
||||||
onKeyDown={(e) => {
|
})}
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
onClick={() => handleSelect(num)}
|
||||||
e.preventDefault();
|
onKeyDown={(e) => {
|
||||||
handleSelect(num);
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
}
|
e.preventDefault();
|
||||||
}}
|
handleSelect(num);
|
||||||
>
|
}
|
||||||
{num}
|
}}
|
||||||
|
>
|
||||||
|
{num}
|
||||||
|
{num === 0 && (
|
||||||
|
<QuestionCircleOutlined
|
||||||
|
style={{
|
||||||
|
marginLeft: 4,
|
||||||
|
fontSize: 11,
|
||||||
|
color: 'var(--ant-color-text-tertiary)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -91,14 +91,13 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
detailData,
|
detailData: instanceTypeList,
|
||||||
loading: instanceTypesLoading,
|
loading: instanceTypesLoading,
|
||||||
fetchData
|
fetchData
|
||||||
} = useQueryInstanceTypes();
|
} = useQueryInstanceTypes();
|
||||||
const { detailData: templatesData, fetchData: fetchTemplates } =
|
const { detailData: templatesData, fetchData: fetchTemplates } =
|
||||||
useQueryTemplates();
|
useQueryTemplates();
|
||||||
|
|
||||||
const instanceTypeList = detailData?.items || [];
|
|
||||||
const templateList = templatesData?.items || [];
|
const templateList = templatesData?.items || [];
|
||||||
// const readonly = action === PageAction.VIEW;
|
// const readonly = action === PageAction.VIEW;
|
||||||
const readonly = false;
|
const readonly = false;
|
||||||
@@ -180,7 +179,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// initial
|
// initial for first
|
||||||
const applyAutoSelection = (
|
const applyAutoSelection = (
|
||||||
instanceTypes: InstanceTypeItem[],
|
instanceTypes: InstanceTypeItem[],
|
||||||
templates: TemplateItem[]
|
templates: TemplateItem[]
|
||||||
@@ -230,13 +229,12 @@ const AddModal: React.FC<AddModalProps> = ({
|
|||||||
|
|
||||||
if (action === PageAction.CREATE) {
|
if (action === PageAction.CREATE) {
|
||||||
const session = ++sessionRef.current;
|
const session = ++sessionRef.current;
|
||||||
Promise.all([
|
Promise.all([fetchData({ page: -1 }), fetchTemplates({ page: -1 })]).then(
|
||||||
fetchData({ page: 1, perPage: 100 }),
|
([instanceResItems, templatesRes]) => {
|
||||||
fetchTemplates({ page: -1 })
|
if (sessionRef.current !== session) return;
|
||||||
]).then(([instanceRes, templatesRes]) => {
|
applyAutoSelection(instanceResItems || [], templatesRes?.items || []);
|
||||||
if (sessionRef.current !== session) return;
|
}
|
||||||
applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [open, shouldAutoSelectResource, action]);
|
}, [open, shouldAutoSelectResource, action]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { TemplateCard } from '@gpustack/core-ui';
|
|||||||
import { Empty, Spin } from 'antd';
|
import { Empty, Spin } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { getAcceleratorMax } from '../config';
|
|
||||||
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
import { InstanceTypeItem as InstanceTypeItemModel } from '../config/types';
|
||||||
import InstanceTypeItem from './instance-type-item';
|
import InstanceTypeItem from './instance-type-item';
|
||||||
|
|
||||||
@@ -20,34 +19,17 @@ interface InstanceTypeListProps {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU types always have stock (no accelerator). Accelerated types are
|
|
||||||
// considered available when at least one tier offers onceMaxRequest > 0.
|
|
||||||
const isAvailable = (item: InstanceTypeItemModel) => {
|
|
||||||
if (!item.spec?.acceleratable) return true;
|
|
||||||
return getAcceleratorMax(item.status?.acceleratorTiers) > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
dataList = [],
|
dataList = [],
|
||||||
loading
|
loading
|
||||||
}) => {
|
}) => {
|
||||||
const handleSelect = (
|
const handleSelect = (item: InstanceTypeItemModel) => {
|
||||||
item: InstanceTypeItemModel & { disabled?: boolean }
|
|
||||||
) => {
|
|
||||||
if (item.disabled || value === item.name) return;
|
if (item.disabled || value === item.name) return;
|
||||||
onChange?.(item);
|
onChange?.(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterList = dataList.map((item) => {
|
|
||||||
const available = isAvailable(item);
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
disabled: !available
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dataList.length) {
|
if (!dataList.length) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +47,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TypeGrid>
|
<TypeGrid>
|
||||||
{filterList.map((item) => {
|
{dataList.map((item) => {
|
||||||
const name = item.name;
|
const name = item.name;
|
||||||
return (
|
return (
|
||||||
<TemplateCard
|
<TemplateCard
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface FormData {
|
|||||||
displayName?: string | null;
|
displayName?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
enable_ssh?: boolean;
|
enable_ssh?: boolean;
|
||||||
|
storageMode?: string;
|
||||||
spec: {
|
spec: {
|
||||||
type: string;
|
type: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { DefaultImagePullPolicy } from '../../templates/config';
|
|||||||
import TemplateBasicForm, {
|
import TemplateBasicForm, {
|
||||||
BasicResourceMax
|
BasicResourceMax
|
||||||
} from '../../templates/forms/basic';
|
} from '../../templates/forms/basic';
|
||||||
import { pickCandidateForAccelerator } from '../config';
|
import { pickCandidateForAccelerator, StorageModeValueMap } from '../config';
|
||||||
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';
|
||||||
@@ -142,11 +142,10 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
updateActiveKey
|
updateActiveKey
|
||||||
} = useScrollActiveChange({
|
} = useScrollActiveChange({
|
||||||
initalActiveKeys: [TABKeysMap.BASIC],
|
initalActiveKeys: [TABKeysMap.BASIC],
|
||||||
initialCollapseKeys: [
|
initialCollapseKeys:
|
||||||
TABKeysMap.INSTANCE_TYPE,
|
action === PageAction.EDIT
|
||||||
TABKeysMap.TEMPLATE,
|
? []
|
||||||
TABKeysMap.STORAGE
|
: [TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE]
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -166,10 +165,14 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasSSHPort && !form.getFieldValue('enable_ssh')) {
|
if (
|
||||||
|
hasSSHPort &&
|
||||||
|
!form.getFieldValue('enable_ssh') &&
|
||||||
|
action === PageAction.CREATE
|
||||||
|
) {
|
||||||
form.setFieldValue('enable_ssh', true);
|
form.setFieldValue('enable_ssh', true);
|
||||||
}
|
}
|
||||||
}, [hasSSHPort, form]);
|
}, [hasSSHPort, form, action]);
|
||||||
|
|
||||||
const segmentOptions = useMemo(
|
const segmentOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -272,6 +275,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
}));
|
}));
|
||||||
rawHandleOnFinishFailed({ ...errorInfo, errorFields });
|
rawHandleOnFinishFailed({ ...errorInfo, errorFields });
|
||||||
};
|
};
|
||||||
|
const detectMode = (volume?: FormData['spec']['volume']) => {
|
||||||
|
if (volume?.persistent?.name || volume?.persistentTemplate?.name) {
|
||||||
|
return StorageModeValueMap.Persistent;
|
||||||
|
}
|
||||||
|
return StorageModeValueMap.Temporary;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -286,7 +295,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
) {
|
) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...currentData,
|
...currentData,
|
||||||
enable_ssh: !!currentData?.spec?.sshPublicKeys?.length
|
enable_ssh: !!currentData?.spec?.sshPublicKeys?.length,
|
||||||
|
storageMode: detectMode(currentData?.spec?.volume)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [action, currentData, form, open, realAction, instanceTypeList]);
|
}, [action, currentData, form, open, realAction, instanceTypeList]);
|
||||||
@@ -297,14 +307,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
(item: any) => item?.protocol === 'TCP' && item?.port === SSH_PORT
|
||||||
);
|
);
|
||||||
|
|
||||||
if (values.enable_ssh && !submittedHasSSHPort) {
|
if (!submittedHasSSHPort) {
|
||||||
submittedPorts.push({
|
submittedPorts.push({
|
||||||
protocol: 'TCP',
|
protocol: 'TCP',
|
||||||
port: SSH_PORT,
|
port: SSH_PORT,
|
||||||
name: 'SSH'
|
name: 'SSH'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log('submit values', values, submittedPorts);
|
|
||||||
await onFinish({
|
await onFinish({
|
||||||
..._.omit(values, ['enable_ssh']),
|
..._.omit(values, ['enable_ssh']),
|
||||||
spec: {
|
spec: {
|
||||||
@@ -348,6 +357,14 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
navigate('/gpu-service/public-keys');
|
navigate('/gpu-service/public-keys');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnEnableSSHChange = (e: any) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
console.log('enable ssh change', checked);
|
||||||
|
if (!checked) {
|
||||||
|
form.setFieldValue(['spec', 'sshPublicKeys'], []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollSpyTabs
|
<ScrollSpyTabs
|
||||||
ref={scrollTabsRef}
|
ref={scrollTabsRef}
|
||||||
@@ -387,7 +404,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enable_ssh: false
|
enable_ssh: false,
|
||||||
|
storageMode: StorageModeValueMap.Temporary
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Basic action={formAction} disabled={disabled} />
|
<Basic action={formAction} disabled={disabled} />
|
||||||
@@ -451,19 +469,21 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: 8 }}
|
||||||
>
|
>
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
|
onChange={handleOnEnableSSHChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'gpuservice.instance.ssh.enable'
|
id: 'gpuservice.instance.ssh.enable'
|
||||||
})}
|
})}
|
||||||
></CheckboxField>
|
></CheckboxField>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{sshEnabled && (
|
{
|
||||||
<div className={instanceStyles.sshkeySelection}>
|
<div className={instanceStyles.sshkeySelection}>
|
||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name={['spec', 'sshPublicKeys']}
|
name={['spec', 'sshPublicKeys']}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: 12
|
marginBottom: 12
|
||||||
}}
|
}}
|
||||||
|
hidden={!sshEnabled}
|
||||||
normalize={(value) =>
|
normalize={(value) =>
|
||||||
Array.isArray(value)
|
Array.isArray(value)
|
||||||
? value?.map((item) => ({ name: item }))
|
? value?.map((item) => ({ name: item }))
|
||||||
@@ -476,7 +496,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
})}
|
})}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: sshEnabled,
|
||||||
message: getRuleMessage('select', 'gpuservice.publicKey')
|
message: getRuleMessage('select', 'gpuservice.publicKey')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
@@ -525,7 +545,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
|||||||
></MultipleSelect>
|
></MultipleSelect>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
</Form>
|
</Form>
|
||||||
</ScrollSpyTabs>
|
</ScrollSpyTabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,11 +82,15 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
const renderInstanceType = () => {
|
const renderInstanceType = () => {
|
||||||
const description = JSON.parse(currentData?.description || '{}').spec || {};
|
const description = JSON.parse(currentData?.description || '{}').spec || {};
|
||||||
return (
|
return (
|
||||||
<SelectedCard>
|
<SelectedCard
|
||||||
|
style={{
|
||||||
|
background: 'var(--ant-color-bg-container-disabled)',
|
||||||
|
color: 'var(--ant-color-text-disabled)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Flex align="flex-start" orientation="vertical">
|
<Flex align="flex-start" orientation="vertical">
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--ant-color-text)',
|
|
||||||
fontWeight: 400
|
fontWeight: 400
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -123,9 +127,9 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
<Form.Item<FormData>
|
<Form.Item<FormData>
|
||||||
name={['spec', 'resources', 'accelerator']}
|
name={['spec', 'resources', 'accelerator']}
|
||||||
hidden={action === PageAction.EDIT}
|
hidden={action === PageAction.EDIT}
|
||||||
normalize={(value) => (value ? _.toString(value) : undefined)}
|
normalize={(value) => (value != null ? _.toString(value) : undefined)}
|
||||||
getValueProps={(value) => ({
|
getValueProps={(value) => ({
|
||||||
value: value ? _.toNumber(value) : undefined
|
value: value != null ? _.toNumber(value) : undefined
|
||||||
})}
|
})}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
@@ -142,12 +146,12 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (num < 1) {
|
if (num < 0) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new Error(
|
new Error(
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: 'gpuservice.instance.gpuCount.min' },
|
{ id: 'gpuservice.instance.gpuCount.min' },
|
||||||
{ count: 1 }
|
{ count: 0 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -158,11 +162,14 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<NumberSelection
|
<NumberSelection
|
||||||
min={1}
|
min={0}
|
||||||
onChange={handleOnGPUCountChange}
|
onChange={handleOnGPUCountChange}
|
||||||
max={maxGpuCount}
|
max={maxGpuCount}
|
||||||
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={
|
labelExtra={
|
||||||
!maxGpuCount &&
|
!maxGpuCount &&
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Flex, Form, Radio } from 'antd';
|
import { Button, Flex, Form, Radio } from 'antd';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
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';
|
||||||
@@ -19,13 +18,6 @@ import StorageOverlay from './storage-overlay';
|
|||||||
|
|
||||||
const DEFAULT_TEMP_CAPACITY_GB = 50;
|
const DEFAULT_TEMP_CAPACITY_GB = 50;
|
||||||
|
|
||||||
const detectMode = (volume?: FormData['spec']['volume']) => {
|
|
||||||
if (volume?.persistent || volume?.persistentTemplate) {
|
|
||||||
return StorageModeValueMap.Persistent;
|
|
||||||
}
|
|
||||||
return StorageModeValueMap.Temporary;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StorageVolume = ({
|
const StorageVolume = ({
|
||||||
disabled,
|
disabled,
|
||||||
action
|
action
|
||||||
@@ -36,16 +28,7 @@ const StorageVolume = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getRuleMessage } = useAppUtils();
|
const { getRuleMessage } = useAppUtils();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const currentVolume = Form.useWatch(['spec', 'volume'], form);
|
const storageMode = Form.useWatch('storageMode', form);
|
||||||
const [storageMode, setStorageMode] = useState<string>(
|
|
||||||
StorageModeValueMap.Temporary
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentVolume) return;
|
|
||||||
setStorageMode(detectMode(currentVolume));
|
|
||||||
}, [currentVolume]);
|
|
||||||
|
|
||||||
const { fetchData: createStorage } = useCreateStorage();
|
const { fetchData: createStorage } = useCreateStorage();
|
||||||
const { detailData: storageData, fetchData: fetchStorage } =
|
const { detailData: storageData, fetchData: fetchStorage } =
|
||||||
useQueryStorage();
|
useQueryStorage();
|
||||||
@@ -81,7 +64,6 @@ const StorageVolume = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleModeChange = (mode: string) => {
|
const handleModeChange = (mode: string) => {
|
||||||
setStorageMode(mode);
|
|
||||||
applyMode(mode);
|
applyMode(mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,49 +81,55 @@ const StorageVolume = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div data-field="storage"></div>
|
<div data-field="storage"></div>
|
||||||
<Flex align="center" justify="space-between" style={{ marginBlock: 16 }}>
|
<Flex
|
||||||
<Radio.Group
|
align="center"
|
||||||
disabled={disabled}
|
justify="space-between"
|
||||||
value={storageMode}
|
style={{ marginBottom: 16, paddingTop: 8 }}
|
||||||
style={{ display: 'flex', gap: 12 }}
|
>
|
||||||
onChange={(e) => handleModeChange(e.target.value)}
|
<Form.Item name="storageMode" noStyle>
|
||||||
options={[
|
<Radio.Group
|
||||||
{
|
disabled={disabled}
|
||||||
label: (
|
value={storageMode}
|
||||||
<LabelInfo
|
style={{ display: 'flex', gap: 12 }}
|
||||||
description={intl.formatMessage({
|
onChange={(e) => handleModeChange(e.target.value)}
|
||||||
id: 'gpuservice.storage.temporary.tips'
|
options={[
|
||||||
})}
|
{
|
||||||
label={
|
label: (
|
||||||
<span className="text-primary">
|
<LabelInfo
|
||||||
{intl.formatMessage({
|
description={intl.formatMessage({
|
||||||
id: 'gpuservice.storage.temporary'
|
id: 'gpuservice.storage.temporary.tips'
|
||||||
})}
|
})}
|
||||||
</span>
|
label={
|
||||||
}
|
<span className="text-primary">
|
||||||
/>
|
{intl.formatMessage({
|
||||||
),
|
id: 'gpuservice.storage.temporary'
|
||||||
value: StorageModeValueMap.Temporary
|
})}
|
||||||
},
|
</span>
|
||||||
{
|
}
|
||||||
label: (
|
/>
|
||||||
<LabelInfo
|
),
|
||||||
description={intl.formatMessage({
|
value: StorageModeValueMap.Temporary
|
||||||
id: 'gpuservice.storage.persistentVolume.tips'
|
},
|
||||||
})}
|
{
|
||||||
label={
|
label: (
|
||||||
<span className="text-primary">
|
<LabelInfo
|
||||||
{intl.formatMessage({
|
description={intl.formatMessage({
|
||||||
id: 'gpuservice.storage.persistentVolume'
|
id: 'gpuservice.storage.persistentVolume.tips'
|
||||||
})}
|
})}
|
||||||
</span>
|
label={
|
||||||
}
|
<span className="text-primary">
|
||||||
/>
|
{intl.formatMessage({
|
||||||
),
|
id: 'gpuservice.storage.persistentVolume'
|
||||||
value: StorageModeValueMap.Persistent
|
})}
|
||||||
}
|
</span>
|
||||||
]}
|
}
|
||||||
/>
|
/>
|
||||||
|
),
|
||||||
|
value: StorageModeValueMap.Persistent
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
{action === PageAction.CREATE &&
|
{action === PageAction.CREATE &&
|
||||||
storageMode === StorageModeValueMap.Persistent && (
|
storageMode === StorageModeValueMap.Persistent && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ type ConnectEntry =
|
|||||||
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
||||||
const ip = record.status?.hostIPs?.[0]?.ip;
|
const ip = record.status?.hostIPs?.[0]?.ip;
|
||||||
const ports = record.status?.ports || [];
|
const ports = record.status?.ports || [];
|
||||||
const hasSshKey = !!record.spec?.sshPublicKeys?.length;
|
|
||||||
const configPorts = record.spec?.ports || [];
|
const configPorts = record.spec?.ports || [];
|
||||||
|
|
||||||
if (!ip) {
|
if (!ip) {
|
||||||
@@ -45,7 +44,9 @@ const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
|||||||
return ports
|
return ports
|
||||||
.filter((p) => p.nodePort)
|
.filter((p) => p.nodePort)
|
||||||
.map<ConnectEntry>((p) => {
|
.map<ConnectEntry>((p) => {
|
||||||
const isSsh = hasSshKey && p.protocol === 'TCP' && p.port === 22;
|
const isSsh =
|
||||||
|
(p.protocol === 'TCP' && p.port === 22) ||
|
||||||
|
_.includes(_.toLower(p.name), 'ssh');
|
||||||
return isSsh
|
return isSsh
|
||||||
? {
|
? {
|
||||||
type: 'ssh',
|
type: 'ssh',
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { PageAction } from '@/config';
|
import { PageAction } from '@/config';
|
||||||
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||||
import useTableFetch from '@/hooks/use-table-fetch';
|
import useTableFetch from '@/hooks/use-table-fetch';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
|
||||||
import {
|
|
||||||
DeleteModal,
|
|
||||||
DropdownButtons,
|
|
||||||
FilterBar,
|
|
||||||
IconFont,
|
|
||||||
NoResult
|
|
||||||
} from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Button, ConfigProvider, message, Modal, Space, Table } from 'antd';
|
import { ConfigProvider, message, Modal, Table } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PageBox from '../../_components/page-box';
|
import PageBox from '../../_components/page-box';
|
||||||
import {
|
import {
|
||||||
@@ -22,7 +15,6 @@ import {
|
|||||||
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';
|
||||||
@@ -164,29 +156,9 @@ const GPUService: React.FC = () => {
|
|||||||
handleInputChange={handleNameChange}
|
handleInputChange={handleNameChange}
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
widths={{ input: 300 }}
|
widths={{ input: 300 }}
|
||||||
right={
|
handleClickPrimary={openCreateInstanceModal}
|
||||||
<Space size={16}>
|
buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })}
|
||||||
<Button
|
handleDeleteByBatch={handleDeleteBatch}
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="primary"
|
|
||||||
onClick={openCreateInstanceModal}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: 'gpuservice.instance.add' })}
|
|
||||||
</Button>
|
|
||||||
<DropdownButtons
|
|
||||||
items={batchActionList}
|
|
||||||
extra={
|
|
||||||
rowSelection.selectedRowKeys.length > 0 && (
|
|
||||||
<span>({rowSelection.selectedRowKeys.length})</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size="large"
|
|
||||||
showText={true}
|
|
||||||
disabled={!rowSelection.selectedRowKeys.length}
|
|
||||||
onSelect={handleBatchActionSelect}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { useQueryData } from '@gpustack/core-ui';
|
import { useQueryData } from '@gpustack/core-ui';
|
||||||
import { useCallback } from 'react';
|
import React from 'react';
|
||||||
import { queryGPUServiceInstanceTypes } from '../apis';
|
import { queryGPUServiceInstanceTypes } from '../apis';
|
||||||
|
import { getAcceleratorMax } from '../config';
|
||||||
import { InstanceTypeItem } from '../config/types';
|
import { InstanceTypeItem } from '../config/types';
|
||||||
|
|
||||||
|
type InstanceType = InstanceTypeItem & {
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default function useQueryInstanceTypes() {
|
export default function useQueryInstanceTypes() {
|
||||||
const fetchDetail = useCallback(
|
const fetchDetail = (
|
||||||
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
|
params: Global.SearchParams = { page: 1, perPage: 100 },
|
||||||
queryGPUServiceInstanceTypes(params, options),
|
options?: any
|
||||||
[]
|
) => queryGPUServiceInstanceTypes(params, options);
|
||||||
);
|
|
||||||
|
|
||||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||||
Global.PageResponse<InstanceTypeItem>,
|
Global.PageResponse<InstanceTypeItem>,
|
||||||
@@ -18,10 +22,31 @@ export default function useQueryInstanceTypes() {
|
|||||||
key: 'instanceTypes'
|
key: 'instanceTypes'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [dataList, setDataList] = React.useState<InstanceType[]>([]);
|
||||||
|
|
||||||
|
const isAvailable = (item: InstanceTypeItem) => {
|
||||||
|
if (!item.spec?.acceleratable) return true;
|
||||||
|
return getAcceleratorMax(item.status?.acceleratorTiers) > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryInstanceTypes = async (
|
||||||
|
params: Global.SearchParams = { page: -1 }
|
||||||
|
) => {
|
||||||
|
const res = await fetchData(params);
|
||||||
|
|
||||||
|
const list = (res?.items || []).map((item) => ({
|
||||||
|
...item,
|
||||||
|
disabled: !isAvailable(item)
|
||||||
|
}));
|
||||||
|
console.log('queryInstanceTypes', list);
|
||||||
|
setDataList(list);
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
detailData,
|
detailData: dataList,
|
||||||
loading,
|
loading,
|
||||||
cancelRequest,
|
cancelRequest,
|
||||||
fetchData
|
fetchData: queryInstanceTypes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const GPUServicePublicKeys: React.FC = () => {
|
|||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
image={<IconFont type="icon-storage-outlined" />}
|
image={<IconFont type="icon-ssh-outlined" />}
|
||||||
filters={_.pick(queryParams, ['search'])}
|
filters={_.pick(queryParams, ['search'])}
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.gpuservice.sshkey.nofound'
|
id: 'noresult.gpuservice.sshkey.nofound'
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const GPUServiceStorage: React.FC = () => {
|
|||||||
loading={dataSource.loading}
|
loading={dataSource.loading}
|
||||||
loadend={dataSource.loadend}
|
loadend={dataSource.loadend}
|
||||||
dataSource={dataSource.dataList}
|
dataSource={dataSource.dataList}
|
||||||
image={<IconFont type="icon-storage-outlined" />}
|
image={<IconFont type="icon-database-outlined" />}
|
||||||
filters={_.pick(queryParams, ['search'])}
|
filters={_.pick(queryParams, ['search'])}
|
||||||
noFoundText={intl.formatMessage({
|
noFoundText={intl.formatMessage({
|
||||||
id: 'noresult.gpuservice.storage.nofound'
|
id: 'noresult.gpuservice.storage.nofound'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ascendLogo from '@/assets/logo/ascend.png';
|
|||||||
import CambriconPNG from '@/assets/logo/cambricon.png';
|
import CambriconPNG from '@/assets/logo/cambricon.png';
|
||||||
import hyponPNG from '@/assets/logo/hygon.png';
|
import hyponPNG from '@/assets/logo/hygon.png';
|
||||||
import iluvatarWEBP from '@/assets/logo/Iluvatar.png';
|
import iluvatarWEBP from '@/assets/logo/Iluvatar.png';
|
||||||
import jupyterLogo from '@/assets/logo/jupyter.png';
|
import jupyterLogo from '@/assets/logo/jupyter_logo.png';
|
||||||
import metaxLogo from '@/assets/logo/metax.png';
|
import metaxLogo from '@/assets/logo/metax.png';
|
||||||
import mooreLogo from '@/assets/logo/moore-logo.png';
|
import mooreLogo from '@/assets/logo/moore-logo.png';
|
||||||
import nvidiaLogo from '@/assets/logo/nvidia.png';
|
import nvidiaLogo from '@/assets/logo/nvidia.png';
|
||||||
@@ -11,7 +11,7 @@ import sgLangLogo from '@/assets/logo/sglang.png';
|
|||||||
import theadLogoEN from '@/assets/logo/t-head-en.png';
|
import theadLogoEN from '@/assets/logo/t-head-en.png';
|
||||||
import theadLogoZH from '@/assets/logo/t-head-zh.png';
|
import theadLogoZH from '@/assets/logo/t-head-zh.png';
|
||||||
import tensorflowkLogo from '@/assets/logo/tensorflow.svg';
|
import tensorflowkLogo from '@/assets/logo/tensorflow.svg';
|
||||||
import ubuntuLogo from '@/assets/logo/ubuntu.png';
|
import ubuntuLogo from '@/assets/logo/ubuntu_logo.png';
|
||||||
import vllmLogo from '@/assets/logo/vllm.png';
|
import vllmLogo from '@/assets/logo/vllm.png';
|
||||||
import {
|
import {
|
||||||
GPUsConfigs,
|
GPUsConfigs,
|
||||||
@@ -50,7 +50,9 @@ const manufacturerLabelMap: Record<string, string> = Object.values(
|
|||||||
{ cpu: 'CPU' } as Record<string, string>
|
{ cpu: 'CPU' } as Record<string, string>
|
||||||
);
|
);
|
||||||
|
|
||||||
const matchImageLogo = (image: string | undefined): string | null => {
|
const matchImageLogo = (
|
||||||
|
image: string | undefined
|
||||||
|
): { logo: string; type: string } | null => {
|
||||||
if (!image) return null;
|
if (!image) return null;
|
||||||
const lower = image.toLowerCase();
|
const lower = image.toLowerCase();
|
||||||
let matched: keyof typeof imageLogoMap | null = null;
|
let matched: keyof typeof imageLogoMap | null = null;
|
||||||
@@ -64,7 +66,12 @@ const matchImageLogo = (image: string | undefined): string | null => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return matched ? imageLogoMap[matched] : null;
|
return matched
|
||||||
|
? {
|
||||||
|
logo: imageLogoMap[matched],
|
||||||
|
type: matched
|
||||||
|
}
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledCard = styled(TemplateCard)`
|
const StyledCard = styled(TemplateCard)`
|
||||||
@@ -147,8 +154,13 @@ const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
|
|||||||
|
|
||||||
const renderLogo = () => {
|
const renderLogo = () => {
|
||||||
const imageLogo = matchImageLogo(data.spec?.image);
|
const imageLogo = matchImageLogo(data.spec?.image);
|
||||||
if (imageLogo) {
|
if (imageLogo?.logo) {
|
||||||
return <LogoImg src={imageLogo} height={22} />;
|
return (
|
||||||
|
<LogoImg
|
||||||
|
src={imageLogo.logo}
|
||||||
|
height={imageLogo.type === 'ubuntu' ? 26 : 22}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
switch (data.manufacturer) {
|
switch (data.manufacturer) {
|
||||||
case manfacturerValueMap.NVIDIA:
|
case manfacturerValueMap.NVIDIA:
|
||||||
|
|||||||
Reference in New Issue
Block a user