Refactor backend and frontend: Added migration scripts, updated dependencies, improved error handling, and enhanced workflow editor functionality. Removed version from docker-compose file.
This commit is contained in:
parent
74fb343ead
commit
1c317d1a1d
30
backend/knexfile.js
Normal file
30
backend/knexfile.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
development: {
|
||||||
|
client: 'pg',
|
||||||
|
connection: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/flowforge',
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: 'knex_migrations',
|
||||||
|
directory: './migrations'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
production: {
|
||||||
|
client: 'pg',
|
||||||
|
connection: process.env.DATABASE_URL,
|
||||||
|
pool: {
|
||||||
|
min: 2,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: 'knex_migrations',
|
||||||
|
directory: './migrations'
|
||||||
|
},
|
||||||
|
ssl: process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false
|
||||||
|
}
|
||||||
|
};
|
@ -70,3 +70,133 @@
|
|||||||
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-07 08:55:59"}
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-07 08:55:59"}
|
||||||
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-07 08:56:07"}
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-07 08:56:07"}
|
||||||
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-07 08:56:07"}
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-07 08:56:07"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:30:14"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:30:14"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:31:23"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:31:23"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:31:25"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:31:25"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:31:35"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:31:35"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:32:07"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:32:07"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"error":"Cannot find module 'nodemailer'\nRequire stack:\n- /app/src/nodes/email/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"error":"Cannot find module 'vm2'\nRequire stack:\n- /app/src/nodes/function/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"error":"Cannot find module 'axios'\nRequire stack:\n- /app/src/nodes/http-request/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 3 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"error":"Cannot find module 'nodemailer'\nRequire stack:\n- /app/src/nodes/email/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"error":"Cannot find module 'vm2'\nRequire stack:\n- /app/src/nodes/function/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"error":"Cannot find module 'axios'\nRequire stack:\n- /app/src/nodes/http-request/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 3 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:57:20"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:57:22"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:57:26"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:58:10"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"error":"Cannot find module 'isolated-vm'\nRequire stack:\n- /app/src/nodes/function/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 5 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:58:40"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 12:58:47"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 13:06:51"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 13:50:21"}
|
||||||
|
{"level":"info","message":"Server running on port 4000","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"info","message":"Initializing node registry","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: delay","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: email","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: function","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: logger","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"debug","message":"Loaded node type: webhook","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"info","message":"Node registry initialized with 6 node types","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
{"level":"info","message":"Database connection established successfully","service":"flowforge-backend","timestamp":"2025-06-08 13:50:29"}
|
||||||
|
@ -32,3 +32,10 @@
|
|||||||
{"level":"error","message":"SyntaxError: Unexpected token o in JSON at position 1","method":"GET","path":"/api/workflows/971f09e0-6490-4be3-a6cb-928e9f2ca5a5","service":"flowforge-backend","stack":"SyntaxError: Unexpected token o in JSON at position 1\n at JSON.parse (<anonymous>)\n at getWorkflowById (/app/src/models/workflow.js:96:17)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async getById (/app/src/controllers/workflow.js:75:22)","timestamp":"2025-06-07 08:51:08"}
|
{"level":"error","message":"SyntaxError: Unexpected token o in JSON at position 1","method":"GET","path":"/api/workflows/971f09e0-6490-4be3-a6cb-928e9f2ca5a5","service":"flowforge-backend","stack":"SyntaxError: Unexpected token o in JSON at position 1\n at JSON.parse (<anonymous>)\n at getWorkflowById (/app/src/models/workflow.js:96:17)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async getById (/app/src/controllers/workflow.js:75:22)","timestamp":"2025-06-07 08:51:08"}
|
||||||
{"level":"error","message":"SyntaxError: Unexpected token o in JSON at position 1","method":"GET","path":"/api/workflows/7140abc0-968a-4728-925b-27f49f139b6d","service":"flowforge-backend","stack":"SyntaxError: Unexpected token o in JSON at position 1\n at JSON.parse (<anonymous>)\n at getWorkflowById (/app/src/models/workflow.js:96:17)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async getById (/app/src/controllers/workflow.js:75:22)","timestamp":"2025-06-07 08:51:12"}
|
{"level":"error","message":"SyntaxError: Unexpected token o in JSON at position 1","method":"GET","path":"/api/workflows/7140abc0-968a-4728-925b-27f49f139b6d","service":"flowforge-backend","stack":"SyntaxError: Unexpected token o in JSON at position 1\n at JSON.parse (<anonymous>)\n at getWorkflowById (/app/src/models/workflow.js:96:17)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async getById (/app/src/controllers/workflow.js:75:22)","timestamp":"2025-06-07 08:51:12"}
|
||||||
{"level":"error","message":"SyntaxError: Unexpected token o in JSON at position 1","method":"GET","path":"/api/workflows/7140abc0-968a-4728-925b-27f49f139b6d","service":"flowforge-backend","stack":"SyntaxError: Unexpected token o in JSON at position 1\n at JSON.parse (<anonymous>)\n at getWorkflowById (/app/src/models/workflow.js:96:17)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async getById (/app/src/controllers/workflow.js:75:22)","timestamp":"2025-06-07 08:51:12"}
|
{"level":"error","message":"SyntaxError: Unexpected token o in JSON at position 1","method":"GET","path":"/api/workflows/7140abc0-968a-4728-925b-27f49f139b6d","service":"flowforge-backend","stack":"SyntaxError: Unexpected token o in JSON at position 1\n at JSON.parse (<anonymous>)\n at getWorkflowById (/app/src/models/workflow.js:96:17)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async getById (/app/src/controllers/workflow.js:75:22)","timestamp":"2025-06-07 08:51:12"}
|
||||||
|
{"error":"Cannot find module 'nodemailer'\nRequire stack:\n- /app/src/nodes/email/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"error":"Cannot find module 'vm2'\nRequire stack:\n- /app/src/nodes/function/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"error":"Cannot find module 'axios'\nRequire stack:\n- /app/src/nodes/http-request/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:56:22"}
|
||||||
|
{"error":"Cannot find module 'nodemailer'\nRequire stack:\n- /app/src/nodes/email/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: email","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"error":"Cannot find module 'vm2'\nRequire stack:\n- /app/src/nodes/function/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"error":"Cannot find module 'axios'\nRequire stack:\n- /app/src/nodes/http-request/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: http-request","service":"flowforge-backend","timestamp":"2025-06-08 12:57:09"}
|
||||||
|
{"error":"Cannot find module 'isolated-vm'\nRequire stack:\n- /app/src/nodes/function/runner.js\n- /app/src/services/nodeRegistry.js\n- /app/src/index.js","level":"error","message":"Failed to load node type: function","service":"flowforge-backend","timestamp":"2025-06-08 12:58:29"}
|
||||||
|
5751
backend/package-lock.json
generated
Normal file
5751
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,9 +6,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"dev": "nodemon src/index.js",
|
"dev": "nodemon src/index.js",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"migrate": "knex migrate:latest",
|
||||||
|
"migrate:rollback": "knex migrate:rollback",
|
||||||
|
"migrate:status": "knex migrate:status"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"bull": "^4.10.4",
|
"bull": "^4.10.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@ -16,8 +20,10 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"helmet": "^6.1.5",
|
"helmet": "^6.1.5",
|
||||||
|
"isolated-vm": "^5.0.1",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
|
"nodemailer": "^6.9.3",
|
||||||
"pg": "^8.10.0",
|
"pg": "^8.10.0",
|
||||||
"redis": "^4.6.6",
|
"redis": "^4.6.6",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
@ -5,6 +5,7 @@ const helmet = require('helmet');
|
|||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const { errorHandler } = require('./middleware/errorHandler');
|
const { errorHandler } = require('./middleware/errorHandler');
|
||||||
const logger = require('./utils/logger');
|
const logger = require('./utils/logger');
|
||||||
|
const nodeRegistry = require('./services/nodeRegistry');
|
||||||
|
|
||||||
// Import routes
|
// Import routes
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
@ -24,7 +25,7 @@ app.use(express.json());
|
|||||||
// Rate limiting
|
// Rate limiting
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 100, // limit each IP to 100 requests per windowMs
|
max: 1000, // limit each IP to 1000 requests per windowMs (increased for dev)
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
});
|
});
|
||||||
@ -44,9 +45,18 @@ app.get('/health', (req, res) => {
|
|||||||
// Error handling middleware
|
// Error handling middleware
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
const initializeServices = async () => {
|
||||||
|
// Initialize node registry
|
||||||
|
await nodeRegistry.initialize();
|
||||||
|
};
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, async () => {
|
||||||
logger.info(`Server running on port ${PORT}`);
|
logger.info(`Server running on port ${PORT}`);
|
||||||
|
|
||||||
|
// Initialize services after server starts
|
||||||
|
await initializeServices();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle unhandled promise rejections
|
// Handle unhandled promise rejections
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { VM } = require('vm2');
|
const ivm = require('isolated-vm');
|
||||||
const logger = require('../../utils/logger');
|
const logger = require('../../utils/logger');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,53 +17,57 @@ async function run(nodeConfig, inputData) {
|
|||||||
// Set timeout (default: 5000ms)
|
// Set timeout (default: 5000ms)
|
||||||
const timeout = nodeConfig.timeout || 5000;
|
const timeout = nodeConfig.timeout || 5000;
|
||||||
|
|
||||||
// Create a sandboxed VM
|
// Create a new isolated context
|
||||||
const vm = new VM({
|
const isolate = new ivm.Isolate({ memoryLimit: 32 });
|
||||||
timeout,
|
const context = await isolate.createContext();
|
||||||
sandbox: {
|
|
||||||
// Provide input data to the sandbox
|
// Get a reference to the global object
|
||||||
input: inputData.input || {},
|
const jail = context.global;
|
||||||
// Provide console methods that log to our logger
|
|
||||||
console: {
|
// Add input data to the context
|
||||||
|
await jail.set('input', new ivm.ExternalCopy(inputData.input || {}).copyInto());
|
||||||
|
|
||||||
|
// Add console methods that log to our logger
|
||||||
|
const logMethods = {
|
||||||
log: (...args) => logger.info('Function node log:', ...args),
|
log: (...args) => logger.info('Function node log:', ...args),
|
||||||
info: (...args) => logger.info('Function node info:', ...args),
|
info: (...args) => logger.info('Function node info:', ...args),
|
||||||
warn: (...args) => logger.warn('Function node warn:', ...args),
|
warn: (...args) => logger.warn('Function node warn:', ...args),
|
||||||
error: (...args) => logger.error('Function node error:', ...args)
|
error: (...args) => logger.error('Function node error:', ...args)
|
||||||
}
|
};
|
||||||
},
|
|
||||||
// Prevent access to Node.js internal modules
|
|
||||||
require: {
|
|
||||||
external: false,
|
|
||||||
builtin: ['path', 'util', 'buffer'],
|
|
||||||
root: "./",
|
|
||||||
mock: {
|
|
||||||
fs: {
|
|
||||||
readFileSync: () => 'Not allowed'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wrap the code in an async function
|
await jail.set('console', new ivm.ExternalCopy(logMethods).copyInto());
|
||||||
|
|
||||||
|
// Wrap the code in an async function that returns the result
|
||||||
const wrappedCode = `
|
const wrappedCode = `
|
||||||
(async function() {
|
(async function() {
|
||||||
|
try {
|
||||||
${nodeConfig.code}
|
${nodeConfig.code}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Execute the code
|
// Execute the code with timeout
|
||||||
const result = await vm.run(wrappedCode);
|
const script = await isolate.compileScript(wrappedCode);
|
||||||
|
const result = await script.run(context, { timeout });
|
||||||
|
|
||||||
// Validate the result
|
// Handle the result
|
||||||
if (result === undefined || result === null) {
|
let output = {};
|
||||||
return {
|
if (result !== undefined && result !== null) {
|
||||||
output: {}
|
// If result is a primitive value, wrap it
|
||||||
};
|
if (typeof result === 'object') {
|
||||||
|
output = result;
|
||||||
|
} else {
|
||||||
|
output = { value: result };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the result
|
// Clean up
|
||||||
|
isolate.dispose();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
output: result
|
output: output
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Function node error', { error: error.message });
|
logger.error('Function node error', { error: error.message });
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
|
15260
frontend/package-lock.json
generated
Normal file
15260
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
|||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"reactflow": "^11.7.0",
|
"reactflow": "^11.10.4",
|
||||||
"react-router-dom": "^6.14.1",
|
"react-router-dom": "^6.14.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
|
@ -31,7 +31,7 @@ const getNodeIcon = (type) => {
|
|||||||
case 'http-request':
|
case 'http-request':
|
||||||
return (
|
return (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
case 'function':
|
case 'function':
|
||||||
@ -67,21 +67,66 @@ const getNodeIcon = (type) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomNode = ({ data, selected }) => {
|
const CustomNode = ({ id, data, selected, onEdit, onDelete }) => {
|
||||||
const nodeColor = getNodeColor(data.nodeType);
|
const nodeColor = getNodeColor(data.nodeType);
|
||||||
const nodeIcon = getNodeIcon(data.nodeType);
|
const nodeIcon = getNodeIcon(data.nodeType);
|
||||||
|
|
||||||
|
const handleEdit = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onEdit) {
|
||||||
|
onEdit(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onDelete) {
|
||||||
|
onDelete(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${selected ? 'ring-2 ring-primary-400' : ''} bg-white rounded-md shadow-md border-l-4 ${nodeColor} p-3 min-w-[180px]`}>
|
<div
|
||||||
|
className={`group relative ${selected ? 'ring-2 ring-primary-400' : ''} bg-white rounded-md shadow-md border-l-4 ${nodeColor} p-3 min-w-[180px] cursor-grab active:cursor-grabbing hover:shadow-lg transition-shadow`}
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
|
>
|
||||||
|
{/* Action buttons - show on hover or when selected */}
|
||||||
|
<div className={`absolute -top-2 -right-2 flex space-x-1 transition-opacity ${selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}>
|
||||||
|
{/* Edit button */}
|
||||||
|
<button
|
||||||
|
onClick={handleEdit}
|
||||||
|
className="w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center shadow-md transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
|
title="Edit node"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Delete button */}
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="w-6 h-6 bg-red-500 hover:bg-red-600 text-white rounded-full flex items-center justify-center shadow-md transition-colors"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
|
title="Delete node"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Input handle */}
|
{/* Input handle */}
|
||||||
<Handle
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Top}
|
position={Position.Top}
|
||||||
className="w-3 h-3 rounded-full bg-gray-400 border-2 border-white"
|
className="w-3 h-3 rounded-full bg-gray-400 border-2 border-white"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Node content */}
|
{/* Node content */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center" style={{ pointerEvents: 'none' }}>
|
||||||
<div className="flex-shrink-0 text-gray-500">
|
<div className="flex-shrink-0 text-gray-500">
|
||||||
{nodeIcon}
|
{nodeIcon}
|
||||||
</div>
|
</div>
|
||||||
@ -100,6 +145,7 @@ const CustomNode = ({ data, selected }) => {
|
|||||||
type="source"
|
type="source"
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
className="w-3 h-3 rounded-full bg-gray-400 border-2 border-white"
|
className="w-3 h-3 rounded-full bg-gray-400 border-2 border-white"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -20,6 +20,13 @@ code {
|
|||||||
/* Custom styles for react-flow */
|
/* Custom styles for react-flow */
|
||||||
.react-flow__node {
|
.react-flow__node {
|
||||||
@apply shadow-md rounded-md;
|
@apply shadow-md rounded-md;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__node-default {
|
.react-flow__node-default {
|
||||||
@ -28,6 +35,7 @@ code {
|
|||||||
|
|
||||||
.react-flow__handle {
|
.react-flow__handle {
|
||||||
@apply w-3 h-3;
|
@apply w-3 h-3;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-flow__handle-top {
|
.react-flow__handle-top {
|
||||||
@ -46,6 +54,11 @@ code {
|
|||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure React Flow pane is properly configured */
|
||||||
|
.react-flow__pane {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
/* Node types */
|
/* Node types */
|
||||||
.node-webhook {
|
.node-webhook {
|
||||||
@apply border-l-4 border-l-blue-500;
|
@apply border-l-4 border-l-blue-500;
|
||||||
|
@ -48,10 +48,18 @@ const WorkflowEditor = () => {
|
|||||||
const [latestExecutionId, setLatestExecutionId] = useState(null);
|
const [latestExecutionId, setLatestExecutionId] = useState(null);
|
||||||
const [currentVersion, setCurrentVersion] = useState(1);
|
const [currentVersion, setCurrentVersion] = useState(1);
|
||||||
const [showTabs, setShowTabs] = useState(true);
|
const [showTabs, setShowTabs] = useState(true);
|
||||||
|
const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1 });
|
||||||
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||||
|
|
||||||
// Node type definitions for React Flow
|
// Node type definitions for React Flow
|
||||||
const nodeTypeDefinitions = {
|
const nodeTypeDefinitions = {
|
||||||
customNode: CustomNode
|
customNode: (nodeProps) => (
|
||||||
|
<CustomNode
|
||||||
|
{...nodeProps}
|
||||||
|
onEdit={handleEditNode}
|
||||||
|
onDelete={handleDeleteNode}
|
||||||
|
/>
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load workflow if editing existing one
|
// Load workflow if editing existing one
|
||||||
@ -82,12 +90,17 @@ const WorkflowEditor = () => {
|
|||||||
const flowNodes = workflowData.nodes.map(node => ({
|
const flowNodes = workflowData.nodes.map(node => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: 'customNode',
|
type: 'customNode',
|
||||||
position: { x: node.position_x, y: node.position_y },
|
position: {
|
||||||
|
x: Number(node.position_x) || 0,
|
||||||
|
y: Number(node.position_y) || 0
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
label: node.name,
|
label: node.name,
|
||||||
nodeType: node.type,
|
nodeType: node.type,
|
||||||
config: node.config || {}
|
config: node.config || {}
|
||||||
}
|
},
|
||||||
|
draggable: true,
|
||||||
|
selectable: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const flowEdges = workflowData.connections.map(conn => ({
|
const flowEdges = workflowData.connections.map(conn => ({
|
||||||
@ -110,7 +123,7 @@ const WorkflowEditor = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [id]);
|
}, [id, setNodes, setEdges]);
|
||||||
|
|
||||||
// Handle connections between nodes
|
// Handle connections between nodes
|
||||||
const onConnect = useCallback((params) => {
|
const onConnect = useCallback((params) => {
|
||||||
@ -151,6 +164,8 @@ const WorkflowEditor = () => {
|
|||||||
nodeType: nodeData.type,
|
nodeType: nodeData.type,
|
||||||
config: {}
|
config: {}
|
||||||
},
|
},
|
||||||
|
draggable: true,
|
||||||
|
selectable: true
|
||||||
};
|
};
|
||||||
|
|
||||||
setNodes((nds) => nds.concat(newNode));
|
setNodes((nds) => nds.concat(newNode));
|
||||||
@ -283,18 +298,35 @@ const WorkflowEditor = () => {
|
|||||||
position: {
|
position: {
|
||||||
x: node.position.x + 50,
|
x: node.position.x + 50,
|
||||||
y: node.position.y + 50
|
y: node.position.y + 50
|
||||||
}
|
},
|
||||||
|
draggable: true,
|
||||||
|
selectable: true
|
||||||
};
|
};
|
||||||
|
|
||||||
setNodes((nds) => nds.concat(newNode));
|
setNodes((nds) => nds.concat(newNode));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete a node
|
// Handle node editing
|
||||||
const handleDeleteNode = (node) => {
|
const handleEditNode = useCallback((nodeId) => {
|
||||||
setNodes((nds) => nds.filter((n) => n.id !== node.id));
|
const node = nodes.find(n => n.id === nodeId);
|
||||||
setEdges((eds) => eds.filter((e) => e.source !== node.id && e.target !== node.id));
|
if (node) {
|
||||||
|
setSelectedNode(node);
|
||||||
|
}
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
|
// Handle node deletion
|
||||||
|
const handleDeleteNode = useCallback((nodeId) => {
|
||||||
|
// Show confirmation dialog
|
||||||
|
if (window.confirm('Are you sure you want to delete this node?')) {
|
||||||
|
setNodes((nds) => nds.filter((n) => n.id !== nodeId));
|
||||||
|
setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId));
|
||||||
|
|
||||||
|
// Clear selection if the deleted node was selected
|
||||||
|
if (selectedNode && selectedNode.id === nodeId) {
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
}, [setNodes, setEdges, selectedNode]);
|
||||||
|
|
||||||
// Handle workflow execution
|
// Handle workflow execution
|
||||||
const handleExecuteWorkflow = (executionId) => {
|
const handleExecuteWorkflow = (executionId) => {
|
||||||
@ -320,12 +352,17 @@ const WorkflowEditor = () => {
|
|||||||
const flowNodes = workflowData.nodes.map(node => ({
|
const flowNodes = workflowData.nodes.map(node => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: 'customNode',
|
type: 'customNode',
|
||||||
position: { x: node.position_x, y: node.position_y },
|
position: {
|
||||||
|
x: Number(node.position_x) || 0,
|
||||||
|
y: Number(node.position_y) || 0
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
label: node.name,
|
label: node.name,
|
||||||
nodeType: node.type,
|
nodeType: node.type,
|
||||||
config: node.config || {}
|
config: node.config || {}
|
||||||
}
|
},
|
||||||
|
draggable: true,
|
||||||
|
selectable: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const flowEdges = workflowData.connections.map(conn => ({
|
const flowEdges = workflowData.connections.map(conn => ({
|
||||||
@ -339,6 +376,8 @@ const WorkflowEditor = () => {
|
|||||||
setNodes(flowNodes);
|
setNodes(flowNodes);
|
||||||
setEdges(flowEdges);
|
setEdges(flowEdges);
|
||||||
setCurrentVersion(version);
|
setCurrentVersion(version);
|
||||||
|
|
||||||
|
// Don't reset viewport when restoring versions - preserve user's current view
|
||||||
toast.success(`Restored workflow to version ${version}`);
|
toast.success(`Restored workflow to version ${version}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restoring version:', error);
|
console.error('Error restoring version:', error);
|
||||||
@ -348,6 +387,55 @@ const WorkflowEditor = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle node changes
|
||||||
|
const handleNodesChange = useCallback((changes) => {
|
||||||
|
onNodesChange(changes);
|
||||||
|
}, [onNodesChange]);
|
||||||
|
|
||||||
|
// Handle edge changes
|
||||||
|
const handleEdgesChange = useCallback((changes) => {
|
||||||
|
onEdgesChange(changes);
|
||||||
|
}, [onEdgesChange]);
|
||||||
|
|
||||||
|
// Handle viewport changes to maintain pan/zoom state
|
||||||
|
const onViewportChange = useCallback((newViewport) => {
|
||||||
|
setViewport(newViewport);
|
||||||
|
// Save viewport to localStorage for this workflow
|
||||||
|
if (id && id !== 'new') {
|
||||||
|
localStorage.setItem(`workflow-viewport-${id}`, JSON.stringify(newViewport));
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// Load saved viewport for existing workflows
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && id !== 'new') {
|
||||||
|
const savedViewport = localStorage.getItem(`workflow-viewport-${id}`);
|
||||||
|
if (savedViewport) {
|
||||||
|
try {
|
||||||
|
const parsedViewport = JSON.parse(savedViewport);
|
||||||
|
setViewport(parsedViewport);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse saved viewport:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// Handle React Flow initialization
|
||||||
|
const onReactFlowInit = useCallback((reactFlowInstance) => {
|
||||||
|
setReactFlowInstance(reactFlowInstance);
|
||||||
|
|
||||||
|
// Only fit view on first load of new workflows, or if no saved viewport
|
||||||
|
if (isFirstLoad && (!id || id === 'new' || !localStorage.getItem(`workflow-viewport-${id}`))) {
|
||||||
|
setTimeout(() => {
|
||||||
|
reactFlowInstance.fitView({ padding: 0.1, maxZoom: 1.2 });
|
||||||
|
setIsFirstLoad(false);
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
setIsFirstLoad(false);
|
||||||
|
}
|
||||||
|
}, [isFirstLoad, id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-6rem)]">
|
<div className="h-[calc(100vh-6rem)]">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -387,7 +475,7 @@ const WorkflowEditor = () => {
|
|||||||
workflowId={id}
|
workflowId={id}
|
||||||
selectedNode={selectedNode}
|
selectedNode={selectedNode}
|
||||||
onDuplicateNode={handleDuplicateNode}
|
onDuplicateNode={handleDuplicateNode}
|
||||||
onDeleteNode={handleDeleteNode}
|
onDeleteNode={(node) => handleDeleteNode(node.id)}
|
||||||
onExecuteWorkflow={handleExecuteWorkflow}
|
onExecuteWorkflow={handleExecuteWorkflow}
|
||||||
onTestNode={() => setShowNodeTester(true)}
|
onTestNode={() => setShowNodeTester(true)}
|
||||||
/>
|
/>
|
||||||
@ -422,16 +510,27 @@ const WorkflowEditor = () => {
|
|||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={handleNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={handleEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
onInit={setReactFlowInstance}
|
onInit={onReactFlowInit}
|
||||||
|
onViewportChange={onViewportChange}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onPaneClick={onPaneClick}
|
onPaneClick={onPaneClick}
|
||||||
nodeTypes={nodeTypeDefinitions}
|
nodeTypes={nodeTypeDefinitions}
|
||||||
fitView
|
nodesDraggable={true}
|
||||||
|
nodesConnectable={true}
|
||||||
|
elementsSelectable={true}
|
||||||
|
panOnDrag={true}
|
||||||
|
panOnScroll={true}
|
||||||
|
zoomOnScroll={true}
|
||||||
|
zoomOnPinch={true}
|
||||||
|
zoomOnDoubleClick={false}
|
||||||
|
selectNodesOnDrag={false}
|
||||||
|
viewport={viewport}
|
||||||
|
fitViewOnInit={false}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user