This commit is contained in:
dengqichen 2025-10-22 16:27:42 +08:00
parent 4fded64d7c
commit ff88de9aa8
7 changed files with 154 additions and 179 deletions

View File

@ -38,6 +38,10 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
const [loading, setLoading] = useState(false);
const { toast } = useToast();
// 固定的节点信息(不通过 schema直接管理
const [nodeName, setNodeName] = useState('');
const [description, setDescription] = useState('');
// 动态数据源缓存
const [dataSourceCache, setDataSourceCache] = useState<Record<string, DataSourceOption[]>>({});
const [loadingDataSources, setLoadingDataSources] = useState(false);
@ -45,12 +49,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// 获取节点定义
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
// ✅ 生成 Zod Schema
const configSchema = useMemo(() => {
if (!nodeDefinition?.configSchema) return null;
return convertJsonSchemaToZod(nodeDefinition.configSchema);
}, [nodeDefinition?.configSchema]);
// ✅ 生成 Zod Schema仅输入映射
const inputMappingSchema = useMemo(() => {
if (!nodeDefinition || !isConfigurableNode(nodeDefinition) || !nodeDefinition.inputMappingSchema) {
return null;
@ -58,31 +57,22 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
return convertJsonSchemaToZod(nodeDefinition.inputMappingSchema);
}, [nodeDefinition]);
// ✅ 创建表单实例(基本配置)
const configForm = useForm({
resolver: configSchema ? zodResolver(configSchema) : undefined,
defaultValues: {}
});
// ✅ 创建表单实例(输入映射)
// ✅ 创建表单实例(仅输入映射)
const inputForm = useForm({
resolver: inputMappingSchema ? zodResolver(inputMappingSchema) : undefined,
defaultValues: {}
});
// ✅ 预加载动态数据源
// ✅ 预加载动态数据源(仅输入映射)
useEffect(() => {
if (!visible || !nodeDefinition) return;
const loadDynamicData = async () => {
const configTypes = extractDataSourceTypes(nodeDefinition.configSchema);
const inputTypes = isConfigurableNode(nodeDefinition) && nodeDefinition.inputMappingSchema
? extractDataSourceTypes(nodeDefinition.inputMappingSchema)
: [];
const allTypes = [...new Set([...configTypes, ...inputTypes])];
if (allTypes.length === 0) {
if (inputTypes.length === 0) {
return;
}
@ -90,7 +80,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
try {
const cache: Record<string, DataSourceOption[]> = {};
await Promise.all(
allTypes.map(async (type) => {
inputTypes.map(async (type) => {
const data = await loadDataSource(type as DataSourceType);
cache[type] = data;
})
@ -116,21 +106,11 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
if (visible && node && nodeDefinition) {
const nodeData = node.data || {};
// 设置基本配置默认值
const defaultConfig = {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description || ''
};
// 设置固定节点信息(从 configs 或 nodeDefinition 获取)
setNodeName(nodeData.configs?.nodeName || nodeDefinition.nodeName);
setDescription(nodeData.configs?.description || nodeDefinition.description || '');
// 转换 UUID 格式为显示名称格式
const displayConfigs = convertObjectToDisplayName(
{ ...defaultConfig, ...(nodeData.configs || {}) },
allNodes
);
configForm.reset(displayConfigs);
// 设置输入映射默认值(也需要转换)
// 设置输入映射默认值(转换 UUID 为显示名称)
if (isConfigurableNode(nodeDefinition)) {
const displayInputMapping = convertObjectToDisplayName(
nodeData.inputMapping || {},
@ -148,8 +128,18 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
const handleSave = () => {
if (!node || !nodeDefinition) return;
// 使用 handleSubmit 验证并获取数据
configForm.handleSubmit(async (configData) => {
// 验证节点名称(必填)
if (!nodeName || !nodeName.trim()) {
toast({
title: '保存失败',
description: '节点名称不能为空',
variant: 'destructive'
});
return;
}
// 使用 handleSubmit 验证输入映射(如果存在)
const submitForm = async () => {
setLoading(true);
try {
// 获取输入映射数据
@ -158,14 +148,20 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
inputData = inputForm.getValues();
}
// 转换显示名称格式为 UUID 格式
const uuidConfigs = convertObjectToUUID(configData as Record<string, any>, allNodes);
// 转换显示名称格式为 UUID 格式(仅输入映射需要)
const uuidInputMapping = convertObjectToUUID(inputData as Record<string, any>, allNodes);
// 构建 configs包含元数据
const configs = {
nodeName: nodeName.trim(),
nodeCode: nodeDefinition.nodeCode, // 只读,从定义获取
description: description.trim()
};
// 构建更新数据
const updatedData: Partial<FlowNodeData> = {
label: (configData as any).nodeName || nodeDefinition.nodeName,
configs: uuidConfigs,
label: nodeName.trim(),
configs,
inputMapping: uuidInputMapping,
outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : []
};
@ -189,19 +185,25 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
} finally {
setLoading(false);
}
})();
};
// 如果有输入映射schema使用表单验证
if (inputMappingSchema) {
inputForm.handleSubmit(submitForm)();
} else {
submitForm();
}
};
// ✅ 重置表单
const handleReset = () => {
if (!node || !nodeDefinition) return;
const defaultConfig = {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description || ''
};
configForm.reset(defaultConfig);
// 重置节点信息
setNodeName(nodeDefinition.nodeName);
setDescription(nodeDefinition.description || '');
// 重置输入映射
inputForm.reset({});
toast({
@ -413,18 +415,62 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
<div className="flex-1 overflow-y-auto px-6 py-4">
<Accordion
type="multiple"
defaultValue={["config"]}
defaultValue={["info", "input"]}
className="w-full"
>
{/* 基本配置 */}
<AccordionItem value="config" className="border-b">
{/* 节点信息(固定表单,不通过 schema */}
<AccordionItem value="info" className="border-b">
<AccordionTrigger className="text-base font-semibold flex-row-reverse justify-end gap-2 hover:no-underline">
</AccordionTrigger>
<AccordionContent className="px-1 space-y-4">
<Form {...configForm}>
{renderFormFields(nodeDefinition.configSchema, configForm)}
</Form>
{/* 节点编码(只读) */}
<div className="space-y-2">
<label className="text-sm font-medium">
</label>
<Input
value={nodeDefinition.nodeCode}
disabled
className="bg-muted cursor-not-allowed"
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 节点名称 */}
<div className="space-y-2">
<label className="text-sm font-medium">
<span className="text-red-500">*</span>
</label>
<Input
value={nodeName}
onChange={(e) => setNodeName(e.target.value)}
placeholder="请输入节点名称"
disabled={loading}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 节点描述 */}
<div className="space-y-2">
<label className="text-sm font-medium">
</label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="请输入节点描述"
disabled={loading}
rows={3}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
</AccordionContent>
</AccordionItem>

View File

@ -35,33 +35,6 @@ export const EndEventNodeDefinition: BaseNodeDefinition = {
showBadge: false, // 结束节点不显示徽章
showHoverMenu: false // 结束节点不显示菜单
}
},
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "结束"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "END_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的结束节点"
}
},
required: ["nodeName", "nodeCode"]
}
};

View File

@ -38,33 +38,8 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
}
},
// 基本配置Schema
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息和Jenkins构建配置参数",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "Jenkins构建"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "JENKINS_BUILD"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "通过Jenkins执行构建任务"
}
},
required: ["nodeName", "nodeCode", "jenkinsServerId"]
},
// 基本配置Schema仅业务配置元数据已移至固定表单
configSchema: undefined,
inputMappingSchema: {
type: "object",
title: "输入",

View File

@ -38,33 +38,8 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
}
},
// 基本配置 Schema
configSchema: {
type: "object",
title: "基本配置",
description: "通知节点的基本配置信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "通知"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "NOTIFICATION"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "发送通知消息到指定渠道"
}
},
required: ["nodeName", "nodeCode", "notificationType", "title", "content"]
},
// 基本配置Schema仅业务配置元数据已移至固定表单
configSchema: undefined,
inputMappingSchema: {
type: "object",
title: "输入",
@ -78,11 +53,17 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
// enumNames: ["邮件", "钉钉", "企业微信", "短信"],
// default: "EMAIL"
// },
notificationType: {
// notificationChannelType: {
// type: "string",
// title: "通知类型",
// description: "选择通知发送的渠道",
// 'x-dataSource': DataSourceType.NOTIFICATION_CHANNEL_TYPES
// },
notificationChannel: {
type: "string",
title: "通知类型",
title: "通知渠道",
description: "选择通知发送的渠道",
'x-dataSource': DataSourceType.NOTIFICATION_CHANNEL_TYPES
'x-dataSource': DataSourceType.NOTIFICATION_CHANNELS
},
title: {
type: "string",
@ -98,7 +79,7 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
default: ""
}
},
required: ["jenkinsServerId"]
required: ["notificationChannel", "title", "content"]
},
outputs: [{
name: "status",

View File

@ -35,33 +35,6 @@ export const StartEventNodeDefinition: BaseNodeDefinition = {
showBadge: false, // 开始节点不显示徽章
showHoverMenu: false // 开始节点不显示菜单
}
},
configSchema: {
type: "object",
title: "基本配置",
description: "节点的基本信息",
properties: {
nodeName: {
type: "string",
title: "节点名称",
description: "节点在流程图中显示的名称",
default: "开始"
},
nodeCode: {
type: "string",
title: "节点编码",
description: "节点的唯一标识符",
default: "START_EVENT"
},
description: {
type: "string",
title: "节点描述",
description: "节点的详细说明",
default: "工作流的起始节点"
}
},
required: ["nodeName", "nodeCode"]
}
};

