mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-15 08:25:38 +08:00
fix flowgram style
This commit is contained in:
parent
326ea992e9
commit
cff9c1cf68
27558
pnpm-lock.yaml
27558
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -591,7 +591,7 @@
|
|||
"是": "Yes",
|
||||
"否": "No",
|
||||
"共": "Total",
|
||||
"项": "items"
|
||||
"项": "items",
|
||||
"日志名称": "Log Name",
|
||||
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate",
|
||||
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports absolute paths like /dev/null",
|
||||
|
|
|
|||
|
|
@ -591,7 +591,7 @@
|
|||
"是": "是",
|
||||
"否": "否",
|
||||
"共": "共",
|
||||
"项": "项"
|
||||
"项": "项",
|
||||
"日志名称": "日志名称",
|
||||
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成",
|
||||
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Modal, Form, Input, message } from 'antd';
|
|||
import { request } from '@/utils/http';
|
||||
import intl from 'react-intl-universal';
|
||||
import { FreeLayoutEditor } from '@flowgram.ai/free-layout-editor';
|
||||
import '@flowgram.ai/free-layout-editor/dist/index.css';
|
||||
import '@flowgram.ai/free-layout-editor/index.css';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ const Scenario = () => {
|
|||
title: intl.get('操作'),
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: any) => (
|
||||
<Dropdown menu={getActionsMenu(record)} trigger={['click']}>
|
||||
<Button type="link" icon={<EllipsisOutlined />} />
|
||||
|
|
@ -296,7 +296,7 @@ const Scenario = () => {
|
|||
setIsModalVisible(false);
|
||||
setSelectedScenario(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
onOk={() => {
|
||||
setIsModalVisible(false);
|
||||
setSelectedScenario(null);
|
||||
fetchScenarios();
|
||||
|
|
|
|||
|
|
@ -1,471 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Form, Input, message, Button, Drawer, Select, InputNumber, Space } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import intl from 'react-intl-universal';
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
Edge,
|
||||
Controls,
|
||||
Background,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
addEdge,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
Connection,
|
||||
BackgroundVariant,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import {
|
||||
NodeRenderers,
|
||||
NodeTemplates,
|
||||
createNode,
|
||||
WorkflowGraph,
|
||||
} from './nodeTypes';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface ScenarioModalProps {
|
||||
visible: boolean;
|
||||
scenario: any;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const VisualWorkflowModal: React.FC<ScenarioModalProps> = ({
|
||||
visible,
|
||||
scenario,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [edges, setEdges] = useState<Edge[]>([]);
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [nodeConfigForm] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (scenario) {
|
||||
form.setFieldsValue({
|
||||
name: scenario.name,
|
||||
description: scenario.description || '',
|
||||
});
|
||||
|
||||
if (scenario.workflowGraph) {
|
||||
setNodes(scenario.workflowGraph.nodes || []);
|
||||
setEdges(scenario.workflowGraph.edges || []);
|
||||
} else {
|
||||
initializeDefaultWorkflow();
|
||||
}
|
||||
} else {
|
||||
form.resetFields();
|
||||
initializeDefaultWorkflow();
|
||||
}
|
||||
}
|
||||
}, [visible, scenario, form]);
|
||||
|
||||
const initializeDefaultWorkflow = () => {
|
||||
const triggerNode = createNode('trigger', { x: 250, y: 100 }, {
|
||||
label: intl.get('时间触发'),
|
||||
triggerType: 'time',
|
||||
config: { schedule: '0 0 * * *' },
|
||||
});
|
||||
|
||||
setNodes([triggerNode]);
|
||||
setEdges([]);
|
||||
};
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[],
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[],
|
||||
);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleAddNode = (type: string, template: any) => {
|
||||
const newNode = createNode(type, { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 }, template);
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
};
|
||||
|
||||
const handleNodeClick = (_event: React.MouseEvent, node: Node) => {
|
||||
setSelectedNode(node);
|
||||
nodeConfigForm.setFieldsValue(node.data);
|
||||
setDrawerVisible(true);
|
||||
};
|
||||
|
||||
const handleNodeConfigSave = async () => {
|
||||
try {
|
||||
const values = await nodeConfigForm.validateFields();
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === selectedNode?.id
|
||||
? { ...node, data: { ...node.data, ...values } }
|
||||
: node
|
||||
)
|
||||
);
|
||||
setDrawerVisible(false);
|
||||
message.success(intl.get('配置已保存'));
|
||||
} catch (error) {
|
||||
console.error('节点配置验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
const workflowGraph: WorkflowGraph = {
|
||||
nodes: nodes.map(node => ({
|
||||
id: node.id,
|
||||
type: node.type || 'default',
|
||||
position: node.position,
|
||||
data: node.data,
|
||||
})),
|
||||
edges: edges.map(edge => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
})),
|
||||
};
|
||||
|
||||
const endpoint = scenario ? '/api/scenarios' : '/api/scenarios';
|
||||
const method = scenario ? 'put' : 'post';
|
||||
const payload = {
|
||||
...values,
|
||||
workflowGraph,
|
||||
...(scenario ? { id: scenario.id } : {}),
|
||||
};
|
||||
|
||||
const { code } = await request[method](endpoint, payload);
|
||||
if (code === 200) {
|
||||
message.success(
|
||||
scenario ? intl.get('更新成功') : intl.get('创建成功'),
|
||||
);
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save scenario:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const nodeTypes = {
|
||||
trigger: ({ data }: any) => NodeRenderers.trigger({ data, id: '', type: 'trigger', position: { x: 0, y: 0 } }),
|
||||
condition: ({ data }: any) => NodeRenderers.condition({ data, id: '', type: 'condition', position: { x: 0, y: 0 } }),
|
||||
action: ({ data }: any) => NodeRenderers.action({ data, id: '', type: 'action', position: { x: 0, y: 0 } }),
|
||||
control: ({ data }: any) => NodeRenderers.control({ data, id: '', type: 'control', position: { x: 0, y: 0 } }),
|
||||
logic_gate: ({ data }: any) => NodeRenderers.logic_gate({ data, id: '', type: 'logic_gate', position: { x: 0, y: 0 } }),
|
||||
};
|
||||
|
||||
const renderNodeConfig = () => {
|
||||
if (!selectedNode) return null;
|
||||
|
||||
switch (selectedNode.type) {
|
||||
case 'trigger':
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="label" label={intl.get('名称')} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="triggerType" label={intl.get('触发类型')} rules={[{ required: true }]}>
|
||||
<Select>
|
||||
<Option value="time">{intl.get('时间触发')}</Option>
|
||||
<Option value="webhook">Webhook</Option>
|
||||
<Option value="variable">{intl.get('变量监听')}</Option>
|
||||
<Option value="task_status">{intl.get('任务状态')}</Option>
|
||||
<Option value="system_event">{intl.get('系统事件')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.triggerType !== curr.triggerType}>
|
||||
{({ getFieldValue }) => {
|
||||
const triggerType = getFieldValue('triggerType');
|
||||
if (triggerType === 'time') {
|
||||
return (
|
||||
<Form.Item name={['config', 'schedule']} label="Cron 表达式">
|
||||
<Input placeholder="0 0 * * *" />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (triggerType === 'variable') {
|
||||
return (
|
||||
<Form.Item name={['config', 'watchPath']} label={intl.get('监听路径')}>
|
||||
<Input placeholder="/path/to/watch" />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (triggerType === 'system_event') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item name={['config', 'eventType']} label={intl.get('事件类型')}>
|
||||
<Select>
|
||||
<Option value="disk_space">{intl.get('磁盘空间')}</Option>
|
||||
<Option value="memory">{intl.get('内存使用')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name={['config', 'threshold']} label={intl.get('阈值')}>
|
||||
<InputNumber min={0} max={100} addonAfter="%" />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'condition':
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="label" label={intl.get('名称')} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="field" label={intl.get('字段名')} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="operator" label={intl.get('操作符')} 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">{intl.get('包含')}</Option>
|
||||
<Option value="not_contains">{intl.get('不包含')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="value" label={intl.get('值')} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'action':
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="label" label={intl.get('名称')} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="actionType" label={intl.get('动作类型')} rules={[{ required: true }]}>
|
||||
<Select>
|
||||
<Option value="run_task">{intl.get('运行任务')}</Option>
|
||||
<Option value="set_variable">{intl.get('设置变量')}</Option>
|
||||
<Option value="execute_command">{intl.get('执行命令')}</Option>
|
||||
<Option value="send_notification">{intl.get('发送通知')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.actionType !== curr.actionType}>
|
||||
{({ getFieldValue }) => {
|
||||
const actionType = getFieldValue('actionType');
|
||||
if (actionType === 'run_task') {
|
||||
return (
|
||||
<Form.Item name="cronId" label={intl.get('任务 ID')}>
|
||||
<InputNumber min={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (actionType === 'set_variable') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="name" label={intl.get('变量名')}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="value" label={intl.get('变量值')}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
} else if (actionType === 'execute_command') {
|
||||
return (
|
||||
<Form.Item name="command" label={intl.get('命令')}>
|
||||
<TextArea rows={3} />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (actionType === 'send_notification') {
|
||||
return (
|
||||
<Form.Item name="message" label={intl.get('消息')}>
|
||||
<TextArea rows={3} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'control':
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="label" label={intl.get('名称')} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="controlType" label={intl.get('控制类型')} rules={[{ required: true }]}>
|
||||
<Select>
|
||||
<Option value="delay">{intl.get('延迟执行')}</Option>
|
||||
<Option value="retry">{intl.get('重试策略')}</Option>
|
||||
<Option value="circuit_breaker">{intl.get('熔断器')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.controlType !== curr.controlType}>
|
||||
{({ getFieldValue }) => {
|
||||
const controlType = getFieldValue('controlType');
|
||||
if (controlType === 'delay') {
|
||||
return (
|
||||
<Form.Item name="delaySeconds" label={intl.get('延迟时间')}>
|
||||
<InputNumber min={1} addonAfter={intl.get('秒')} />
|
||||
</Form.Item>
|
||||
);
|
||||
} else if (controlType === 'retry') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="maxRetries" label={intl.get('最大重试次数')}>
|
||||
<InputNumber min={1} max={10} />
|
||||
</Form.Item>
|
||||
<Form.Item name="retryDelay" label={intl.get('重试延迟')}>
|
||||
<InputNumber min={1} addonAfter={intl.get('秒')} />
|
||||
</Form.Item>
|
||||
<Form.Item name="backoffMultiplier" label={intl.get('退避倍数')}>
|
||||
<InputNumber min={1} step={0.5} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
} else if (controlType === 'circuit_breaker') {
|
||||
return (
|
||||
<Form.Item name="failureThreshold" label={intl.get('失败熔断阈值')}>
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={scenario ? intl.get('编辑场景') : intl.get('新建场景')}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={1400}
|
||||
destroyOnClose
|
||||
style={{ top: 20 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={intl.get('名称')}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label={intl.get('描述')}>
|
||||
<TextArea rows={2} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={intl.get('工作流设计')}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddNode('trigger', NodeTemplates.triggers.time)}
|
||||
>
|
||||
{intl.get('触发器')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddNode('condition', NodeTemplates.conditions.equals)}
|
||||
>
|
||||
{intl.get('条件')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddNode('action', NodeTemplates.actions.run_task)}
|
||||
>
|
||||
{intl.get('动作')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddNode('control', NodeTemplates.controls.delay)}
|
||||
>
|
||||
{intl.get('控制流')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleAddNode('logic_gate', NodeTemplates.logic_gates.and)}
|
||||
>
|
||||
{intl.get('逻辑门')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
height: 500,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={handleNodeClick}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Drawer
|
||||
title={intl.get('节点配置')}
|
||||
placement="right"
|
||||
onClose={() => setDrawerVisible(false)}
|
||||
open={drawerVisible}
|
||||
width={400}
|
||||
extra={
|
||||
<Button type="primary" onClick={handleNodeConfigSave}>
|
||||
{intl.get('保存')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form form={nodeConfigForm} layout="vertical">
|
||||
{renderNodeConfig()}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisualWorkflowModal;
|
||||
Loading…
Reference in New Issue
Block a user