SHELL可以正确的设置和回显。

This commit is contained in:
dengqichen 2024-12-06 13:31:46 +08:00
parent 41975feedc
commit c58e799c22
2 changed files with 138 additions and 217 deletions

View File

@ -67,217 +67,107 @@ const NodeConfig: React.FC<NodeConfigProps> = ({ nodeType, form, onValuesChange
return config || {}; return config || {};
}, [selectedExecutor, nodeType.executors]); }, [selectedExecutor, nodeType.executors]);
// 当执行器变更时,重置执行器相关的配置 // 渲染基本配置表单项
useEffect(() => { const renderBasicFormItems = () => (
if (selectedExecutor) { <>
const fields = form.getFieldsValue() as FormFields;
const executorFields: FormFields = {};
// 清除旧的执行器配置
Object.keys(fields).forEach(key => {
if (key !== 'executor' && key !== 'name' && key !== 'description') {
executorFields[key] = undefined;
}
});
// 设置新的执行器默认配置
form.setFieldsValue({
...executorFields,
...executorDefaultConfig
});
}
}, [selectedExecutor, form, executorDefaultConfig]);
// 根据属性类型渲染表单控件
const renderFormItem = (key: string, property: any, required: boolean = false) => {
const {
type,
title,
description,
minimum,
maximum,
minLength,
maxLength,
enum: enumValues,
enumNames,
pattern,
format,
} = property;
const rules: Rule[] = [];
// 添加必填规则
if (required) {
rules.push({ required: true, message: `请输入${title}` } as Rule);
}
// 添加长度限制
if (minLength !== undefined) {
rules.push({ type: 'string', min: minLength, message: `最少输入${minLength}个字符` } as Rule);
}
if (maxLength !== undefined) {
rules.push({ type: 'string', max: maxLength, message: `最多输入${maxLength}个字符` } as Rule);
}
// 添加数值范围限制
if (type === 'number') {
// 添加数字类型验证
rules.push({
type: 'number',
message: '请输入有效的数字',
transform(value) {
if (value === '' || value === undefined || value === null) return undefined;
const num = Number(value);
return isNaN(num) ? undefined : num;
},
} as Rule);
// 添加整数验证
rules.push({
validator: async (_: any, value: any) => {
if (value === undefined || value === null || value === '') return;
if (!Number.isInteger(Number(value))) {
throw new Error('请输入整数');
}
}
} as Rule);
// 最小值验证
if (minimum !== undefined) {
rules.push({
validator: async (_: any, value: any) => {
if (value === undefined || value === null || value === '') return;
const num = Number(value);
if (num < minimum) {
throw new Error(`不能小于${minimum}`);
}
}
} as Rule);
}
// 最大值验证
if (maximum !== undefined) {
rules.push({
validator: async (_: any, value: any) => {
if (value === undefined || value === null || value === '') return;
const num = Number(value);
if (num > maximum) {
throw new Error(`不能大于${maximum}`);
}
}
} as Rule);
}
}
// 添加正则校验
if (pattern) {
try {
rules.push({ pattern: new RegExp(pattern), message: `格式不正确` } as Rule);
} catch (error) {
console.error('正则表达式解析错误:', error);
}
}
let formItem;
switch (type) {
case 'string':
if (enumValues) {
formItem = (
<Select
style={{ width: '100%' }}
placeholder={`请选择${title}`}
options={enumValues.map((value: string, index: number) => ({
label: enumNames?.[index] || value,
value,
}))}
/>
);
} else if (format === 'shell') {
formItem = <Input.TextArea rows={6} placeholder={`请输入${title}`} />;
} else {
formItem = <Input placeholder={`请输入${title}`} />;
}
break;
case 'number':
formItem = (
<InputNumber
style={{ width: '100%' }}
placeholder={`请输入${title}`}
min={minimum}
max={maximum}
precision={0} // 只允许整数
step={1} // 步长为1
/>
);
break;
case 'boolean':
formItem = <Switch checkedChildren="是" unCheckedChildren="否" />;
break;
default:
formItem = <Input placeholder={`请输入${title}`} />;
}
return (
<Form.Item <Form.Item
key={key} label="节点名称"
label={title} name="name"
name={key} rules={[{ required: true, message: '请输入节点名称' }]}
tooltip={description}
rules={rules}
> >
{formItem} <Input placeholder="请输入节点名称" />
</Form.Item> </Form.Item>
<Form.Item
label="节点描述"
name="description"
>
<Input.TextArea placeholder="请输入节点描述" />
</Form.Item>
</>
); );
};
// 获取初始值 // 渲染Shell执行器特定配置
const getInitialValues = () => { const renderShellExecutorConfig = () => {
const values = { ...nodeDefaultConfig }; if (selectedExecutor !== 'SHELL') return null;
// 如果有执行器配置,添加执行器的默认值 return (
if (selectedExecutor && executorDefaultConfig) { <>
Object.assign(values, executorDefaultConfig); <Form.Item
} label="脚本内容"
name="script"
return values; rules={[{ required: true, message: '请输入脚本内容' }]}
}; >
<Input.TextArea rows={6} placeholder="请输入脚本内容" />
// 渲染节点基本配置 </Form.Item>
const renderNodeConfig = () => { <Form.Item
if (!nodeSchema?.properties) return null; label="工作目录"
return Object.entries(nodeSchema.properties).map(([key, property]) => name="workingDirectory"
renderFormItem(key, property, nodeSchema.required?.includes(key)) >
); <Input placeholder="请输入工作目录" />
}; </Form.Item>
<Form.Item
// 渲染执行器配置 label="超时时间"
const renderExecutorConfig = () => { name="timeout"
if (!executorSchema?.properties) return null; rules={[{ type: 'number', min: 0, message: '超时时间必须大于0' }]}
return Object.entries(executorSchema.properties).map(([key, property]) => >
renderFormItem(key, property, executorSchema.required?.includes(key)) <InputNumber placeholder="请输入超时时间(秒)" style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label="重试次数"
name="retryTimes"
rules={[{ type: 'number', min: 0, message: '重试次数不能为负数' }]}
>
<InputNumber placeholder="请输入重试次数" style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label="重试间隔"
name="retryInterval"
rules={[{ type: 'number', min: 0, message: '重试间隔不能为负数' }]}
>
<InputNumber placeholder="请输入重试间隔(秒)" style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label="环境变量"
name="environment"
>
<Input.TextArea placeholder="请输入环境变量" />
</Form.Item>
<Form.Item
label="成功退出码"
name="successExitCode"
>
<Input placeholder="请输入成功退出码" />
</Form.Item>
</>
); );
}; };
return ( return (
<div className="node-config">
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
onValuesChange={onValuesChange} onValuesChange={onValuesChange}
initialValues={getInitialValues()} initialValues={{ ...nodeDefaultConfig, ...executorDefaultConfig }}
> >
{/* 节点基本配置 */} {renderBasicFormItems()}
{renderNodeConfig()} <Divider />
{/* 执行器配置 */} <Form.Item
{selectedExecutor && executorSchema && ( label="执行器"
<> name="executor"
<Divider></Divider> rules={[{ required: true, message: '请选择执行器' }]}
{renderExecutorConfig()} >
</> <Select
)} placeholder="请选择执行器"
options={nodeType.executors?.map(executor => ({
label: executor.name,
value: executor.code
}))}
/>
</Form.Item>
{renderShellExecutorConfig()}
</Form> </Form>
</div>
); );
}; };

