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