feat: templates, storage
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user