fix: default instance type, storageMode

This commit is contained in:
jialin
2026-05-26 22:40:02 +08:00
committed by jialin
parent a51f44c135
commit 2f04e01f4f
21 changed files with 213 additions and 185 deletions
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

+2
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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': 'Память',
+1
View File
@@ -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',
+1
View File
@@ -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;
+35 -15
View File
@@ -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',
+5 -33
View File
@@ -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
}; };
} }
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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: