Implement visual workflow editor with React Flow and custom node types

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-09 13:38:22 +00:00
parent 00b1bc71e0
commit 5ed2e5b809
5 changed files with 1252 additions and 28 deletions

View File

@ -54,12 +54,15 @@
} }
}, },
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.10.0",
"@grpc/grpc-js": "^1.14.0", "@grpc/grpc-js": "^1.14.0",
"@grpc/proto-loader": "^0.8.0", "@grpc/proto-loader": "^0.8.0",
"@keyv/sqlite": "^4.0.1",
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"celebrate": "^15.0.3", "celebrate": "^15.0.3",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.9.0", "cron-parser": "^4.9.0",
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
@ -69,54 +72,51 @@
"express-jwt": "^8.4.1", "express-jwt": "^8.4.1",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.4.1",
"express-urlrewrite": "^2.0.3", "express-urlrewrite": "^2.0.3",
"undici": "^7.9.0", "helmet": "^8.1.0",
"hpagent": "^1.2.0", "hpagent": "^1.2.0",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^3.0.3",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"ip2region": "2.3.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"keyv": "^5.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"@bufbuild/protobuf": "^2.10.0", "proper-lockfile": "^4.1.2",
"ps-tree": "^1.2.0", "ps-tree": "^1.2.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"request-ip": "3.3.0",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"toad-scheduler": "^3.0.1", "toad-scheduler": "^3.0.1",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"undici": "^7.9.0",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0"
"request-ip": "3.3.0",
"ip2region": "2.3.0",
"keyv": "^5.2.3",
"@keyv/sqlite": "^4.0.1",
"proper-lockfile": "^4.1.2",
"compression": "^1.7.4",
"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",
"@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",
"@codemirror/view": "^6.34.1",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.34.1",
"@flowgram.ai/core": "^1.0.2",
"@flowgram.ai/free-layout-editor": "^1.0.2",
"@flowgram.ai/reactive": "^1.0.2",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^2.0.2",
"react-router-dom": "6.26.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/file-saver": "2.0.2", "@types/file-saver": "2.0.2",
"@types/helmet": "^4.0.0",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
@ -124,17 +124,17 @@
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/proper-lockfile": "^4.1.4",
"@types/ps-tree": "^1.1.6",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4", "@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/request-ip": "0.0.41",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/request-ip": "0.0.41",
"@types/proper-lockfile": "^4.1.4",
"@types/ps-tree": "^1.1.6",
"@uiw/codemirror-extensions-langs": "^4.21.9", "@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.4.4", "@umijs/max": "^4.4.4",
@ -146,9 +146,9 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"react-hotkeys-hook": "^4.6.1",
"file-saver": "2.0.2", "file-saver": "2.0.2",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"moment": "2.30.1",
"monaco-editor": "0.33.0", "monaco-editor": "0.33.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^2.5.1", "prettier": "^2.5.1",
@ -164,16 +164,17 @@
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-hotkeys-hook": "^4.6.1",
"react-intl-universal": "^2.12.0", "react-intl-universal": "^2.12.0",
"react-router-dom": "6.26.1",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"reactflow": "^11.11.4",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"ts-proto": "^2.6.1", "ts-proto": "^2.6.1",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "5.2.2", "typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0", "virtualizedtableforantd4": "1.3.0"
"@types/compression": "^1.7.2",
"@types/helmet": "^4.0.0"
} }
} }

View File

