mirror of
https://github.com/whyour/qinglong.git
synced 2025-11-10 00:26:09 +08:00
Integrate Flowgram visual workflow editor: update data models and add dependencies
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
3b95ea64d3
commit
fffc1e4fd1
|
|
@ -30,13 +30,14 @@ export default (app: Router) => {
|
|||
body: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string().optional().allow(''),
|
||||
workflowGraph: Joi.object().optional(),
|
||||
triggerType: Joi.string()
|
||||
.valid('variable', 'webhook', 'task_status', 'time', 'system_event')
|
||||
.required(),
|
||||
.optional(),
|
||||
triggerConfig: Joi.object().optional(),
|
||||
conditionLogic: Joi.string().valid('AND', 'OR').default('AND'),
|
||||
conditions: Joi.array().optional().default([]),
|
||||
actions: Joi.array().required(),
|
||||
actions: Joi.array().optional(),
|
||||
retryStrategy: Joi.object({
|
||||
maxRetries: Joi.number().min(0).max(10),
|
||||
retryDelay: Joi.number().min(1),
|
||||
|
|
@ -67,6 +68,7 @@ export default (app: Router) => {
|
|||
id: Joi.number().required(),
|
||||
name: Joi.string().optional(),
|
||||
description: Joi.string().optional().allow(''),
|
||||
workflowGraph: Joi.object().optional(),
|
||||
triggerType: Joi.string()
|
||||
.valid('variable', 'webhook', 'task_status', 'time', 'system_event')
|
||||
.optional(),
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ export class Scenario {
|
|||
name: string;
|
||||
description?: string;
|
||||
isEnabled?: 1 | 0;
|
||||
triggerType?: string; // 'variable' | 'webhook' | 'task_status' | 'time' | 'system_event'
|
||||
triggerConfig?: any; // JSON configuration for the trigger
|
||||
conditionLogic?: 'AND' | 'OR';
|
||||
conditions?: any[]; // Array of condition objects
|
||||
actions?: any[]; // Array of actions to execute
|
||||
workflowGraph?: any; // Flowgram workflow graph structure
|
||||
triggerType?: string; // Deprecated: kept for backward compatibility
|
||||
triggerConfig?: any; // Deprecated: kept for backward compatibility
|
||||
conditionLogic?: 'AND' | 'OR'; // Deprecated: kept for backward compatibility
|
||||
conditions?: any[]; // Deprecated: kept for backward compatibility
|
||||
actions?: any[]; // Deprecated: kept for backward compatibility
|
||||
retryStrategy?: {
|
||||
maxRetries: number;
|
||||
retryDelay: number; // in seconds
|
||||
|
|
@ -33,6 +34,7 @@ export class Scenario {
|
|||
this.name = options.name;
|
||||
this.description = options.description;
|
||||
this.isEnabled = options.isEnabled ?? 1;
|
||||
this.workflowGraph = options.workflowGraph || null;
|
||||
this.triggerType = options.triggerType;
|
||||
this.triggerConfig = options.triggerConfig;
|
||||
this.conditionLogic = options.conditionLogic || 'AND';
|
||||
|
|
@ -64,9 +66,13 @@ export const ScenarioModel = sequelize.define<ScenarioInstance>('Scenario', {
|
|||
type: DataTypes.NUMBER,
|
||||
defaultValue: 1,
|
||||
},
|
||||
workflowGraph: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
},
|
||||
triggerType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
},
|
||||
triggerConfig: {
|
||||
type: DataTypes.JSON,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ export default async () => {
|
|||
try {
|
||||
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query('alter table Scenarios add column workflowGraph JSON');
|
||||
} catch (error) {}
|
||||
|
||||
Logger.info('✌️ DB loaded');
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@
|
|||
"helmet": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flowgram.ai/free-layout-editor": "^1.0.2",
|
||||
"@flowgram.ai/core": "^1.0.2",
|
||||
"@flowgram.ai/reactive": "^1.0.2",
|
||||
"moment": "2.30.1",
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/pro-layout": "6.38.22",
|
||||
|
|
|
|||
196
src/pages/scenario/flowgramModal.tsx
Normal file
196
src/pages/scenario/flowgramModal.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
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';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface ScenarioModalProps {
|
||||
visible: boolean;
|
||||
scenario: any;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ScenarioModal: React.FC<ScenarioModalProps> = ({
|
||||
visible,
|
||||
scenario,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const editorRef = useRef<any>(null);
|
||||
const [workflowGraph, setWorkflowGraph] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (scenario) {
|
||||
form.setFieldsValue({
|
||||
name: scenario.name,
|
||||
description: scenario.description || '',
|
||||
});
|
||||
setWorkflowGraph(scenario.workflowGraph || getInitialWorkflow());
|
||||
} else {
|
||||
form.resetFields();
|
||||
setWorkflowGraph(getInitialWorkflow());
|
||||
}
|
||||
}
|
||||
}, [visible, scenario, form]);
|
||||
|
||||
const getInitialWorkflow = () => {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger-1',
|
||||
type: 'trigger',
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
label: intl.get('触发器'),
|
||||
triggerType: 'time',
|
||||
config: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
// Get workflow data from editor
|
||||
const currentWorkflow = editorRef.current?.getData();
|
||||
|
||||
const endpoint = scenario ? '/api/scenarios' : '/api/scenarios';
|
||||
const method = scenario ? 'put' : 'post';
|
||||
const payload = {
|
||||
...values,
|
||||
workflowGraph: currentWorkflow || 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 handleWorkflowChange = (newWorkflow: any) => {
|
||||
setWorkflowGraph(newWorkflow);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={scenario ? intl.get('编辑场景') : intl.get('新建场景')}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSubmit}
|
||||
confirmLoading={loading}
|
||||
width={1200}
|
||||
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={{
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4,
|
||||
height: 500,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<FreeLayoutEditor
|
||||
ref={editorRef}
|
||||
data={workflowGraph}
|
||||
onChange={handleWorkflowChange}
|
||||
nodeTypes={{
|
||||
trigger: {
|
||||
render: (node: any) => (
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
background: '#1890ff',
|
||||
color: 'white',
|
||||
borderRadius: 4,
|
||||
minWidth: 150,
|
||||
}}
|
||||
>
|
||||
<div>{node.data.label}</div>
|
||||
<div style={{ fontSize: 12, marginTop: 4 }}>
|
||||
{node.data.triggerType}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
condition: {
|
||||
render: (node: any) => (
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
background: '#52c41a',
|
||||
color: 'white',
|
||||
borderRadius: 4,
|
||||
minWidth: 150,
|
||||
}}
|
||||
>
|
||||
<div>{node.data.label}</div>
|
||||
<div style={{ fontSize: 12, marginTop: 4 }}>
|
||||
{node.data.operator}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
action: {
|
||||
render: (node: any) => (
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
background: '#fa8c16',
|
||||
color: 'white',
|
||||
borderRadius: 4,
|
||||
minWidth: 150,
|
||||
}}
|
||||
>
|
||||
<div>{node.data.label}</div>
|
||||
<div style={{ fontSize: 12, marginTop: 4 }}>
|
||||
{node.data.actionType}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScenarioModal;
|
||||
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from '@ant-design/icons';
|
||||
import { request } from '@/utils/http';
|
||||
import intl from 'react-intl-universal';
|
||||
import ScenarioModal from './modal';
|
||||
import ScenarioModal from './flowgramModal';
|
||||
import ScenarioLogModal from './logModal';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user