View File

@ -30,7 +30,9 @@ interface NodeData {
retryInterval?: number; retryInterval?: number;
script?: string; script?: string;
timeout?: number; timeout?: number;
workingDir?: string; workingDirectory?: string;
environment?: string;
successExitCode?: string;
[key: string]: any; [key: string]: any;
}; };
} }
@ -259,6 +261,8 @@ const FlowDesigner: React.FC = () => {
graph.on('node:click', ({node}: {node: Node}) => { graph.on('node:click', ({node}: {node: Node}) => {
setCurrentNode(node); setCurrentNode(node);
const data = node.getData() as NodeData; const data = node.getData() as NodeData;
console.log('Node clicked, data:', data);
if (data) { if (data) {
// 获取节点类型 // 获取节点类型
const nodeType = nodeTypes.find(type => type.code === data.type); const nodeType = nodeTypes.find(type => type.code === data.type);
@ -270,20 +274,23 @@ const FlowDesigner: React.FC = () => {
name: data.name || nodeType.name, name: data.name || nodeType.name,
description: data.description, description: data.description,
executor: data.config?.executor, executor: data.config?.executor,
retryTimes: data.config?.retryTimes,
retryInterval: data.config?.retryInterval,
}; };
// 如果是Shell节点添加执行器特定配置 // 如果是Shell节点添加执行器特定配置
if (data.type === 'SHELL' && data.config?.executor === 'SHELL') { if (data.type === 'SHELL' && data.config?.executor === 'SHELL') {
console.log('Shell node config:', data.config);
Object.assign(formValues, { Object.assign(formValues, {
script: data.config?.script || '', script: data.config.script,
timeout: data.config?.timeout || 300, timeout: data.config.timeout,
workingDir: data.config?.workingDir || '/tmp' workingDirectory: data.config.workingDirectory,
environment: data.config.environment,
successExitCode: data.config.successExitCode,
retryTimes: data.config.retryTimes,
retryInterval: data.config.retryInterval,
}); });
} }
// 设置表单值 console.log('Setting form values:', formValues);
form.setFieldsValue(formValues); form.setFieldsValue(formValues);
setConfigVisible(true); setConfigVisible(true);
} else { } else {
@ -415,6 +422,23 @@ const FlowDesigner: React.FC = () => {
setConfigVisible(true); setConfigVisible(true);
}; };
// 验证Shell节点配置
function validateShellConfig(config: any): boolean {
if (!config.script?.trim()) {
throw new Error('脚本内容不能为空');
}
if (config.timeout !== undefined && config.timeout <= 0) {
throw new Error('超时时间必须大于0');
}
if (config.retryTimes !== undefined && config.retryTimes < 0) {
throw new Error('重试次数不能为负数');
}
if (config.retryInterval !== undefined && config.retryInterval < 0) {
throw new Error('重试间隔不能为负数');
}
return true;
}
// 处理配置保存 // 处理配置保存
const handleConfigSave = async () => { const handleConfigSave = async () => {
try { try {
@ -423,6 +447,11 @@ const FlowDesigner: React.FC = () => {
const data = currentNode.getData() as NodeData; const data = currentNode.getData() as NodeData;
const { name, description, ...config } = values; const { name, description, ...config } = values;
// 如果是Shell节点验证配置
if (data.type === 'SHELL' && config.executor === 'SHELL') {
validateShellConfig(config);
}
// 更新节点数据 // 更新节点数据
currentNode.setData({ currentNode.setData({
...data, ...data,
@ -438,7 +467,8 @@ const FlowDesigner: React.FC = () => {
setConfigVisible(false); setConfigVisible(false);
} }
} catch (error) { } catch (error) {
// 表单验证失败 // 表单验证失败或Shell配置验证失败
message.error((error as Error).message);
} }
}; };
@ -533,22 +563,23 @@ const FlowDesigner: React.FC = () => {
// 加载节点配置数据 // 加载节点配置数据
if (detail.nodeConfig) { if (detail.nodeConfig) {
console.log('Loading node config:', detail.nodeConfig);
const nodeConfigData = JSON.parse(detail.nodeConfig); const nodeConfigData = JSON.parse(detail.nodeConfig);
const nodeConfigs = nodeConfigData.nodes.reduce((acc: Record<string, any>, node: any) => { console.log('Parsed node config:', nodeConfigData);
acc[node.id] = node;
return acc;
}, {});
// 更新节点数据和显示 // 更新节点数据和显示
graph.getNodes().forEach(node => { graph.getNodes().forEach(node => {
const nodeConfig = nodeConfigs[node.id]; const nodeConfig = nodeConfigData.nodes.find((n: any) => n.id === node.id);
if (nodeConfig) { if (nodeConfig) {
node.setData({ console.log('Node config found:', nodeConfig);
const nodeData = {
type: nodeConfig.type, type: nodeConfig.type,
name: nodeConfig.name, name: nodeConfig.name,
description: nodeConfig.description, description: nodeConfig.description,
config: nodeConfig.config || {} config: nodeConfig.config
}); };
console.log('Setting node data:', nodeData);
node.setData(nodeData);
node.setAttrByPath('label/text', nodeConfig.name); node.setAttrByPath('label/text', nodeConfig.name);
} }
}); });