feat: templates, storage

This commit is contained in:
jialin
2026-05-11 19:49:21 +08:00
committed by jialin
parent 3ae49dc826
commit 7ea7f6666d
52 changed files with 3604 additions and 188 deletions
@@ -0,0 +1,64 @@
import { request } from '@umijs/max';
import { mockTemplateData } from '../config/mock-data';
import { FormData, ListItem } from '../config/types';
export const GPU_SERVICE_TEMPLATES_API = '/gpu-service-templates';
export async function queryGPUServiceTemplates(
params: Global.SearchParams,
options?: any
) {
// return request<Global.PageResponse<ListItem>>(GPU_SERVICE_TEMPLATES_API, {
// method: 'GET',
// params,
// cancelToken: options?.token
// });
const page = params.page || 1;
const perPage = params.perPage || 24;
const search = params.search?.toLowerCase();
const vendor = params.vendor;
const filteredData = mockTemplateData.filter((item) => {
const matchSearch = search
? item.name.toLowerCase().includes(search)
: true;
const matchVendor = vendor ? item.vendor === vendor : true;
return matchSearch && matchVendor;
});
const start = (page - 1) * perPage;
const items = filteredData.slice(start, start + perPage);
return {
items,
pagination: {
total: filteredData.length,
totalPage: Math.ceil(filteredData.length / perPage),
page,
perPage
}
} as Global.PageResponse<ListItem>;
}
export async function createGPUServiceTemplate(params: { data: FormData }) {
// return request<ListItem>(GPU_SERVICE_TEMPLATES_API, {
// method: 'POST',
// data: params.data
// });
return true;
}
export async function updateGPUServiceTemplate(params: {
id: number;
data: FormData;
}) {
// return request<ListItem>(`${GPU_SERVICE_TEMPLATES_API}/${params.id}`, {
// method: 'PUT',
// data: params.data
// });
return true;
}
export async function deleteGPUServiceTemplate(id: number) {
return request(`${GPU_SERVICE_TEMPLATES_API}/${id}`, {
method: 'DELETE'
});
}
@@ -0,0 +1,72 @@
import { PageActionType } from '@/config/types';
import { ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServiceTemplateForm from '../forms';
type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
currentData?: ListItem | null;
onOk: (values: FormData) => void;
onCancel: () => void;
};
const AddModal: React.FC<AddModalProps> = ({
title,
action,
open,
currentData,
onOk,
onCancel
}) => {
const form = useRef<any>(null);
const handleSubmit = () => {
form.current?.submit();
};
const handleCancel = () => {
form.current?.resetFields();
onCancel();
};
const onFinish = async (values: FormData) => {
onOk({
...values
});
};
return (
<FormDrawer
title={title}
open={open}
onCancel={handleCancel}
onSubmit={handleSubmit}
width={600}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
style={{
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
}
>
<GPUServiceTemplateForm
ref={form}
action={action}
currentData={currentData}
onFinish={onFinish}
open={open}
/>
</FormDrawer>
);
};
export default AddModal;
@@ -0,0 +1,208 @@
import ascendLogo from '@/assets/logo/ascend.png';
import CambriconPNG from '@/assets/logo/cambricon.png';
import hygonPNG from '@/assets/logo/hygon.png';
import iluvatarLogo 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, GPUsConfigs } from '@/pages/resources/config/gpu-driver';
import {
AutoTooltip,
DropdownActions,
IconFont,
TemplateCard
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import styled from 'styled-components';
import { templateActions, TemplateStatusValueMap } from '../config';
import { ListItem } from '../config/types';
const StyledCard = styled(TemplateCard)`
&:hover {
.operations {
background-color: var(--ant-color-fill-tertiary);
border-radius: var(--ant-border-radius-lg);
}
}
`;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
height: 24px;
width: 100%;
`;
const VendorLogo = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
max-width: 88px;
height: 18px;
.logo-img {
max-width: 100%;
object-fit: contain;
}
.amd-logo {
font-size: 28px;
line-height: 1;
color: var(--ant-color-text);
}
`;
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;
`;
const Content = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: 12px;
gap: 8px;
color: var(--ant-color-text-secondary);
`;
const InfoItem = styled.div`
display: grid;
width: 100%;
grid-template-columns: max-content minmax(0, 1fr);
align-items: center;
gap: 8px;
color: var(--ant-color-text-tertiary);
.icon {
color: var(--ant-color-text-quaternary);
}
.value {
color: var(--ant-color-text);
}
`;
interface TemplateCardProps {
data: ListItem;
onSelect?: (item: { action: string; data: ListItem }) => void;
}
const vendorLogoMap: Record<string, { src: string; height: number }> = {
[GPUDriverMap.NVIDIA]: { src: nvidiaLogo, height: 16 },
[GPUDriverMap.ASCEND]: { src: ascendLogo, height: 18 },
[GPUDriverMap.HYGON]: { src: hygonPNG, height: 16 },
[GPUDriverMap.MOORE_THREADS]: { src: mooreLogo, height: 18 },
[GPUDriverMap.ILUVATAR]: { src: iluvatarLogo, height: 18 },
[GPUDriverMap.CAMBRICON]: { src: CambriconPNG, height: 18 },
[GPUDriverMap.METAX]: { src: metaxLogo, height: 18 }
};
const TemplateCardItem: React.FC<TemplateCardProps> = ({ data, onSelect }) => {
const intl = useIntl();
const vendorLabel = data.vendor
? GPUsConfigs[data.vendor]?.label || data.vendor
: '-';
const renderVendor = () => {
if (data.vendor === GPUDriverMap.AMD) {
return (
<VendorLogo title={vendorLabel}>
<IconFont className="amd-logo" type="icon-amd-logo" />
</VendorLogo>
);
}
const logo =
data.vendor === GPUDriverMap.THEAD
? {
src: intl.locale === 'zh-CN' ? theadLogoZH : theadLogoEN,
height: 18
}
: vendorLogoMap[data.vendor || ''];
if (!logo) {
return vendorLabel;
}
return (
<VendorLogo title={vendorLabel}>
<img
alt={vendorLabel}
className="logo-img"
src={logo.src}
style={{ height: logo.height }}
/>
</VendorLogo>
);
};
const handleOnSelect = (item: any) => {
onSelect?.({ action: item.key, data });
};
const handleonClickAction = (e: React.MouseEvent) => {
e.stopPropagation();
};
const renderActions = () => {
return (
<span onClick={handleonClickAction} className="operations">
<DropdownActions
menu={{
items: templateActions,
onClick: handleOnSelect
}}
>
<Button
icon={<IconFont type="icon-more" />}
size="small"
type="text"
/>
</DropdownActions>
</span>
);
};
const status = data.status || TemplateStatusValueMap.Enabled;
return (
<StyledCard
clickable={false}
hoverable={true}
disabled={false}
height={126}
ghost
header={
<Header>
{renderVendor()}
{renderActions()}
</Header>
}
>
<Content>
<CardName>
<AutoTooltip ghost minWidth={20}>
{data.name}
</AutoTooltip>
</CardName>
<InfoItem>
<span>
<IconFont className="icon" type="icon-model" /> :
</span>
<AutoTooltip ghost minWidth={20}>
<span className="value">{data.image || '-'}</span>
</AutoTooltip>
</InfoItem>
</Content>
</StyledCard>
);
};
export default TemplateCardItem;
@@ -0,0 +1,92 @@
import {
InfiniteScroller,
ResizeContainer,
TemplateCardSkeleton,
useScrollerContext
} from '@gpustack/core-ui';
import { Spin } from 'antd';
import React from 'react';
import backendListCss from '../../../backends/styles/backend-list.less';
import { ListItem } from '../config/types';
import TemplateCard from './template-card';
interface TemplateListProps {
dataList: ListItem[];
loading: boolean;
isFirst: boolean;
onSelect?: (item: { action: string; data: ListItem }) => void;
}
const ListSkeleton: React.FC<{
loading: boolean;
isFirst: boolean;
}> = ({ loading, isFirst }) => {
return (
<div>
{loading && (
<div className={backendListCss.SpinWrapper}>
<Spin
spinning={loading}
size="middle"
style={{
width: '100%'
}}
classNames={{
root: 'skelton-wrapper'
}}
>
{isFirst && (
<div className={backendListCss.SkeletonWrapper}>
<TemplateCardSkeleton
skeletonProps={{
title: false
}}
skeletonStyle={{
display: 'flex',
flexDirection: 'column',
gap: 24
}}
/>
</div>
)}
</Spin>
</div>
)}
</div>
);
};
const TemplateCardList: React.FC<TemplateListProps> = ({
dataList,
loading,
isFirst,
onSelect
}) => {
const {
total,
current,
loading: contextLoading,
refresh,
throttleDelay
} = useScrollerContext();
return (
<InfiniteScroller
total={total}
current={current}
loading={contextLoading}
refresh={refresh}
throttleDelay={throttleDelay}
>
<ResizeContainer
defaultSpan={8}
resizable={true}
dataList={dataList}
renderItem={(item) => <TemplateCard data={item} onSelect={onSelect} />}
/>
<ListSkeleton loading={loading} isFirst={isFirst} />
</InfiniteScroller>
);
};
export default TemplateCardList;
@@ -0,0 +1,25 @@
import { icons } from '@gpustack/core-ui';
export const TemplateStatusValueMap = {
Enabled: 'enabled',
Disabled: 'disabled'
};
export const TemplateStatusLabelMap: Record<string, string> = {
[TemplateStatusValueMap.Enabled]: '启用',
[TemplateStatusValueMap.Disabled]: '禁用'
};
export const templateActions = [
{
label: '编辑',
key: 'edit',
icon: icons.EditOutlined
},
{
label: '删除',
key: 'delete',
icon: icons.DeleteOutlined,
danger: true
}
];
@@ -0,0 +1,154 @@
import { ListItem } from './types';
export const mockTemplateData: ListItem[] = [
{
id: 1,
name: 'Ubuntu CUDA Dev',
image: 'nvidia/cuda:12.4.1-devel-ubuntu22.04',
vendor: 'cuda',
run_command: '/bin/bash',
boot_disk_size_gb: 30,
volume_size_gb: 100,
volume_mount_path: '/workspace',
ports: [
{
protocol: 'tcp',
value: 22
},
{
protocol: 'udp',
value: 8888
}
],
env: {
NVIDIA_VISIBLE_DEVICES: 'all'
},
gpu_count: 1,
replicas: 1,
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',
vendor: 'cuda',
run_command: 'python train.py',
boot_disk_size_gb: 50,
volume_size_gb: 200,
volume_mount_path: '/data',
ports: [
{
protocol: 'tcp',
value: 6006
}
],
env: {
PYTHONUNBUFFERED: '1'
},
gpu_count: 2,
replicas: 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',
vendor: 'rocm',
run_command: 'jupyter lab --ip=0.0.0.0 --allow-root',
boot_disk_size_gb: 40,
volume_size_gb: 120,
volume_mount_path: '/notebooks',
ports: [
{
protocol: 'http',
value: 8888
}
],
env: {
HSA_OVERRIDE_GFX_VERSION: '10.3.0'
},
gpu_count: 1,
replicas: 1,
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',
vendor: 'cann',
run_command: '/usr/local/Ascend/mindie/latest/bin/mindieservice_daemon',
boot_disk_size_gb: 60,
volume_size_gb: 160,
volume_mount_path: '/models',
ports: [
{
protocol: 'http',
value: 1025
}
],
env: {
ASCEND_VISIBLE_DEVICES: '0'
},
gpu_count: 1,
replicas: 1,
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',
vendor: 'cuda',
run_command: 'sleep infinity',
boot_disk_size_gb: 20,
volume_size_gb: 50,
volume_mount_path: '/mnt/data',
ports: [
{
protocol: 'tcp',
value: 22
}
],
env: {},
gpu_count: 0,
replicas: 1,
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',
vendor: 'cuda',
run_command: 'python -m vllm.entrypoints.openai.api_server',
boot_disk_size_gb: 80,
volume_size_gb: 300,
volume_mount_path: '/models',
ports: [
{
protocol: 'udp',
value: 8000
},
{
protocol: 'tcp',
value: 8080
}
],
env: {
VLLM_WORKER_MULTIPROC_METHOD: 'spawn'
},
gpu_count: 4,
replicas: 2,
status: 'enabled',
created_at: '2026-04-06T10:00:00Z',
updated_at: '2026-04-15T10:00:00Z'
}
];
@@ -0,0 +1,27 @@
export interface PortItem {
protocol: 'udp' | 'tcp';
value?: number;
}
export interface FormData {
name: string;
image?: string;
vendor?: string;
image_pull_policy?: string;
run_command?: string;
boot_disk_size_gb?: number;
volume_size_gb?: number;
volume_mount_path?: string;
ports?: PortItem[];
env?: Record<string, any>;
}
export interface ListItem extends FormData {
id: number;
description?: string;
gpu_count?: number;
replicas?: number;
status?: string;
created_at?: string;
updated_at?: string;
}
@@ -0,0 +1,111 @@
import { GPUsConfigs } from '@/pages/resources/config/gpu-driver';
import {
Input as CInput,
InputNumber as CInputNumber,
LabelSelector,
Select as SealSelect,
Textarea
} from '@gpustack/core-ui';
import { Form, Select } from 'antd';
import { FormData } from '../config/types';
import Ports from './ports';
const Basic = () => {
const form = Form.useFormInstance<FormData>();
const envs = Form.useWatch('env', form);
const gpuVendorOptions = Object.values(GPUsConfigs);
const handleEnvChange = (labels: Record<string, any>) => {
form.setFieldValue('env', labels);
};
return (
<>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: '请输入模板名称'
}
]}
>
<CInput.Input label="名称" required />
</Form.Item>
<Form.Item<FormData>
name="vendor"
rules={[
{
required: true,
message: '请选择适用设备厂商'
}
]}
>
<SealSelect label="适用设备厂商" required>
{gpuVendorOptions.map((item) => (
<Select.Option value={item.value} key={item.value}>
{item.label}
</Select.Option>
))}
</SealSelect>
</Form.Item>
<Form.Item<FormData>
name="image"
rules={[
{
required: true,
message: '请输入容器镜像'
}
]}
>
<CInput.Input label="容器镜像" required />
</Form.Item>
{/* <Form.Item<FormData> name="image_pull_policy">
<SealSelect
allowClear
label="镜像拉取策略"
options={[
{ label: 'IfNotPresent', value: 'IfNotPresent' },
{ label: 'Always', value: 'Always' },
{ label: 'Never', value: 'Never' }
]}
></SealSelect>
</Form.Item> */}
<Form.Item<FormData> name="run_command">
<Textarea
label="容器启动命令"
placeholder={'例如:/bin/bash -c "your command"'}
trim={false}
alwaysFocus
scaleSize
/>
</Form.Item>
<Form.Item<FormData> name="boot_disk_size_gb">
<CInputNumber min={1} precision={0} label="容器启动盘大小 (GB)" />
</Form.Item>
{/* <div style={{ display: 'flex', gap: 16 }}>
<div style={{ flex: 1 }}>
<Form.Item<FormData> name="volume_size_gb">
<CInputNumber min={1} precision={0} label="存储卷大小 (GB)" />
</Form.Item>
</div>
<div style={{ flex: 2 }}>
<Form.Item<FormData> name="volume_mount_path">
<CInput.Input label="存储卷挂载路径" placeholder="例如:/data" />
</Form.Item>
</div>
</div> */}
<Ports />
<Form.Item<FormData> name="env">
<LabelSelector
label="环境变量"
labels={envs || {}}
btnText="添加变量"
onChange={handleEnvChange}
/>
</Form.Item>
</>
);
};
export default Basic;
@@ -0,0 +1,85 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
interface TemplateFormProps {
ref?: any;
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>;
}
const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
(props, ref) => {
const { action, currentData, open, onFinish } = props;
const [form] = Form.useForm<FormData>();
useEffect(() => {
if (!open) {
form.resetFields();
return;
}
if (action === PageAction.EDIT && currentData) {
form.setFieldsValue({
name: currentData.name,
image: currentData.image,
vendor: currentData.vendor,
image_pull_policy: currentData.image_pull_policy,
run_command: currentData.run_command,
boot_disk_size_gb: currentData.boot_disk_size_gb,
volume_size_gb: currentData.volume_size_gb,
volume_mount_path: currentData.volume_mount_path,
ports: currentData.ports || [],
env: currentData.env || {}
});
return;
}
form.setFieldsValue({
ports: [
{
protocol: 'tcp',
value: 22
}
],
env: {}
});
}, [action, currentData, form, open]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
return (
<Form
name="gpuServiceTemplateForm"
form={form}
onFinish={onFinish}
preserve={false}
initialValues={{
boot_disk_size_gb: 30,
ports: [
{
protocol: 'udp',
value: 22
}
]
}}
>
<Basic />
</Form>
);
}
);
export default GPUServiceTemplateForm;
@@ -0,0 +1,133 @@
import {
InputNumber as CInputNumber,
MetadataList,
Select as SealSelect
} from '@gpustack/core-ui';
import { Form } from 'antd';
import { FormData, PortItem as PortItemType } from '../config/types';
type PortProtocol = PortItemType['protocol'];
const protocolOptions = [
{
label: 'UDP',
value: 'udp'
},
{
label: 'TCP',
value: 'tcp'
}
];
interface PortItemProps {
item: PortItemType;
index: number;
onChange: (item: PortItemType) => void;
}
const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
return (
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
<div style={{ flex: 1 }}>
<SealSelect
value={item.protocol}
options={protocolOptions}
onChange={(value) => {
onChange({
...item,
protocol: value as PortProtocol
});
}}
style={{ width: '100%' }}
></SealSelect>
</div>
<div style={{ flex: 2 }}>
<CInputNumber
min={1}
max={65535}
disabled={index === 0}
precision={0}
value={item.value}
onChange={(value) => {
onChange({
...item,
value: typeof value === 'number' ? value : undefined
});
}}
style={{ width: '100%' }}
/>
</div>
</div>
);
};
const Ports: React.FC = () => {
const form = Form.useFormInstance<FormData>();
const ports = Form.useWatch('ports', form) || [];
const updatePorts = (list: PortItemType[]) => {
form.setFieldValue('ports', list);
};
const handleAdd = () => {
updatePorts([
...ports,
{
protocol: 'tcp'
}
]);
};
const handleDelete = (index: number) => {
const newList = [...ports];
newList.splice(index, 1);
updatePorts(newList);
};
const handleChange = (index: number, item: PortItemType) => {
const newList = [...ports];
newList[index] = item;
updatePorts(newList);
};
return (
<Form.Item<FormData>
name="ports"
rules={[
{
validator: async (_, value) => {
if (!value?.length) {
return Promise.resolve();
}
const hasInvalidPort = value.some(
(item: PortItemType) => !item.protocol || !item.value
);
if (hasInvalidPort) {
return Promise.reject(new Error('请填写完整的端口配置'));
}
return Promise.resolve();
}
}
]}
>
<MetadataList
dataList={ports}
btnText="添加端口"
label="端口"
onAdd={handleAdd}
onDelete={handleDelete}
>
{(item, index) => (
<PortItem
key={index}
index={index}
item={item}
onChange={(data) => handleChange(index, data)}
/>
)}
</MetadataList>
</Form.Item>
);
};
export default Ports;
@@ -0,0 +1,48 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react';
import { ListItem } from '../config/types';
const useCreateTemplate = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean;
action: PageActionType;
currentData?: ListItem;
title: string;
}>({
open: false,
action: PageAction.CREATE,
currentData: undefined,
title: ''
});
const openModal = (action: PageActionType, title: string, row?: ListItem) => {
setOpenModalStatus({
open: true,
title,
action,
currentData: row
});
saveScrollHeight();
};
const closeModal = () => {
setOpenModalStatus({
open: false,
action: PageAction.CREATE,
currentData: undefined,
title: ''
});
restoreScrollHeight();
};
return {
openTemplateModalStatus: openModalStatus,
openTemplateModal: openModal,
closeTemplateModal: closeModal
};
};
export default useCreateTemplate;
+164 -1
View File
@@ -1,7 +1,170 @@
import { PageAction } from '@/config';
import useTableFetch from '@/hooks/use-table-fetch';
import { GPUsConfigs } from '@/pages/resources/config/gpu-driver';
import {
DeleteModal,
FilterBar,
IconFont,
InfiniteScrollerProvider,
NoResult
} from '@gpustack/core-ui';
import { useMemoizedFn } from 'ahooks';
import { message } from 'antd';
import _ from 'lodash';
import PageBox from '../../_components/page-box';
import {
createGPUServiceTemplate,
deleteGPUServiceTemplate,
GPU_SERVICE_TEMPLATES_API,
queryGPUServiceTemplates,
updateGPUServiceTemplate
} from './apis';
import AddModal from './components/add-modal';
import TemplateCardList from './components/template-list';
import { FormData, ListItem } from './config/types';
import useCreateTemplate from './hooks/use-create-template';
const GPUServiceTemplates: React.FC = () => {
return <PageBox> GPU Service Templates Page</PageBox>;
const {
dataSource,
rowSelection,
queryParams,
modalRef,
handleQueryChange,
fetchData,
handleDelete,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
fetchAPI: queryGPUServiceTemplates,
deleteAPI: deleteGPUServiceTemplate,
API: GPU_SERVICE_TEMPLATES_API,
watch: false,
isInfiniteScroll: true,
contentForDelete: 'GPU 实例模板',
defaultQueryParams: {
perPage: 24
}
});
const { openTemplateModalStatus, openTemplateModal, closeTemplateModal } =
useCreateTemplate();
const handleAddTemplate = () => {
openTemplateModal(PageAction.CREATE, '添加实例模板');
};
const handleEditTemplate = (row: ListItem) => {
openTemplateModal(PageAction.EDIT, '编辑实例模板', row);
};
const handleModalOk = async (data: FormData) => {
try {
if (openTemplateModalStatus.action === PageAction.EDIT) {
await updateGPUServiceTemplate({
id: openTemplateModalStatus.currentData!.id,
data
});
} else {
await createGPUServiceTemplate({ data });
}
closeTemplateModal();
handleSearch();
message.success('操作成功');
} catch (error) {
message.error('操作失败');
}
};
const gpuVendorOptions = Object.values(GPUsConfigs).map((item) => ({
label: item.label,
value: item.value
}));
const handleFilterByVendor = (value: string) => {
handleQueryChange({
vendor: value,
page: 1
});
};
const handleOnSelect = (item: { action: string; data: ListItem }) => {
if (item.action === 'edit') {
handleEditTemplate(item.data);
return;
}
if (item.action === 'delete') {
handleDelete({ ...item.data, name: item.data.name });
}
};
const loadMore = useMemoizedFn((nextPage: number) => {
fetchData({
query: {
...queryParams,
page: nextPage
},
loadmore: true
});
});
return (
<PageBox>
<FilterBar
marginBottom={22}
marginTop={30}
widths={{
input: 230
}}
inputHolder="按名称过滤"
selectHolder="按厂商过滤"
buttonText="添加实例模板"
handleClickPrimary={handleAddTemplate}
handleSearch={handleSearch}
handleSelectChange={handleFilterByVendor}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
showSelect={true}
selectOptions={gpuVendorOptions}
/>
<InfiniteScrollerProvider
value={{
total: dataSource.totalPage,
current: queryParams.page!,
loading: dataSource.loading,
refresh: loadMore,
throttleDelay: 300
}}
>
<TemplateCardList
dataList={dataSource.dataList}
loading={dataSource.loading}
isFirst={!dataSource.loadend}
onSelect={handleOnSelect}
/>
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-instance-template-filled" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的实例模板"
title="暂无实例模板"
subTitle="创建一个实例模板后会显示在这里"
onClick={handleAddTemplate}
buttonText="立即添加"
/>
</InfiniteScrollerProvider>
<AddModal
action={openTemplateModalStatus.action}
open={openTemplateModalStatus.open}
title={openTemplateModalStatus.title}
currentData={openTemplateModalStatus.currentData}
onCancel={closeTemplateModal}
onOk={handleModalOk}
/>
<DeleteModal ref={modalRef} />
</PageBox>
);
};
export default GPUServiceTemplates;