1
This commit is contained in:
parent
4fded64d7c
commit
ff88de9aa8
@ -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>
|
||||
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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: "输入",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user