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
+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}
>