fix: ssh key, template test

This commit is contained in:
jialin
2026-05-11 19:49:21 +08:00
committed by jialin
parent 63ac4e0e75
commit fc900c8ce6
45 changed files with 1027 additions and 1090 deletions
@@ -1,7 +1,7 @@
import { request } from '@umijs/max';
import { FormData, ListItem } from '../config/types';
export const GPU_SERVICE_TEMPLATES_API = '/gpu-service-templates';
export const GPU_SERVICE_TEMPLATES_API = '/gpu-instance-templates';
export async function queryGPUServiceTemplates(
params: Global.SearchParams,
@@ -1,10 +1,21 @@
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 metaxLogo from '@/assets/logo/metax.png';
import mooreLogo from '@/assets/logo/moore-logo.png';
import nvidiaLogo from '@/assets/logo/nvidia.png';
import theadLogoEN from '@/assets/logo/t-head-en.png';
import theadLogoZH from '@/assets/logo/t-head-zh.png';
import { GPUDriverMap } from '@/pages/resources/config/gpu-driver';
import {
AutoTooltip,
DropdownActions,
IconFont,
TemplateCard
} from '@gpustack/core-ui';
import { Button } from 'antd';
import { useIntl } from '@umijs/max';
import { Button, Tag } from 'antd';
import styled from 'styled-components';
import { templateActions } from '../config';
import { ListItem } from '../config/types';
@@ -24,16 +35,21 @@ const Header = styled.div`
justify-content: space-between;
height: 24px;
width: 100%;
.title {
display: flex;
align-items: center;
}
`;
const CardTitle = styled.div`
const CardName = styled.div`
font-weight: 500;
font-size: 14px;
display: flex;
align-items: center;
color: var(--ant-color-text);
margin-bottom: 8px;
gap: 8px;
flex: 1;
width: 100%;
min-width: 0;
`;
@@ -61,12 +77,18 @@ const InfoItem = styled.div`
}
`;
const LogoImg = styled.img`
object-fit: contain;
`;
interface TemplateCardProps {
data: ListItem;
onSelect?: (item: { action: string; data: ListItem }) => void;
}
const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
const intl = useIntl();
const handleOnSelect = (item: any) => {
onSelect?.({ action: item.key, data });
};
@@ -75,6 +97,50 @@ const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
e.stopPropagation();
};
const renderLogo = () => {
switch (data.manufacturer) {
case GPUDriverMap.NVIDIA:
return <LogoImg src={nvidiaLogo} height={18} />;
case GPUDriverMap.AMD:
return (
<IconFont
type="icon-amd-logo"
style={{ fontSize: 36, color: 'var(--ant-color-text)' }}
/>
);
case GPUDriverMap.ASCEND:
return <LogoImg src={ascendLogo} height={24} />;
case GPUDriverMap.HYGON:
return <LogoImg src={hyponPNG} height={18} />;
case GPUDriverMap.METAX:
return <LogoImg src={metaxLogo} height={20} />;
case GPUDriverMap.MOORE_THREADS:
return <LogoImg src={mooreLogo} height={22} />;
case GPUDriverMap.ILUVATAR:
return <LogoImg src={iluvatarWEBP} height={22} />;
case GPUDriverMap.CAMBRICON:
return <LogoImg src={CambriconPNG} height={22} />;
case GPUDriverMap.THEAD:
return (
<LogoImg
src={intl?.locale === 'zh-CN' ? theadLogoZH : theadLogoEN}
height={20}
/>
);
case 'cpu':
return (
<Tag color="geekblue" style={{ paddingBlock: 2 }}>
<IconFont
type="icon-cpu"
style={{ fontSize: 22, display: 'flex' }}
/>
</Tag>
);
default:
return null;
}
};
const renderActions = () => {
return (
<span onClick={handleonClickAction} className="operations">
@@ -94,15 +160,6 @@ const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
);
};
const ports = data.ports || [];
const portText = ports.length
? ports.map((p) => `${p.protocol?.toUpperCase()}:${p.port}`).join(', ')
: '-';
const resources = [data.resources?.cpu, data.resources?.ram]
.filter(Boolean)
.join(' / ');
return (
<StyledCard
clickable={false}
@@ -112,46 +169,24 @@ const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
ghost
header={
<Header>
<CardTitle>
<AutoTooltip ghost minWidth={20}>
{data.name || '-'}
</AutoTooltip>
</CardTitle>
<div className="title">{renderLogo()}</div>
{renderActions()}
</Header>
}
>
<Content>
<InfoItem>
<span>
<IconFont className="icon" type="icon-model" /> :
</span>
<CardName>
<AutoTooltip ghost minWidth={20}>
<span className="value">{data.image || '-'}</span>
{data.name || '-'}
</AutoTooltip>
</InfoItem>
</CardName>
<InfoItem>
<span>
<IconFont className="icon" type="icon-storage-outlined" /> :
<IconFont className="icon" type="icon-model" />{' '}
{intl.formatMessage({ id: 'gpuservice.template.card.image' })}:
</span>
<AutoTooltip ghost minWidth={20}>
<span className="value">{data.volumeMount || '-'}</span>
</AutoTooltip>
</InfoItem>
<InfoItem>
<span>
<IconFont className="icon" type="icon-gpu1" /> :
</span>
<AutoTooltip ghost minWidth={20}>
<span className="value">{resources || '-'}</span>
</AutoTooltip>
</InfoItem>
<InfoItem>
<span>
<IconFont className="icon" type="icon-network" /> :
</span>
<AutoTooltip ghost minWidth={20}>
<span className="value">{portText}</span>
<span className="value">{data.spec?.image || '-'}</span>
</AutoTooltip>
</InfoItem>
</Content>
@@ -1,4 +1,5 @@
import { icons } from '@gpustack/core-ui';
import { ImagePullPolicy } from './types';
export const TemplateStatusValueMap = {
Enabled: 'enabled',
@@ -6,19 +7,45 @@ export const TemplateStatusValueMap = {
};
export const TemplateStatusLabelMap: Record<string, string> = {
[TemplateStatusValueMap.Enabled]: '启用',
[TemplateStatusValueMap.Disabled]: '禁用'
[TemplateStatusValueMap.Enabled]: 'common.button.enable',
[TemplateStatusValueMap.Disabled]: 'common.button.disable'
};
export const ImagePullPolicyOptions: {
label: string;
value: ImagePullPolicy;
locale: boolean;
}[] = [
{
label: 'gpuservice.template.imagePullPolicy.always',
value: 'Always',
locale: true
},
{
label: 'gpuservice.template.imagePullPolicy.ifNotPresent',
value: 'IfNotPresent',
locale: true
},
{
label: 'gpuservice.template.imagePullPolicy.never',
value: 'Never',
locale: true
}
];
export const DefaultImagePullPolicy: ImagePullPolicy = 'IfNotPresent';
export const templateActions = [
{
label: '编辑',
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: '删除',
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
danger: true
}
@@ -1,106 +0,0 @@
import { ListItem } from './types';
export const mockTemplateData: ListItem[] = [
{
id: 1,
name: 'Ubuntu CUDA Dev',
image: 'nvidia/cuda:12.4.1-devel-ubuntu22.04',
command: ['/bin/bash'],
volumeMount: '/workspace',
resources: {
cpu: '4',
ram: '16Gi'
},
ports: [
{ protocol: 'tcp', port: 22 },
{ protocol: 'udp', port: 8888 }
],
env: [{ name: 'NVIDIA_VISIBLE_DEVICES', value: 'all' }],
status: 'enabled',
created_at: '2026-04-01T10:00:00Z',
updated_at: '2026-04-10T10:00:00Z'
},
{
id: 2,
name: 'PyTorch Training',
image: 'pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel',
command: ['python', 'train.py'],
volumeMount: '/data',
resources: {
cpu: '8',
ram: '32Gi'
},
ports: [{ protocol: 'tcp', port: 6006 }],
env: [{ name: 'PYTHONUNBUFFERED', value: '1' }],
status: 'enabled',
created_at: '2026-04-02T10:00:00Z',
updated_at: '2026-04-11T10:00:00Z'
},
{
id: 3,
name: 'ROCm Notebook',
image: 'rocm/pytorch:latest',
command: ['jupyter', 'lab', '--ip=0.0.0.0', '--allow-root'],
volumeMount: '/notebooks',
resources: {
cpu: '4',
ram: '16Gi'
},
ports: [{ protocol: 'tcp', port: 8888 }],
env: [{ name: 'HSA_OVERRIDE_GFX_VERSION', value: '10.3.0' }],
status: 'enabled',
created_at: '2026-04-03T10:00:00Z',
updated_at: '2026-04-12T10:00:00Z'
},
{
id: 4,
name: 'Ascend MindIE',
image: 'ascend/mindie:latest',
command: ['/usr/local/Ascend/mindie/latest/bin/mindieservice_daemon'],
volumeMount: '/models',
resources: {
cpu: '4',
ram: '16Gi'
},
ports: [{ protocol: 'tcp', port: 1025 }],
env: [{ name: 'ASCEND_VISIBLE_DEVICES', value: '0' }],
status: 'enabled',
created_at: '2026-04-04T10:00:00Z',
updated_at: '2026-04-13T10:00:00Z'
},
{
id: 5,
name: 'CPU Utility',
image: 'ubuntu:22.04',
command: ['sleep', 'infinity'],
volumeMount: '/mnt/data',
resources: {
cpu: '2',
ram: '4Gi'
},
ports: [{ protocol: 'tcp', port: 22 }],
env: [],
status: 'disabled',
created_at: '2026-04-05T10:00:00Z',
updated_at: '2026-04-14T10:00:00Z'
},
{
id: 6,
name: 'Inference Server',
image: 'vllm/vllm-openai:latest',
command: ['python', '-m', 'vllm.entrypoints.openai.api_server'],
volumeMount: '/models',
resources: {
cpu: '16',
ram: '64Gi'
},
ports: [
{ protocol: 'udp', port: 8000 },
{ protocol: 'tcp', port: 8080 }
],
env: [{ name: 'VLLM_WORKER_MULTIPROC_METHOD', value: 'spawn' }],
status: 'enabled',
created_at: '2026-04-06T10:00:00Z',
updated_at: '2026-04-15T10:00:00Z'
}
];
+18 -13
View File
@@ -1,8 +1,10 @@
export interface PortItem {
protocol: 'udp' | 'tcp';
protocol: 'UDP' | 'TCP';
port: number;
}
export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never';
export interface EnvItem {
name: string;
value: string;
@@ -10,18 +12,21 @@ export interface EnvItem {
export interface FormData {
name: string;
image: string;
// imagePullPolicy: string;
// imagePullSecret: {
// name: string;
// };
command: string[];
ports: PortItem[];
env: EnvItem[];
volumeMount: string;
resources: {
cpu: string;
ram: string;
description: string;
manufacturer: string;
spec: {
image: string;
imagePullPolicy: ImagePullPolicy;
command: string[];
ports: PortItem[];
env: EnvItem[];
volumeMount: string;
resources: {
cpu: string;
ram: string;
localStorage: string;
accelerator: string;
};
};
}
+127 -42
View File
@@ -1,5 +1,16 @@
import { Input as CInput, Textarea } from '@gpustack/core-ui';
import { Form } from 'antd';
import {
Input as CInput,
InputNumber,
Select as SealSelect,
Textarea,
useAppUtils
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form } from 'antd';
import _ from 'lodash';
import { useMemo } from 'react';
import { GPUsConfigs } from '../../../resources/config/gpu-driver';
import { ImagePullPolicyOptions } from '../config';
import { FormData } from '../config/types';
import Env from './env';
import Ports from './ports';
@@ -9,72 +20,146 @@ interface BasicProps {
}
const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
const form = Form.useFormInstance<FormData>();
const { getRuleMessage } = useAppUtils();
const intl = useIntl();
const handleCommandChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const list = value
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0);
form.setFieldValue('command', list);
};
const command = Form.useWatch('command', form);
const commandText = Array.isArray(command) ? command.join('\n') : '';
const manufacturerOptions = useMemo(
() =>
Object.values(GPUsConfigs).map((item) => ({
label: item.label,
value: item.value
})),
[]
);
return (
<>
{page === 'template' && (
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: '请输入模板名称'
}
]}
>
<CInput.Input label="名称" required />
</Form.Item>
<>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: getRuleMessage('input', 'common.table.name')
}
]}
>
<CInput.Input
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
</>
)}
<Form.Item<FormData>
name="image"
name="manufacturer"
rules={[
{
required: true,
message: '请输入容器镜像'
message: getRuleMessage('select', 'resources.table.vendor')
}
]}
>
<CInput.Input label="容器镜像" required />
<SealSelect
label={intl.formatMessage({ id: 'resources.table.vendor' })}
required
options={[...manufacturerOptions, { label: 'CPU', value: 'cpu' }]}
/>
</Form.Item>
<Form.Item>
<Form.Item<FormData>
name={['spec', 'image']}
rules={[
{
required: true,
message: getRuleMessage('input', 'gpuservice.template.image')
}
]}
>
<CInput.Input
label={intl.formatMessage({ id: 'gpuservice.template.image' })}
required
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'imagePullPolicy']}>
<SealSelect
label={intl.formatMessage({
id: 'gpuservice.template.imagePullPolicy'
})}
options={ImagePullPolicyOptions.map((item) => ({
label: intl.formatMessage({ id: item.label }),
value: item.value
}))}
/>
</Form.Item>
<Form.Item<FormData>
name={['spec', 'command']}
normalize={(value: string) => _.split(value, '\n').map(_.trim)}
getValueProps={(value) => ({ value: _.join(value, '\n') })}
>
<Textarea
label="容器启动命令"
placeholder={'每行一个参数,例如:\n/bin/bash\n-c\nyour command'}
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
placeholder={intl.formatMessage({
id: 'gpuservice.template.command.placeholder'
})}
trim={false}
alwaysFocus
scaleSize
value={commandText}
onChange={handleCommandChange}
/>
</Form.Item>
<Form.Item<FormData> name="volumeMount">
<CInput.Input label="挂载路径" placeholder="例如:/workspace" />
</Form.Item>
<div style={{ display: 'flex', gap: 16 }}>
<Flex gap={16}>
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['resources', 'cpu']}>
<CInput.Input label="CPU" placeholder="例如:4" />
<Form.Item<FormData> name={['spec', 'volumeMount']}>
<CInput.Input
label={intl.formatMessage({ id: 'gpuservice.template.mountPath' })}
placeholder={intl.formatMessage({
id: 'clusters.volume.mountPath.format'
})}
/>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['resources', 'ram']}>
<CInput.Input label="内存" placeholder="例如:8Gi" />
<Form.Item<FormData>
name={['spec', 'resources', 'localStorage']}
normalize={(value) => (value ? `${value}Gi` : undefined)}
getValueProps={(value) => ({
value: value ? String(value).replace(/Gi$/, '') : ''
})}
>
<InputNumber
label={intl.formatMessage({
id: 'gpuservice.template.containerDisk'
})}
/>
</Form.Item>
</div>
</div>
</Flex>
<Flex gap={16}>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'resources', 'cpu']}
normalize={(value) => (value ? `${value}` : '')}
getValueProps={(value) => ({ value: value ? String(value) : '' })}
>
<InputNumber label="CPU" />
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'resources', 'ram']}
normalize={(value) => (value ? `${value}Gi` : undefined)}
getValueProps={(value) => ({
value: value ? String(value).replace(/Gi$/, '') : ''
})}
>
<InputNumber
label={intl.formatMessage({ id: 'gpuservice.template.memory' })}
/>
</Form.Item>
</div>
</Flex>
<Ports />
<Env />
</>
+21 -8
View File
@@ -1,4 +1,5 @@
import { Input as CInput, MetadataList } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { EnvItem as EnvItemType, FormData } from '../config/types';
@@ -9,12 +10,15 @@ interface EnvItemProps {
}
const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
const intl = useIntl();
return (
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
<div style={{ flex: 1 }}>
<CInput.Input
value={item.name}
placeholder="名称"
placeholder={intl.formatMessage({
id: 'gpuservice.template.env.name'
})}
onChange={(e) => {
onChange({
...item,
@@ -27,7 +31,9 @@ const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
<div style={{ flex: 1 }}>
<CInput.Input
value={item.value}
placeholder="值"
placeholder={intl.formatMessage({
id: 'gpuservice.template.env.value'
})}
onChange={(e) => {
onChange({
...item,
@@ -42,11 +48,12 @@ const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
};
const Env: React.FC = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const env = Form.useWatch('env', form) || [];
const env = Form.useWatch(['spec', 'env'], form) || [];
const updateEnv = (list: EnvItemType[]) => {
form.setFieldValue('env', list);
form.setFieldValue(['spec', 'env'], list);
};
const handleAdd = () => {
@@ -73,7 +80,7 @@ const Env: React.FC = () => {
return (
<Form.Item<FormData>
name="env"
name={['spec', 'env']}
rules={[
{
validator: async (_, value) => {
@@ -82,7 +89,13 @@ const Env: React.FC = () => {
}
const hasInvalid = value.some((item: EnvItemType) => !item.name);
if (hasInvalid) {
return Promise.reject(new Error('请填写完整的环境变量'));
return Promise.reject(
new Error(
intl.formatMessage({
id: 'gpuservice.template.env.invalid'
})
)
);
}
return Promise.resolve();
}
@@ -91,8 +104,8 @@ const Env: React.FC = () => {
>
<MetadataList
dataList={env}
btnText="添加环境变量"
label="环境变量"
btnText={intl.formatMessage({ id: 'gpuservice.template.env.add' })}
label={intl.formatMessage({ id: 'gpuservice.template.env' })}
onAdd={handleAdd}
onDelete={handleDelete}
>
+26 -22
View File
@@ -2,6 +2,7 @@ import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { DefaultImagePullPolicy } from '../config';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
@@ -26,32 +27,22 @@ const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
if (action === PageAction.EDIT && currentData) {
form.setFieldsValue({
name: currentData.name,
image: currentData.image,
command: currentData.command || [],
ports: currentData.ports || [],
env: currentData.env || [],
volumeMount: currentData.volumeMount,
resources: {
cpu: currentData.resources?.cpu,
ram: currentData.resources?.ram
}
...currentData
});
return;
}
form.setFieldsValue({
command: [],
ports: [
{
protocol: 'tcp',
port: 22
}
],
env: []
});
}, [action, currentData, form, open]);
const handleFinish = async (values: FormData) => {
await onFinish({
...values,
spec: {
...values.spec,
command: values.spec?.command?.filter(Boolean) ?? []
}
});
};
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
@@ -65,8 +56,21 @@ const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
<Form
name="gpuServiceTemplateForm"
form={form}
onFinish={onFinish}
onFinish={handleFinish}
preserve={false}
initialValues={{
spec: {
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [
{
protocol: 'TCP',
port: 22
}
],
env: []
}
}}
>
<Basic />
</Form>
+18 -10
View File
@@ -3,6 +3,7 @@ import {
MetadataList,
Select as SealSelect
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData, PortItem as PortItemType } from '../config/types';
@@ -11,11 +12,11 @@ type PortProtocol = PortItemType['protocol'];
const protocolOptions = [
{
label: 'UDP',
value: 'udp'
value: 'UDP'
},
{
label: 'TCP',
value: 'tcp'
value: 'TCP'
}
];
@@ -41,7 +42,7 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
style={{ width: '100%' }}
></SealSelect>
</div>
<div style={{ flex: 2 }}>
<div style={{ flex: 1 }}>
<CInputNumber
min={1}
max={65535}
@@ -61,18 +62,19 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
};
const Ports: React.FC = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const ports = Form.useWatch('ports', form) || [];
const ports = Form.useWatch(['spec', 'ports'], form) || [];
const updatePorts = (list: PortItemType[]) => {
form.setFieldValue('ports', list);
form.setFieldValue(['spec', 'ports'], list);
};
const handleAdd = () => {
updatePorts([
...ports,
{
protocol: 'tcp',
protocol: 'TCP',
port: undefined as any
}
]);
@@ -92,7 +94,7 @@ const Ports: React.FC = () => {
return (
<Form.Item<FormData>
name="ports"
name={['spec', 'ports']}
rules={[
{
validator: async (_, value) => {
@@ -103,7 +105,13 @@ const Ports: React.FC = () => {
(item: PortItemType) => !item.protocol || !item.port
);
if (hasInvalidPort) {
return Promise.reject(new Error('请填写完整的端口配置'));
return Promise.reject(
new Error(
intl.formatMessage({
id: 'gpuservice.template.ports.invalid'
})
)
);
}
return Promise.resolve();
}
@@ -112,8 +120,8 @@ const Ports: React.FC = () => {
>
<MetadataList
dataList={ports}
btnText="添加端口"
label="端口"
btnText={intl.formatMessage({ id: 'gpuservice.template.ports.add' })}
label={intl.formatMessage({ id: 'gpuservice.template.ports' })}
onAdd={handleAdd}
onDelete={handleDelete}
>
+55 -14
View File
@@ -7,9 +7,11 @@ import {
InfiniteScrollerProvider,
NoResult
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { message } from 'antd';
import _ from 'lodash';
import { useMemo } from 'react';
import { GPUsConfigs } from '../../resources/config/gpu-driver';
import PageBox from '../../_components/page-box';
import {
createGPUServiceTemplate,
@@ -24,6 +26,7 @@ import { FormData, ListItem } from './config/types';
import useCreateTemplate from './hooks/use-create-template';
const GPUServiceTemplates: React.FC = () => {
const intl = useIntl();
const {
dataSource,
rowSelection,
@@ -32,14 +35,15 @@ const GPUServiceTemplates: React.FC = () => {
fetchData,
handleDelete,
handleSearch,
handleNameChange
handleNameChange,
handleQueryChange
} = useTableFetch<ListItem>({
fetchAPI: queryGPUServiceTemplates,
deleteAPI: deleteGPUServiceTemplate,
API: GPU_SERVICE_TEMPLATES_API,
watch: false,
isInfiniteScroll: true,
contentForDelete: 'GPU 实例模板',
contentForDelete: intl.formatMessage({ id: 'gpuservice.template' }),
defaultQueryParams: {
perPage: 24
}
@@ -47,12 +51,37 @@ const GPUServiceTemplates: React.FC = () => {
const { openTemplateModalStatus, openTemplateModal, closeTemplateModal } =
useCreateTemplate();
const manufacturerOptions = useMemo(
() => [
...Object.values(GPUsConfigs).map((item) => ({
label: item.label,
value: item.value
})),
{ label: 'CPU', value: 'cpu' }
],
[]
);
const handleFilterByManufacturer = (value: string) => {
handleQueryChange({
manufacturer: value,
page: 1
});
};
const handleAddTemplate = () => {
openTemplateModal(PageAction.CREATE, '添加实例模板');
openTemplateModal(
PageAction.CREATE,
intl.formatMessage({ id: 'gpuservice.template.add' })
);
};
const handleEditTemplate = (row: ListItem) => {
openTemplateModal(PageAction.EDIT, '编辑实例模板', row);
openTemplateModal(
PageAction.EDIT,
intl.formatMessage({ id: 'gpuservice.template.edit' }),
row
);
};
const handleModalOk = async (data: FormData) => {
@@ -67,9 +96,8 @@ const GPUServiceTemplates: React.FC = () => {
}
closeTemplateModal();
handleSearch();
message.success('操作成功');
} catch (error) {
message.error('操作失败');
// ignore
}
};
@@ -101,13 +129,20 @@ const GPUServiceTemplates: React.FC = () => {
widths={{
input: 230
}}
inputHolder="按名称过滤"
buttonText="添加实例模板"
inputHolder={intl.formatMessage({
id: 'gpuservice.template.filter.name'
})}
selectHolder={intl.formatMessage({
id: 'gpuservice.template.filter.vendor'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.template.add' })}
handleClickPrimary={handleAddTemplate}
handleSearch={handleSearch}
handleSelectChange={handleFilterByManufacturer}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
showSelect={false}
showSelect={true}
selectOptions={manufacturerOptions}
/>
<InfiniteScrollerProvider
value={{
@@ -130,11 +165,17 @@ const GPUServiceTemplates: React.FC = () => {
dataSource={dataSource.dataList}
image={<IconFont type="icon-instance-template-filled" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的实例模板"
title="暂无实例模板"
subTitle="创建一个实例模板后会显示在这里"
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.template.nofound'
})}
title={intl.formatMessage({
id: 'noresult.gpuservice.template.title'
})}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.template.subTitle'
})}
onClick={handleAddTemplate}
buttonText="立即添加"
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
</InfiniteScrollerProvider>
<AddModal
@@ -0,0 +1,27 @@
import { useQueryData } from '@gpustack/core-ui';
import { useCallback } from 'react';
import { queryGPUServiceTemplates } from '../apis';
import { ListItem } from '../config/types';
export default function useQueryTemplates() {
const fetchDetail = useCallback(
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServiceTemplates(params, options),
[]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
Global.PageResponse<ListItem>,
Global.SearchParams
>({
fetchDetail,
key: 'templates'
});
return {
detailData,
loading,
cancelRequest,
fetchData
};
}