mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-15 08:25:38 +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';
|
||||
import { request } from '@/utils/http';
|
||||
import intl from 'react-intl-universal';
|
||||
import ScenarioModal from './visualWorkflowModal';
|
||||
import ScenarioModal from './flowgramWorkflowModal';
|
||||
import ScenarioLogModal from './logModal';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user