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({
|
body: Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
description: Joi.string().optional().allow(''),
|
description: Joi.string().optional().allow(''),
|
||||||
|
workflowGraph: Joi.object().optional(),
|
||||||
triggerType: Joi.string()
|
triggerType: Joi.string()
|
||||||
.valid('variable', 'webhook', 'task_status', 'time', 'system_event')
|
.valid('variable', 'webhook', 'task_status', 'time', 'system_event')
|
||||||
.required(),
|
.optional(),
|
||||||
triggerConfig: Joi.object().optional(),
|
triggerConfig: Joi.object().optional(),
|
||||||
conditionLogic: Joi.string().valid('AND', 'OR').default('AND'),
|
conditionLogic: Joi.string().valid('AND', 'OR').default('AND'),
|
||||||
conditions: Joi.array().optional().default([]),
|
conditions: Joi.array().optional().default([]),
|
||||||
actions: Joi.array().required(),
|
actions: Joi.array().optional(),
|
||||||
retryStrategy: Joi.object({
|
retryStrategy: Joi.object({
|
||||||
maxRetries: Joi.number().min(0).max(10),
|
maxRetries: Joi.number().min(0).max(10),
|
||||||
retryDelay: Joi.number().min(1),
|
retryDelay: Joi.number().min(1),
|
||||||
|
|
@ -67,6 +68,7 @@ export default (app: Router) => {
|
||||||
id: Joi.number().required(),
|
id: Joi.number().required(),
|
||||||
name: Joi.string().optional(),
|
name: Joi.string().optional(),
|
||||||
description: Joi.string().optional().allow(''),
|
description: Joi.string().optional().allow(''),
|
||||||
|
workflowGraph: Joi.object().optional(),
|
||||||
triggerType: Joi.string()
|
triggerType: Joi.string()
|
||||||
.valid('variable', 'webhook', 'task_status', 'time', 'system_event')
|
.valid('variable', 'webhook', 'task_status', 'time', 'system_event')
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ export class Scenario {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
isEnabled?: 1 | 0;
|
isEnabled?: 1 | 0;
|
||||||
triggerType?: string; // 'variable' | 'webhook' | 'task_status' | 'time' | 'system_event'
|
workflowGraph?: any; // Flowgram workflow graph structure
|
||||||
triggerConfig?: any; // JSON configuration for the trigger
|
triggerType?: string; // Deprecated: kept for backward compatibility
|
||||||
conditionLogic?: 'AND' | 'OR';
|
triggerConfig?: any; // Deprecated: kept for backward compatibility
|
||||||
conditions?: any[]; // Array of condition objects
|
conditionLogic?: 'AND' | 'OR'; // Deprecated: kept for backward compatibility
|
||||||
actions?: any[]; // Array of actions to execute
|
conditions?: any[]; // Deprecated: kept for backward compatibility
|
||||||
|
actions?: any[]; // Deprecated: kept for backward compatibility
|
||||||
retryStrategy?: {
|
retryStrategy?: {
|
||||||
maxRetries: number;
|
maxRetries: number;
|
||||||
retryDelay: number; // in seconds
|
retryDelay: number; // in seconds
|
||||||
|
|
@ -33,6 +34,7 @@ export class Scenario {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
this.description = options.description;
|
this.description = options.description;
|
||||||
this.isEnabled = options.isEnabled ?? 1;
|
this.isEnabled = options.isEnabled ?? 1;
|
||||||
|
this.workflowGraph = options.workflowGraph || null;
|
||||||
this.triggerType = options.triggerType;
|
this.triggerType = options.triggerType;
|
||||||
this.triggerConfig = options.triggerConfig;
|
this.triggerConfig = options.triggerConfig;
|
||||||
this.conditionLogic = options.conditionLogic || 'AND';
|
this.conditionLogic = options.conditionLogic || 'AND';
|
||||||
|
|
@ -64,9 +66,13 @@ export const ScenarioModel = sequelize.define<ScenarioInstance>('Scenario', {
|
||||||
type: DataTypes.NUMBER,
|
type: DataTypes.NUMBER,
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
},
|
},
|
||||||
|
workflowGraph: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
triggerType: {
|
triggerType: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
triggerConfig: {
|
triggerConfig: {
|
||||||
type: DataTypes.JSON,
|
type: DataTypes.JSON,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,9 @@ export default async () => {
|
||||||
try {
|
try {
|
||||||
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
try {
|
||||||
|
await sequelize.query('alter table Scenarios add column workflowGraph JSON');
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
Logger.info('✌️ DB loaded');
|
Logger.info('✌️ DB loaded');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@
|
||||||
"helmet": "^8.1.0"
|
"helmet": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"moment": "2.30.1",
|
||||||
"@ant-design/icons": "^5.0.1",
|
"@ant-design/icons": "^5.0.1",
|
||||||
"@ant-design/pro-layout": "6.38.22",
|
"@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';
|
} 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 './modal';
|
import ScenarioModal from './flowgramModal';
|
||||||
import ScenarioLogModal from './logModal';
|
import ScenarioLogModal from './logModal';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user