@ -348,6 +348,9 @@ importers:
react-split-pane: react-split-pane:
specifier: ^0.1.92 specifier: ^0.1.92
version: 0.1.92(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 0.1.92(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
reactflow:
specifier: ^11.11.4
version: 11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
sockjs-client: sockjs-client:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.1 version: 1.6.1
@ -2030,6 +2033,42 @@ packages:
peerDependencies: peerDependencies:
react: '>=18' react: '>=18'
'@reactflow/background@11.3.14':
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/controls@11.2.14':
resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/core@11.11.4':
resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/minimap@11.7.14':
resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/node-resizer@2.2.14':
resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@reactflow/node-toolbar@1.3.14':
resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@remix-run/router@1.19.1': '@remix-run/router@1.19.1':
resolution: {integrity: sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==} resolution: {integrity: sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -2423,6 +2462,99 @@ packages:
'@types/cross-spawn@6.0.6': '@types/cross-spawn@6.0.6':
resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==}
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
'@types/d3-axis@3.0.6':
resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
'@types/d3-brush@3.0.6':
resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
'@types/d3-chord@3.0.6':
resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-contour@3.0.6':
resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
'@types/d3-delaunay@6.0.4':
resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
'@types/d3-dispatch@3.0.7':
resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-dsv@3.0.7':
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
'@types/d3-ease@3.0.2':
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
'@types/d3-fetch@3.0.7':
resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
'@types/d3-force@3.0.10':
resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
'@types/d3-format@3.0.4':
resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
'@types/d3-geo@3.1.0':
resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
'@types/d3-hierarchy@3.1.7':
resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
'@types/d3-path@3.1.1':
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
'@types/d3-polygon@3.0.2':
resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
'@types/d3-quadtree@3.0.6':
resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
'@types/d3-random@3.0.3':
resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
'@types/d3-scale-chromatic@3.1.0':
resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
'@types/d3-scale@4.0.9':
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
'@types/d3-selection@3.0.11':
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
'@types/d3-shape@3.1.7':
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
'@types/d3-time-format@4.0.3':
resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
'@types/d3-time@3.0.4':
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/d3-transition@3.0.9':
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
'@types/d3-zoom@3.0.8':
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
'@types/d3@7.4.3':
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@ -2451,6 +2583,9 @@ packages:
'@types/file-saver@2.0.2': '@types/file-saver@2.0.2':
resolution: {integrity: sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==} resolution: {integrity: sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/graceful-fs@4.1.9': '@types/graceful-fs@4.1.9':
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
@ -3601,6 +3736,9 @@ packages:
resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
classcat@5.0.5:
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
classnames@2.5.1: classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
@ -3922,9 +4060,47 @@ packages:
d3-array@1.2.4: d3-array@1.2.4:
resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-polygon@1.0.6: d3-polygon@1.0.6:
resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==} resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
d@1.0.2: d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -7175,6 +7351,12 @@ packages:
peerDependencies: peerDependencies:
react: '*' react: '*'
reactflow@11.11.4:
resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
read-pkg-up@7.0.1: read-pkg-up@7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -8567,6 +8749,21 @@ packages:
zod@3.25.76: zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
snapshots: snapshots:
'@ahooksjs/use-request@2.8.15(react@18.3.1)': '@ahooksjs/use-request@2.8.15(react@18.3.1)':
@ -11033,6 +11230,84 @@ snapshots:
'@react-hook/passive-layout-effect': 1.2.1(react@18.3.1) '@react-hook/passive-layout-effect': 1.2.1(react@18.3.1)
react: 18.3.1 react: 18.3.1
'@reactflow/background@11.3.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.7(@types/react@18.3.26)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/controls@11.2.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.7(@types/react@18.3.26)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/core@11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@types/d3': 7.4.3
'@types/d3-drag': 3.0.7
'@types/d3-selection': 3.0.11
'@types/d3-zoom': 3.0.8
classcat: 5.0.5
d3-drag: 3.0.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.7(@types/react@18.3.26)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/minimap@11.7.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/d3-selection': 3.0.11
'@types/d3-zoom': 3.0.8
classcat: 5.0.5
d3-selection: 3.0.0
d3-zoom: 3.0.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.7(@types/react@18.3.26)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/node-resizer@2.2.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
classcat: 5.0.5
d3-drag: 3.0.0
d3-selection: 3.0.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.7(@types/react@18.3.26)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@reactflow/node-toolbar@1.3.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@reactflow/core': 11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.7(@types/react@18.3.26)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@remix-run/router@1.19.1': {} '@remix-run/router@1.19.1': {}
'@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.19.1)(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(@lezer/common@1.3.0)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.3)': '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.19.1)(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(@lezer/common@1.3.0)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.3)':
@ -11534,6 +11809,123 @@ snapshots:
dependencies: dependencies:
'@types/node': 17.0.45 '@types/node': 17.0.45
'@types/d3-array@3.2.2': {}
'@types/d3-axis@3.0.6':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-brush@3.0.6':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-chord@3.0.6': {}
'@types/d3-color@3.1.3': {}
'@types/d3-contour@3.0.6':
dependencies:
'@types/d3-array': 3.2.2
'@types/geojson': 7946.0.16
'@types/d3-delaunay@6.0.4': {}
'@types/d3-dispatch@3.0.7': {}
'@types/d3-drag@3.0.7':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-dsv@3.0.7': {}
'@types/d3-ease@3.0.2': {}
'@types/d3-fetch@3.0.7':
dependencies:
'@types/d3-dsv': 3.0.7
'@types/d3-force@3.0.10': {}
'@types/d3-format@3.0.4': {}
'@types/d3-geo@3.1.0':
dependencies:
'@types/geojson': 7946.0.16
'@types/d3-hierarchy@3.1.7': {}
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
'@types/d3-path@3.1.1': {}
'@types/d3-polygon@3.0.2': {}
'@types/d3-quadtree@3.0.6': {}
'@types/d3-random@3.0.3': {}
'@types/d3-scale-chromatic@3.1.0': {}
'@types/d3-scale@4.0.9':
dependencies:
'@types/d3-time': 3.0.4
'@types/d3-selection@3.0.11': {}
'@types/d3-shape@3.1.7':
dependencies:
'@types/d3-path': 3.1.1
'@types/d3-time-format@4.0.3': {}
'@types/d3-time@3.0.4': {}
'@types/d3-timer@3.0.2': {}
'@types/d3-transition@3.0.9':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-zoom@3.0.8':
dependencies:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3@7.4.3':
dependencies:
'@types/d3-array': 3.2.2
'@types/d3-axis': 3.0.6
'@types/d3-brush': 3.0.6
'@types/d3-chord': 3.0.6
'@types/d3-color': 3.1.3
'@types/d3-contour': 3.0.6
'@types/d3-delaunay': 6.0.4
'@types/d3-dispatch': 3.0.7
'@types/d3-drag': 3.0.7
'@types/d3-dsv': 3.0.7
'@types/d3-ease': 3.0.2
'@types/d3-fetch': 3.0.7
'@types/d3-force': 3.0.10
'@types/d3-format': 3.0.4
'@types/d3-geo': 3.1.0
'@types/d3-hierarchy': 3.1.7
'@types/d3-interpolate': 3.0.4
'@types/d3-path': 3.1.1
'@types/d3-polygon': 3.0.2
'@types/d3-quadtree': 3.0.6
'@types/d3-random': 3.0.3
'@types/d3-scale': 4.0.9
'@types/d3-scale-chromatic': 3.1.0
'@types/d3-selection': 3.0.11
'@types/d3-shape': 3.1.7
'@types/d3-time': 3.0.4
'@types/d3-time-format': 4.0.3
'@types/d3-timer': 3.0.2
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
@ -11575,6 +11967,8 @@ snapshots:
'@types/file-saver@2.0.2': {} '@types/file-saver@2.0.2': {}
'@types/geojson@7946.0.16': {}
'@types/graceful-fs@4.1.9': '@types/graceful-fs@4.1.9':
dependencies: dependencies:
'@types/node': 17.0.45 '@types/node': 17.0.45
@ -12008,7 +12402,7 @@ snapshots:
postcss-preset-env: 7.5.0(postcss@8.5.6) postcss-preset-env: 7.5.0(postcss@8.5.6)
rollup-plugin-visualizer: 5.9.0(rollup@3.29.5) rollup-plugin-visualizer: 5.9.0(rollup@3.29.5)
systemjs: 6.15.1 systemjs: 6.15.1
vite: 4.5.2(@types/node@17.0.45)(less@4.4.2)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.44.1) vite: 4.5.2(@types/node@17.0.45)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.44.1)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- lightningcss - lightningcss
@ -12523,7 +12917,7 @@ snapshots:
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
react-refresh: 0.14.2 react-refresh: 0.14.2
vite: 4.5.2(@types/node@17.0.45)(less@4.4.2)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.44.1) vite: 4.5.2(@types/node@17.0.45)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.44.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -13332,6 +13726,8 @@ snapshots:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
to-buffer: 1.2.2 to-buffer: 1.2.2
classcat@5.0.5: {}
classnames@2.5.1: {} classnames@2.5.1: {}
clean-css@5.3.3: clean-css@5.3.3:
@ -13681,8 +14077,44 @@ snapshots:
d3-array@1.2.4: {} d3-array@1.2.4: {}
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-polygon@1.0.6: {} d3-polygon@1.0.6: {}
d3-selection@3.0.0: {}
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
d@1.0.2: d@1.0.2:
dependencies: dependencies:
es5-ext: 0.10.64 es5-ext: 0.10.64
@ -17469,6 +17901,20 @@ snapshots:
lodash: 4.17.21 lodash: 4.17.21
react: 18.3.1 react: 18.3.1
reactflow@11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@reactflow/background': 11.3.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@reactflow/controls': 11.2.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@reactflow/core': 11.11.4(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@reactflow/minimap': 11.7.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@reactflow/node-resizer': 2.2.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@reactflow/node-toolbar': 1.3.14(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
read-pkg-up@7.0.1: read-pkg-up@7.0.1:
dependencies: dependencies:
find-up: 4.1.0 find-up: 4.1.0
@ -18760,7 +19206,7 @@ snapshots:
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
vite@4.5.2(@types/node@17.0.45)(less@4.4.2)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.44.1): vite@4.5.2(@types/node@17.0.45)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.44.1):
dependencies: dependencies:
esbuild: 0.18.20 esbuild: 0.18.20
postcss: 8.5.6 postcss: 8.5.6
@ -18768,7 +19214,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 17.0.45 '@types/node': 17.0.45
fsevents: 2.3.3 fsevents: 2.3.3
less: 4.4.2 less: 4.1.3
lightningcss: 1.22.1 lightningcss: 1.22.1
sass: 1.54.0 sass: 1.54.0
terser: 5.44.1 terser: 5.44.1
@ -19003,3 +19449,10 @@ snapshots:
zod: 3.25.76 zod: 3.25.76
zod@3.25.76: {} zod@3.25.76: {}
zustand@4.5.7(@types/react@18.3.26)(react@18.3.1):
dependencies:
use-sync-external-store: 1.6.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.26
react: 18.3.1

