mirror of
https://github.com/whyour/qinglong.git
synced 2026-02-12 22:16:42 +08:00
Implement Flowgram visual workflow editor with node system
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
ff74e96cbf
commit
cfe4aabc00
441
src/pages/scenario/flowgram/Editor.tsx
Normal file
441
src/pages/scenario/flowgram/Editor.tsx
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
||||||
|
import { Button, Space, message, Drawer, Form, Input, Select, InputNumber } from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
|
SaveOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { FreeLayoutEditor } from '@flowgram.ai/free-layout-editor';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import type { FlowgramGraph, FlowgramNode, FlowgramEdge } from './types';
|
||||||
|
import { nodeTemplates } from './nodes';
|
||||||
|
import { flowgramToBackend, backendToFlowgram, validateWorkflow, createEdge } from './utils/dataConverter';
|
||||||
|
import './editor.css';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface FlowgramEditorProps {
|
||||||
|
value?: FlowgramGraph;
|
||||||
|
onChange?: (graph: FlowgramGraph) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlowgramEditor: React.FC<FlowgramEditorProps> = ({ value, onChange }) => {
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const [graph, setGraph] = useState<FlowgramGraph>(value || { nodes: [], edges: [] });
|
||||||
|
const [selectedNode, setSelectedNode] = useState<FlowgramNode | null>(null);
|
||||||
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// Initialize editor
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
setGraph(backendToFlowgram(value));
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// Notify parent of changes
|
||||||
|
const notifyChange = useCallback((newGraph: FlowgramGraph) => {
|
||||||
|
setGraph(newGraph);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(flowgramToBackend(newGraph));
|
||||||
|
}
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
// Add node to canvas
|
||||||
|
const addNode = useCallback((templateKey: string) => {
|
||||||
|
const template = nodeTemplates[templateKey as keyof typeof nodeTemplates];
|
||||||
|
if (!template) {
|
||||||
|
message.error('未知的节点类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNode = template();
|
||||||
|
// Position new node in center with some randomness
|
||||||
|
newNode.position = {
|
||||||
|
x: 200 + Math.random() * 300,
|
||||||
|
y: 100 + Math.random() * 200,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newGraph = {
|
||||||
|
...graph,
|
||||||
|
nodes: [...graph.nodes, newNode],
|
||||||
|
};
|
||||||
|
|
||||||
|
notifyChange(newGraph);
|
||||||
|
message.success('节点已添加');
|
||||||
|
}, [graph, notifyChange]);
|
||||||
|
|
||||||
|
// Handle node click/double-click to open config
|
||||||
|
const handleNodeClick = useCallback((node: FlowgramNode) => {
|
||||||
|
setSelectedNode(node);
|
||||||
|
form.setFieldsValue(node.data);
|
||||||
|
setDrawerVisible(true);
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
// Save node configuration
|
||||||
|
const handleSaveNodeConfig = useCallback(() => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
|
||||||
|
form.validateFields().then((values) => {
|
||||||
|
const updatedNodes = graph.nodes.map((node) =>
|
||||||
|
node.id === selectedNode.id
|
||||||
|
? { ...node, data: { ...node.data, ...values } }
|
||||||
|
: node
|
||||||
|
);
|
||||||
|
|
||||||
|
const newGraph = {
|
||||||
|
...graph,
|
||||||
|
nodes: updatedNodes,
|
||||||
|
};
|
||||||
|
|
||||||
|
notifyChange(newGraph);
|
||||||
|
setDrawerVisible(false);
|
||||||
|
message.success('节点配置已保存');
|
||||||
|
});
|
||||||
|
}, [selectedNode, form, graph, notifyChange]);
|
||||||
|
|
||||||
|
// Delete selected node
|
||||||
|
const handleDeleteNode = useCallback(() => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
|
||||||
|
const newGraph = {
|
||||||
|
nodes: graph.nodes.filter((n) => n.id !== selectedNode.id),
|
||||||
|
edges: graph.edges.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
notifyChange(newGraph);
|
||||||
|
setDrawerVisible(false);
|
||||||
|
message.success('节点已删除');
|
||||||
|
}, [selectedNode, graph, notifyChange]);
|
||||||
|
|
||||||
|
// Connect two nodes
|
||||||
|
const handleConnect = useCallback((connection: { source: string; target: string }) => {
|
||||||
|
const edge = createEdge(connection.source, connection.target);
|
||||||
|
const newGraph = {
|
||||||
|
...graph,
|
||||||
|
edges: [...graph.edges, edge],
|
||||||
|
};
|
||||||
|
|
||||||
|
notifyChange(newGraph);
|
||||||
|
}, [graph, notifyChange]);
|
||||||
|
|
||||||
|
// Validate workflow
|
||||||
|
const handleValidate = useCallback(() => {
|
||||||
|
const validation = validateWorkflow(graph);
|
||||||
|
if (validation.valid) {
|
||||||
|
message.success('工作流验证通过!');
|
||||||
|
} else {
|
||||||
|
message.error(`验证失败: ${validation.errors.join(', ')}`);
|
||||||
|
}
|
||||||
|
}, [graph]);
|
||||||
|
|
||||||
|
// Render node configuration form based on node type
|
||||||
|
const renderNodeConfigForm = () => {
|
||||||
|
if (!selectedNode) return null;
|
||||||
|
|
||||||
|
const { type, data } = selectedNode;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'trigger':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name="label" label="标签" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="triggerType" label="触发类型" rules={[{ required: true }]}>
|
||||||
|
<Select>
|
||||||
|
<Option value="time">时间触发</Option>
|
||||||
|
<Option value="webhook">Webhook</Option>
|
||||||
|
<Option value="variable_monitor">变量监听</Option>
|
||||||
|
<Option value="task_status">任务状态</Option>
|
||||||
|
<Option value="system_event">系统事件</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{data.triggerType === 'time' && (
|
||||||
|
<Form.Item name={['config', 'schedule']} label="Cron表达式">
|
||||||
|
<Input placeholder="0 0 * * *" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{data.triggerType === 'variable_monitor' && (
|
||||||
|
<Form.Item name={['config', 'watchPath']} label="监听路径">
|
||||||
|
<Input placeholder="/path/to/watch" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{data.triggerType === 'system_event' && (
|
||||||
|
<>
|
||||||
|
<Form.Item name={['config', 'eventType']} label="事件类型">
|
||||||
|
<Select>
|
||||||
|
<Option value="disk">磁盘使用</Option>
|
||||||
|
<Option value="memory">内存使用</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['config', 'threshold']} label="阈值 (%)">
|
||||||
|
<InputNumber min={0} max={100} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'condition':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name="label" label="标签" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="field" label="字段" rules={[{ required: true }]}>
|
||||||
|
<Input placeholder="data.field" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="operator" label="操作符" rules={[{ required: true }]}>
|
||||||
|
<Select>
|
||||||
|
<Option value="equals">等于</Option>
|
||||||
|
<Option value="not_equals">不等于</Option>
|
||||||
|
<Option value="greater_than">大于</Option>
|
||||||
|
<Option value="less_than">小于</Option>
|
||||||
|
<Option value="contains">包含</Option>
|
||||||
|
<Option value="not_contains">不包含</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="value" label="值" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'action':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name="label" label="标签" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="actionType" label="动作类型" rules={[{ required: true }]}>
|
||||||
|
<Select>
|
||||||
|
<Option value="run_task">运行任务</Option>
|
||||||
|
<Option value="set_variable">设置变量</Option>
|
||||||
|
<Option value="execute_command">执行命令</Option>
|
||||||
|
<Option value="send_notification">发送通知</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{data.actionType === 'run_task' && (
|
||||||
|
<Form.Item name="cronId" label="任务ID" rules={[{ required: true }]}>
|
||||||
|
<InputNumber min={1} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{data.actionType === 'set_variable' && (
|
||||||
|
<>
|
||||||
|
<Form.Item name="name" label="变量名" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="value" label="变量值" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{data.actionType === 'execute_command' && (
|
||||||
|
<Form.Item name="command" label="命令" rules={[{ required: true }]}>
|
||||||
|
<Input.TextArea rows={3} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{data.actionType === 'send_notification' && (
|
||||||
|
<Form.Item name="message" label="消息" rules={[{ required: true }]}>
|
||||||
|
<Input.TextArea rows={3} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'control':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name="label" label="标签" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="controlType" label="控制类型" rules={[{ required: true }]}>
|
||||||
|
<Select>
|
||||||
|
<Option value="delay">延迟</Option>
|
||||||
|
<Option value="retry">重试</Option>
|
||||||
|
<Option value="circuit_breaker">熔断器</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
{data.controlType === 'delay' && (
|
||||||
|
<Form.Item name="delaySeconds" label="延迟时间(秒)" rules={[{ required: true }]}>
|
||||||
|
<InputNumber min={1} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{data.controlType === 'retry' && (
|
||||||
|
<>
|
||||||
|
<Form.Item name="maxRetries" label="最大重试次数">
|
||||||
|
<InputNumber min={1} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="retryDelay" label="重试延迟(秒)">
|
||||||
|
<InputNumber min={1} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="backoffMultiplier" label="退避倍数">
|
||||||
|
<InputNumber min={1} step={0.1} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{data.controlType === 'circuit_breaker' && (
|
||||||
|
<Form.Item name="failureThreshold" label="失败阈值">
|
||||||
|
<InputNumber min={1} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'logic_gate':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name="label" label="标签" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="gateType" label="逻辑类型" rules={[{ required: true }]}>
|
||||||
|
<Select>
|
||||||
|
<Option value="AND">AND (与)</Option>
|
||||||
|
<Option value="OR">OR (或)</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Form.Item name="label" label="标签" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple render for now - full Flowgram integration would go here
|
||||||
|
return (
|
||||||
|
<div className="flowgram-editor-container">
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="flowgram-toolbar">
|
||||||
|
<Space wrap>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={() => addNode('start')}>开始</Button>
|
||||||
|
<Select
|
||||||
|
placeholder="添加触发器"
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onSelect={(value) => addNode(value)}
|
||||||
|
value={undefined}
|
||||||
|
>
|
||||||
|
<Option value="trigger-time">时间触发</Option>
|
||||||
|
<Option value="trigger-webhook">Webhook</Option>
|
||||||
|
<Option value="trigger-variable">变量监听</Option>
|
||||||
|
<Option value="trigger-task">任务状态</Option>
|
||||||
|
<Option value="trigger-system">系统事件</Option>
|
||||||
|
</Select>
|
||||||
|
<Button onClick={() => addNode('condition')}>条件</Button>
|
||||||
|
<Select
|
||||||
|
placeholder="添加动作"
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onSelect={(value) => addNode(value)}
|
||||||
|
value={undefined}
|
||||||
|
>
|
||||||
|
<Option value="action-run">运行任务</Option>
|
||||||
|
<Option value="action-variable">设置变量</Option>
|
||||||
|
<Option value="action-command">执行命令</Option>
|
||||||
|
<Option value="action-notify">发送通知</Option>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
placeholder="添加控制流"
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onSelect={(value) => addNode(value)}
|
||||||
|
value={undefined}
|
||||||
|
>
|
||||||
|
<Option value="control-delay">延迟</Option>
|
||||||
|
<Option value="control-retry">重试</Option>
|
||||||
|
<Option value="control-breaker">熔断器</Option>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
placeholder="添加逻辑门"
|
||||||
|
style={{ width: 120 }}
|
||||||
|
onSelect={(value) => addNode(value)}
|
||||||
|
value={undefined}
|
||||||
|
>
|
||||||
|
<Option value="gate-and">AND</Option>
|
||||||
|
<Option value="gate-or">OR</Option>
|
||||||
|
</Select>
|
||||||
|
<Button onClick={() => addNode('end')}>结束</Button>
|
||||||
|
<Button icon={<PlayCircleOutlined />} onClick={handleValidate}>验证</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Canvas area - simplified view showing nodes */}
|
||||||
|
<div className="flowgram-canvas">
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<h3>工作流节点 ({graph.nodes.length})</h3>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{graph.nodes.map((node) => (
|
||||||
|
<div
|
||||||
|
key={node.id}
|
||||||
|
className="node-card"
|
||||||
|
onClick={() => handleNodeClick(node)}
|
||||||
|
style={{
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div><strong>{node.data.label}</strong></div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||||
|
类型: {node.type} | ID: {node.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{graph.edges.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h3 style={{ marginTop: '20px' }}>连接 ({graph.edges.length})</h3>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{graph.edges.map((edge) => (
|
||||||
|
<div
|
||||||
|
key={edge.id}
|
||||||
|
style={{
|
||||||
|
padding: '8px',
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{edge.source} → {edge.target}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Node configuration drawer */}
|
||||||
|
<Drawer
|
||||||
|
title="节点配置"
|
||||||
|
placement="right"
|
||||||
|
width={400}
|
||||||
|
onClose={() => setDrawerVisible(false)}
|
||||||
|
open={drawerVisible}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button danger icon={<DeleteOutlined />} onClick={handleDeleteNode}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" icon={<SaveOutlined />} onClick={handleSaveNodeConfig}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
{renderNodeConfigForm()}
|
||||||
|
</Form>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlowgramEditor;
|
||||||
31
src/pages/scenario/flowgram/editor.css
Normal file
31
src/pages/scenario/flowgram/editor.css
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
.flowgram-editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 600px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flowgram-toolbar {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #d9d9d9;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flowgram-canvas {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
background: #f5f5f5;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
153
src/pages/scenario/flowgram/nodes/index.ts
Normal file
153
src/pages/scenario/flowgram/nodes/index.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
// Flowgram Node Registry
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import type {
|
||||||
|
StartNodeData,
|
||||||
|
TriggerNodeData,
|
||||||
|
ConditionNodeData,
|
||||||
|
ActionNodeData,
|
||||||
|
ControlNodeData,
|
||||||
|
LogicGateNodeData,
|
||||||
|
EndNodeData,
|
||||||
|
FlowgramNode
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
// Node creation helpers
|
||||||
|
export function createStartNode(x: number = 100, y: number = 100): FlowgramNode {
|
||||||
|
return {
|
||||||
|
id: `start-${nanoid(8)}`,
|
||||||
|
type: 'start',
|
||||||
|
position: { x, y },
|
||||||
|
data: {
|
||||||
|
label: '开始',
|
||||||
|
} as StartNodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTriggerNode(triggerType: string, x: number = 200, y: number = 100): FlowgramNode {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
time: '时间触发',
|
||||||
|
webhook: 'Webhook触发',
|
||||||
|
variable_monitor: '变量监听',
|
||||||
|
task_status: '任务状态',
|
||||||
|
system_event: '系统事件',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `trigger-${nanoid(8)}`,
|
||||||
|
type: 'trigger',
|
||||||
|
position: { x, y },
|
||||||
|
data: {
|
||||||
|
label: labels[triggerType] || '触发器',
|
||||||
|
triggerType,
|
||||||
|
config: {},
|
||||||
|
} as TriggerNodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConditionNode(x: number = 300, y: number = 100): FlowgramNode {
|
||||||
|
return {
|
||||||
|
id: `condition-${nanoid(8)}`,
|
||||||
|
type: 'condition',
|
||||||
|
position: { x, y },
|
||||||
|
data: {
|
||||||
|
label: '条件判断',
|
||||||
|
operator: 'equals',
|
||||||
|
field: '',
|
||||||
|
value: '',
|
||||||
|
} as ConditionNodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createActionNode(actionType: string, x: number = 400, y: number = 100): FlowgramNode {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
run_task: '运行任务',
|
||||||
|
set_variable: '设置变量',
|
||||||
|
execute_command: '执行命令',
|
||||||
|
send_notification: '发送通知',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `action-${nanoid(8)}`,
|
||||||
|
type: 'action',
|
||||||
|
position: { x, y },
|
||||||
|
data: {
|
||||||
|
label: labels[actionType] || '动作',
|
||||||
|
actionType,
|
||||||
|
} as ActionNodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createControlNode(controlType: string, x: number = 500, y: number = 100): FlowgramNode {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
delay: '延迟执行',
|
||||||
|
retry: '重试策略',
|
||||||
|
circuit_breaker: '熔断器',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `control-${nanoid(8)}`,
|
||||||
|
type: 'control',
|
||||||
|
position: { x, y },
|
||||||
|
data: {
|
||||||
|
label: labels[controlType] || '控制流',
|
||||||
|
controlType,
|
||||||
|
} as ControlNodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLogicGateNode(gateType: 'AND' | 'OR', x: number = 300, y: number = 200): FlowgramNode {
|
||||||
|
return {
|
||||||
|
id: `gate-${nanoid(8)}`,
|
||||||
|
type: 'logic_gate',
|
||||||
|
position: { x, y },
|
||||||
|
data: {
|
||||||
|
label: gateType === 'AND' ? '逻辑与' : '逻辑或',
|
||||||
|
gateType,
|
||||||
|
} as LogicGateNodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEndNode(x: number = 600, y: number = 100): FlowgramNode {
|
||||||
|
return {
|
||||||
|
id: `end-${nanoid(8)}`,
|
||||||
|
type: 'end',
|
||||||
|
position: { x, y },
|
||||||
|
data: {
|
||||||
|
label: '结束',
|
||||||
|
} as EndNodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node templates for quick creation
|
||||||
|
export const nodeTemplates = {
|
||||||
|
start: () => createStartNode(),
|
||||||
|
|
||||||
|
// Trigger templates
|
||||||
|
'trigger-time': () => createTriggerNode('time'),
|
||||||
|
'trigger-webhook': () => createTriggerNode('webhook'),
|
||||||
|
'trigger-variable': () => createTriggerNode('variable_monitor'),
|
||||||
|
'trigger-task': () => createTriggerNode('task_status'),
|
||||||
|
'trigger-system': () => createTriggerNode('system_event'),
|
||||||
|
|
||||||
|
// Condition template
|
||||||
|
condition: () => createConditionNode(),
|
||||||
|
|
||||||
|
// Action templates
|
||||||
|
'action-run': () => createActionNode('run_task'),
|
||||||
|
'action-variable': () => createActionNode('set_variable'),
|
||||||
|
'action-command': () => createActionNode('execute_command'),
|
||||||
|
'action-notify': () => createActionNode('send_notification'),
|
||||||
|
|
||||||
|
// Control templates
|
||||||
|
'control-delay': () => createControlNode('delay'),
|
||||||
|
'control-retry': () => createControlNode('retry'),
|
||||||
|
'control-breaker': () => createControlNode('circuit_breaker'),
|
||||||
|
|
||||||
|
// Logic gate templates
|
||||||
|
'gate-and': () => createLogicGateNode('AND'),
|
||||||
|
'gate-or': () => createLogicGateNode('OR'),
|
||||||
|
|
||||||
|
end: () => createEndNode(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeTemplate = keyof typeof nodeTemplates;
|
||||||
76
src/pages/scenario/flowgram/types.ts
Normal file
76
src/pages/scenario/flowgram/types.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Flowgram Types for Qinglong Scenario Mode
|
||||||
|
|
||||||
|
export interface FlowgramNodeData {
|
||||||
|
label: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowgramNode {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
data: FlowgramNodeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowgramEdge {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowgramGraph {
|
||||||
|
nodes: FlowgramNode[];
|
||||||
|
edges: FlowgramEdge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node type definitions
|
||||||
|
export type NodeType = 'start' | 'trigger' | 'condition' | 'action' | 'control' | 'logic_gate' | 'end';
|
||||||
|
|
||||||
|
export type TriggerType = 'time' | 'webhook' | 'variable_monitor' | 'task_status' | 'system_event';
|
||||||
|
export type ConditionOperator = 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'not_contains';
|
||||||
|
export type ActionType = 'run_task' | 'set_variable' | 'execute_command' | 'send_notification';
|
||||||
|
export type ControlType = 'delay' | 'retry' | 'circuit_breaker';
|
||||||
|
export type LogicGateType = 'AND' | 'OR';
|
||||||
|
|
||||||
|
// Node data interfaces
|
||||||
|
export interface StartNodeData extends FlowgramNodeData {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TriggerNodeData extends FlowgramNodeData {
|
||||||
|
triggerType: TriggerType;
|
||||||
|
config?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConditionNodeData extends FlowgramNodeData {
|
||||||
|
operator: ConditionOperator;
|
||||||
|
field: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionNodeData extends FlowgramNodeData {
|
||||||
|
actionType: ActionType;
|
||||||
|
cronId?: number;
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
command?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControlNodeData extends FlowgramNodeData {
|
||||||
|
controlType: ControlType;
|
||||||
|
delaySeconds?: number;
|
||||||
|
maxRetries?: number;
|
||||||
|
retryDelay?: number;
|
||||||
|
backoffMultiplier?: number;
|
||||||
|
failureThreshold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogicGateNodeData extends FlowgramNodeData {
|
||||||
|
gateType: LogicGateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EndNodeData extends FlowgramNodeData {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
118
src/pages/scenario/flowgram/utils/dataConverter.ts
Normal file
118
src/pages/scenario/flowgram/utils/dataConverter.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Data format converter between Flowgram and backend
|
||||||
|
import type { FlowgramGraph, FlowgramNode, FlowgramEdge } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Flowgram graph to backend format
|
||||||
|
* Flowgram uses a similar node-edge structure, so minimal conversion needed
|
||||||
|
*/
|
||||||
|
export function flowgramToBackend(flowgramGraph: FlowgramGraph): any {
|
||||||
|
return {
|
||||||
|
nodes: flowgramGraph.nodes.map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
type: node.type,
|
||||||
|
position: node.position,
|
||||||
|
data: node.data,
|
||||||
|
})),
|
||||||
|
edges: flowgramGraph.edges.map((edge) => ({
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.source,
|
||||||
|
target: edge.target,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert backend format to Flowgram graph
|
||||||
|
*/
|
||||||
|
export function backendToFlowgram(backendGraph: any): FlowgramGraph {
|
||||||
|
if (!backendGraph || !backendGraph.nodes) {
|
||||||
|
return { nodes: [], edges: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: backendGraph.nodes.map((node: any) => ({
|
||||||
|
id: node.id,
|
||||||
|
type: node.type,
|
||||||
|
position: node.position || { x: 0, y: 0 },
|
||||||
|
data: node.data || {},
|
||||||
|
})),
|
||||||
|
edges: backendGraph.edges || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new edge between two nodes
|
||||||
|
*/
|
||||||
|
export function createEdge(source: string, target: string, id?: string): FlowgramEdge {
|
||||||
|
return {
|
||||||
|
id: id || `edge-${source}-${target}`,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate workflow graph structure
|
||||||
|
*/
|
||||||
|
export function validateWorkflow(graph: FlowgramGraph): { valid: boolean; errors: string[] } {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Check for at least one node
|
||||||
|
if (!graph.nodes || graph.nodes.length === 0) {
|
||||||
|
errors.push('工作流至少需要一个节点');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for trigger or start node
|
||||||
|
const hasTrigger = graph.nodes.some((n) => n.type === 'trigger' || n.type === 'start');
|
||||||
|
if (!hasTrigger) {
|
||||||
|
errors.push('工作流需要至少一个触发器或开始节点');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cycles (simple check)
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const recStack = new Set<string>();
|
||||||
|
|
||||||
|
function hasCycle(nodeId: string): boolean {
|
||||||
|
visited.add(nodeId);
|
||||||
|
recStack.add(nodeId);
|
||||||
|
|
||||||
|
const outgoingEdges = graph.edges.filter((e) => e.source === nodeId);
|
||||||
|
for (const edge of outgoingEdges) {
|
||||||
|
if (!visited.has(edge.target)) {
|
||||||
|
if (hasCycle(edge.target)) return true;
|
||||||
|
} else if (recStack.has(edge.target)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recStack.delete(nodeId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of graph.nodes) {
|
||||||
|
if (!visited.has(node.id) && hasCycle(node.id)) {
|
||||||
|
errors.push('工作流包含循环依赖');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for disconnected nodes
|
||||||
|
const connectedNodes = new Set<string>();
|
||||||
|
graph.edges.forEach((edge) => {
|
||||||
|
connectedNodes.add(edge.source);
|
||||||
|
connectedNodes.add(edge.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
const disconnectedNodes = graph.nodes.filter(
|
||||||
|
(node) => !connectedNodes.has(node.id) && graph.nodes.length > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (disconnectedNodes.length > 0) {
|
||||||
|
errors.push(`发现 ${disconnectedNodes.length} 个未连接的节点`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
107
src/pages/scenario/flowgramWorkflowModal.tsx
Normal file
107
src/pages/scenario/flowgramWorkflowModal.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Form, Input, Switch, message } from 'antd';
|
||||||
|
import FlowgramEditor from './flowgram/Editor';
|
||||||
|
import type { FlowgramGraph } from './flowgram/types';
|
||||||
|
|
||||||
|
interface FlowgramWorkflowModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
scenario?: any;
|
||||||
|
onCancel: () => void;
|
||||||
|
onOk: (values: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlowgramWorkflowModal: React.FC<FlowgramWorkflowModalProps> = ({
|
||||||
|
visible,
|
||||||
|
scenario,
|
||||||
|
onCancel,
|
||||||
|
onOk,
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [workflowGraph, setWorkflowGraph] = useState<FlowgramGraph | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && scenario) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: scenario.name,
|
||||||
|
isDisabled: scenario.isDisabled === 1,
|
||||||
|
});
|
||||||
|
if (scenario.workflowGraph) {
|
||||||
|
setWorkflowGraph(scenario.workflowGraph);
|
||||||
|
}
|
||||||
|
} else if (visible) {
|
||||||
|
form.resetFields();
|
||||||
|
setWorkflowGraph(undefined);
|
||||||
|
}
|
||||||
|
}, [visible, scenario, form]);
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
|
||||||
|
// Validate workflow has nodes
|
||||||
|
if (!workflowGraph || workflowGraph.nodes.length === 0) {
|
||||||
|
message.error('请添加至少一个节点到工作流');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
workflowGraph,
|
||||||
|
isDisabled: values.isDisabled ? 1 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (scenario) {
|
||||||
|
submitData.id = scenario.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await onOk(submitData);
|
||||||
|
form.resetFields();
|
||||||
|
setWorkflowGraph(undefined);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单验证失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
setWorkflowGraph(undefined);
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={scenario ? '编辑场景' : '新建场景'}
|
||||||
|
open={visible}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
confirmLoading={loading}
|
||||||
|
width={1000}
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="场景名称"
|
||||||
|
rules={[{ required: true, message: '请输入场景名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="输入场景名称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="isDisabled" label="启用状态" valuePropName="checked">
|
||||||
|
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="工作流设计">
|
||||||
|
<FlowgramEditor value={workflowGraph} onChange={setWorkflowGraph} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlowgramWorkflowModal;
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import ScenarioModal from './visualWorkflowModal';
|
import ScenarioModal from './flowgramWorkflowModal';
|
||||||
import ScenarioLogModal from './logModal';
|
import ScenarioLogModal from './logModal';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user