View File

@ -25,6 +25,7 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
}
const schema = definition.inputMappingSchema;
const inputMapping = nodeData.inputMapping || {};
// 获取所有定义的输入字段(从 schema
if (!schema.properties) {
@ -33,10 +34,19 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
const allInputs = Object.keys(schema.properties).map(key => {
const fieldSchema = schema.properties![key];
const fieldValue = inputMapping[key];
// 检查字段是否已填写非空、非null、非undefined、非空字符串
const isFilled = fieldValue !== undefined &&
fieldValue !== null &&
fieldValue !== '' &&
(typeof fieldValue !== 'number' || !isNaN(fieldValue));
return {
key,
title: fieldSchema.title || key,
description: fieldSchema.description,
isFilled,
};
});
@ -51,13 +61,19 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
{/* 输入字段标签 */}
<div className="flex flex-wrap gap-1.5">
{allInputs.map((input, index) => (
<code
<span
key={index}
className="text-xs px-2 py-1 bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-md font-mono inline-flex items-center"
title={input.description}
className={`
text-xs px-2 py-1 rounded-md font-medium inline-flex items-center
${input.isFilled
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300'
}
`}
title={`${input.description}${input.isFilled ? ' ✓ 已填写' : ' ✗ 未填写'}`}
>
{input.title}
</code>
</span>
))}
</div>
</div>
@ -76,13 +92,13 @@ const BaseNode: React.FC<NodeProps> = ({ data, selected }) => {
{/* 输出字段标签 */}
<div className="flex flex-wrap gap-1.5">
{nodeData.outputs.map((output, index) => (
<code
<span
key={index}
className="text-xs px-2 py-1 bg-secondary text-secondary-foreground rounded-md font-mono inline-flex items-center"
className="text-xs px-2 py-1 bg-secondary text-secondary-foreground rounded-md font-medium inline-flex items-center"
title={output.description}
>
{output.title || output.name}
</code>
</span>
))}
</div>
</div>

View File

@ -8,7 +8,8 @@ export enum DataSourceType {
K8S_CLUSTERS = 'K8S_CLUSTERS',
GIT_REPOSITORIES = 'GIT_REPOSITORIES',
DOCKER_REGISTRIES = 'DOCKER_REGISTRIES',
NOTIFICATION_CHANNEL_TYPES = 'NOTIFICATION_CHANNEL_TYPES'
NOTIFICATION_CHANNEL_TYPES = 'NOTIFICATION_CHANNEL_TYPES',
NOTIFICATION_CHANNELS = 'NOTIFICATION_CHANNELS'
}
/**
@ -26,6 +27,7 @@ export interface DataSourceConfig {
export interface DataSourceOption {
label: string;
value: any;
[key: string]: any;
}
@ -54,6 +56,15 @@ export const DATA_SOURCE_REGISTRY: Record<DataSourceType, DataSourceConfig> = {
}));
}
},
[DataSourceType.NOTIFICATION_CHANNELS]: {
url: '/api/v1/notification-channel/list',
transform: (data: any[]) => {
return data.map((item: any) => ({
label: `(${item.channelType})-${item.name}`,
value: item.id
}));
}
},
[DataSourceType.K8S_CLUSTERS]: {
url: '/api/v1/k8s-cluster/list',
params: {enabled: true},