View File

@ -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 './flowgramModal'; import ScenarioModal from './visualWorkflowModal';
import ScenarioLogModal from './logModal'; import ScenarioLogModal from './logModal';
import dayjs from 'dayjs'; import dayjs from 'dayjs';

View File

@ -0,0 +1,299 @@
// Custom Node Type Definitions for Flowgram Workflow Editor
import React from 'react';
import { Tag } from 'antd';
export interface NodeData {
label: string;
[key: string]: any;
}
export interface WorkflowNode {
id: string;
type: string;
position: { x: number; y: number };
data: NodeData;
}
export interface WorkflowEdge {
id: string;
source: string;
target: string;
}
export interface WorkflowGraph {
nodes: WorkflowNode[];
edges: WorkflowEdge[];
}
// Trigger Node Types
export const TriggerNodeTypes = {
TIME: 'time',
WEBHOOK: 'webhook',
VARIABLE: 'variable',
TASK_STATUS: 'task_status',
SYSTEM_EVENT: 'system_event',
};
// Condition Node Types
export const ConditionNodeTypes = {
EQUALS: 'equals',
NOT_EQUALS: 'not_equals',
GREATER_THAN: 'greater_than',
LESS_THAN: 'less_than',
CONTAINS: 'contains',
NOT_CONTAINS: 'not_contains',
};
// Action Node Types
export const ActionNodeTypes = {
RUN_TASK: 'run_task',
SET_VARIABLE: 'set_variable',
EXECUTE_COMMAND: 'execute_command',
SEND_NOTIFICATION: 'send_notification',
};
// Control Flow Node Types
export const ControlFlowNodeTypes = {
DELAY: 'delay',
RETRY: 'retry',
CIRCUIT_BREAKER: 'circuit_breaker',
AND_GATE: 'and_gate',
OR_GATE: 'or_gate',
};
// Node Renderer Components
export const NodeRenderers = {
trigger: (node: WorkflowNode) => (
<div
style={{
padding: '12px 16px',
background: '#1890ff',
color: 'white',
borderRadius: 6,
minWidth: 160,
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.3)',
}}
>
<div style={{ fontWeight: 600, marginBottom: 4 }}>{node.data.label}</div>
<Tag
style={{
background: 'rgba(255, 255, 255, 0.2)',
border: 'none',
color: 'white',
}}
>
{node.data.triggerType || 'trigger'}
</Tag>
</div>
),
condition: (node: WorkflowNode) => (
<div
style={{
padding: '12px 16px',
background: '#52c41a',
color: 'white',
borderRadius: 6,
minWidth: 160,
boxShadow: '0 2px 8px rgba(82, 196, 26, 0.3)',
}}
>
<div style={{ fontWeight: 600, marginBottom: 4 }}>{node.data.label}</div>
<div style={{ fontSize: 12, opacity: 0.9 }}>
{node.data.field} {node.data.operator} {node.data.value}
</div>
</div>
),
action: (node: WorkflowNode) => (
<div
style={{
padding: '12px 16px',
background: '#fa8c16',
color: 'white',
borderRadius: 6,
minWidth: 160,
boxShadow: '0 2px 8px rgba(250, 140, 22, 0.3)',
}}
>
<div style={{ fontWeight: 600, marginBottom: 4 }}>{node.data.label}</div>
<Tag
style={{
background: 'rgba(255, 255, 255, 0.2)',
border: 'none',
color: 'white',
}}
>
{node.data.actionType || 'action'}
</Tag>
</div>
),
control: (node: WorkflowNode) => (
<div
style={{
padding: '12px 16px',
background: '#722ed1',
color: 'white',
borderRadius: 6,
minWidth: 160,
boxShadow: '0 2px 8px rgba(114, 46, 209, 0.3)',
}}
>
<div style={{ fontWeight: 600, marginBottom: 4 }}>{node.data.label}</div>
<div style={{ fontSize: 12, opacity: 0.9 }}>
{node.data.controlType || 'control'}
</div>
</div>
),
logic_gate: (node: WorkflowNode) => (
<div
style={{
padding: '12px 16px',
background: '#13c2c2',
color: 'white',
borderRadius: 6,
minWidth: 120,
textAlign: 'center',
boxShadow: '0 2px 8px rgba(19, 194, 194, 0.3)',
}}
>
<div style={{ fontWeight: 600, fontSize: 16 }}>
{node.data.gateType || 'AND'}
</div>
</div>
),
};
// Helper function to create a new node
export const createNode = (
type: string,
position: { x: number; y: number },
data: Partial<NodeData>,
): WorkflowNode => {
return {
id: `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type,
position,
data: {
label: data.label || type,
...data,
},
};
};
// Helper function to create a new edge
export const createEdge = (
source: string,
target: string,
): WorkflowEdge => {
return {
id: `edge-${source}-${target}`,
source,
target,
};
};
// Node templates for quick creation
export const NodeTemplates = {
triggers: {
time: {
label: '时间触发',
triggerType: 'time',
config: { schedule: '0 0 * * *' },
},
webhook: {
label: 'Webhook触发',
triggerType: 'webhook',
config: {},
},
variable: {
label: '变量监听',
triggerType: 'variable',
config: { watchPath: '' },
},
task_status: {
label: '任务状态',
triggerType: 'task_status',
config: { cronId: null },
},
system_event: {
label: '系统事件',
triggerType: 'system_event',
config: { eventType: 'disk_space', threshold: 80 },
},
},
conditions: {
equals: {
label: '等于判断',
operator: 'equals',
field: '',
value: '',
},
greater_than: {
label: '大于判断',
operator: 'greater_than',
field: '',
value: 0,
},
contains: {
label: '包含判断',
operator: 'contains',
field: '',
value: '',
},
},
actions: {
run_task: {
label: '运行任务',
actionType: 'run_task',
cronId: null,
},
set_variable: {
label: '设置变量',
actionType: 'set_variable',
name: '',
value: '',
},
execute_command: {
label: '执行命令',
actionType: 'execute_command',
command: '',
},
send_notification: {
label: '发送通知',
actionType: 'send_notification',
message: '',
},
},
controls: {
delay: {
label: '延迟执行',
controlType: 'delay',
delaySeconds: 60,
},
retry: {
label: '重试策略',
controlType: 'retry',
maxRetries: 3,
retryDelay: 5,
backoffMultiplier: 2,
},
circuit_breaker: {
label: '熔断器',
controlType: 'circuit_breaker',
failureThreshold: 3,
},
},
logic_gates: {
and: {
label: 'AND',
gateType: 'AND',
},
or: {
label: 'OR',
gateType: 'OR',
},
},
};

View File

@ -0,0 +1,471 @@
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">&gt;</Option>
<Option value="less_than">&lt;</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;