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,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;