重构前端逻辑

This commit is contained in:
dengqichen 2025-11-07 16:02:41 +08:00
parent 28cae45e51
commit 66efeb6058
8 changed files with 305 additions and 79 deletions

View File

@ -70,6 +70,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
"react-lazylog": "^4.5.3",
"react-redux": "^9.0.4", "react-redux": "^9.0.4",
"react-router-dom": "^6.21.0", "react-router-dom": "^6.21.0",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",

View File

@ -185,6 +185,9 @@ importers:
react-infinite-scroll-component: react-infinite-scroll-component:
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0(react@18.3.1) version: 6.1.0(react@18.3.1)
react-lazylog:
specifier: ^4.5.3
version: 4.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-redux: react-redux:
specifier: ^9.0.4 specifier: ^9.0.4
version: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1) version: 9.2.0(@types/react@18.3.18)(react@18.3.1)(redux@5.0.1)
@ -890,6 +893,11 @@ packages:
'@marijn/find-cluster-break@1.0.2': '@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@mattiasbuelens/web-streams-polyfill@0.2.1':
resolution: {integrity: sha512-oKuFCQFa3W7Hj7zKn0+4ypI8JFm4ZKIoncwAC6wd5WwFW2sL7O1hpPoJdSWpynQ4DJ4lQ6MvFoVDmCLilonDFg==}
engines: {node: '>= 8'}
deprecated: moved to web-streams-polyfill@2.0.0
'@monaco-editor/loader@1.4.0': '@monaco-editor/loader@1.4.0':
resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==}
peerDependencies: peerDependencies:
@ -2211,6 +2219,9 @@ packages:
'@types/uuid@10.0.0': '@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
'@types/whatwg-streams@0.0.7':
resolution: {integrity: sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A==}
'@typescript-eslint/eslint-plugin@6.21.0': '@typescript-eslint/eslint-plugin@6.21.0':
resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
@ -2459,6 +2470,10 @@ packages:
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==}
clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2802,6 +2817,9 @@ packages:
fastq@1.17.1: fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fetch-readablestream@0.2.0:
resolution: {integrity: sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw==}
file-entry-cache@6.0.1: file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0} engines: {node: ^10.12.0 || >=12.0.0}
@ -2956,6 +2974,10 @@ packages:
immer@10.1.1: immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
immutable@3.8.2:
resolution: {integrity: sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==}
engines: {node: '>=0.10.0'}
import-fresh@3.3.0: import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3176,6 +3198,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
mitt@1.2.0:
resolution: {integrity: sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==}
mobx-react-lite@4.1.1: mobx-react-lite@4.1.1:
resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==} resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==}
peerDependencies: peerDependencies:
@ -3751,6 +3776,11 @@ packages:
react-is@18.3.1: react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-lazylog@4.5.3:
resolution: {integrity: sha512-lyov32A/4BqihgXgtNXTHCajXSXkYHPlIEmV8RbYjHIMxCFSnmtdg4kDCI3vATz7dURtiFTvrw5yonHnrS+NNg==}
peerDependencies:
react: '>=16.3.0'
react-lifecycles-compat@3.0.4: react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
@ -3832,6 +3862,10 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-string-replace@0.4.4:
resolution: {integrity: sha512-FAMkhxmDpCsGTwTZg7p/2v+/GTmxAp73so3fbSvlAcBBX36ujiGRNEaM/1u+jiYQrArhns+7eE92g2pi5E5FUA==}
engines: {node: '>=0.12.0'}
react-style-singleton@2.2.3: react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3853,6 +3887,12 @@ packages:
peerDependencies: peerDependencies:
react: '>=16.8.0' react: '>=16.8.0'
react-virtualized@9.22.6:
resolution: {integrity: sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==}
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-window@1.8.11: react-window@1.8.11:
resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==}
engines: {node: '>8.0.0'} engines: {node: '>8.0.0'}
@ -4115,6 +4155,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
text-encoding-utf-8@1.0.2:
resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==}
text-table@0.2.0: text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@ -4275,6 +4318,9 @@ packages:
warning@4.0.3: warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
whatwg-fetch@2.0.4:
resolution: {integrity: sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -5125,6 +5171,10 @@ snapshots:
'@marijn/find-cluster-break@1.0.2': {} '@marijn/find-cluster-break@1.0.2': {}
'@mattiasbuelens/web-streams-polyfill@0.2.1':
dependencies:
'@types/whatwg-streams': 0.0.7
'@monaco-editor/loader@1.4.0(monaco-editor@0.52.2)': '@monaco-editor/loader@1.4.0(monaco-editor@0.52.2)':
dependencies: dependencies:
monaco-editor: 0.52.2 monaco-editor: 0.52.2
@ -6564,6 +6614,8 @@ snapshots:
'@types/uuid@10.0.0': {} '@types/uuid@10.0.0': {}
'@types/whatwg-streams@0.0.7': {}
'@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)': '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.7.2))(eslint@8.57.1)(typescript@5.7.2)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.1
@ -6921,6 +6973,8 @@ snapshots:
classnames@2.5.1: {} classnames@2.5.1: {}
clsx@1.2.1: {}
clsx@2.1.1: {} clsx@2.1.1: {}
cmdk@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): cmdk@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@ -7297,6 +7351,8 @@ snapshots:
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
fetch-readablestream@0.2.0: {}
file-entry-cache@6.0.1: file-entry-cache@6.0.1:
dependencies: dependencies:
flat-cache: 3.2.0 flat-cache: 3.2.0
@ -7462,6 +7518,8 @@ snapshots:
immer@10.1.1: {} immer@10.1.1: {}
immutable@3.8.2: {}
import-fresh@3.3.0: import-fresh@3.3.0:
dependencies: dependencies:
parent-module: 1.0.1 parent-module: 1.0.1
@ -7656,6 +7714,8 @@ snapshots:
minipass@7.1.2: {} minipass@7.1.2: {}
mitt@1.2.0: {}
mobx-react-lite@4.1.1(mobx@6.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): mobx-react-lite@4.1.1(mobx@6.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
mobx: 6.15.0 mobx: 6.15.0
@ -8311,6 +8371,21 @@ snapshots:
react-is@18.3.1: {} react-is@18.3.1: {}
react-lazylog@4.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@mattiasbuelens/web-streams-polyfill': 0.2.1
fetch-readablestream: 0.2.0
immutable: 3.8.2
mitt: 1.2.0
prop-types: 15.8.1
react: 18.3.1
react-string-replace: 0.4.4
react-virtualized: 9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
text-encoding-utf-8: 1.0.2
whatwg-fetch: 2.0.4
transitivePeerDependencies:
- react-dom
react-lifecycles-compat@3.0.4: {} react-lifecycles-compat@3.0.4: {}
react-number-format@5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-number-format@5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@ -8387,6 +8462,10 @@ snapshots:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-string-replace@0.4.4:
dependencies:
lodash: 4.17.21
react-style-singleton@2.2.3(@types/react@18.3.18)(react@18.3.1): react-style-singleton@2.2.3(@types/react@18.3.18)(react@18.3.1):
dependencies: dependencies:
get-nonce: 1.0.1 get-nonce: 1.0.1
@ -8408,6 +8487,17 @@ snapshots:
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
react-virtualized@9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.0
clsx: 1.2.1
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-lifecycles-compat: 3.0.4
react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.26.0 '@babel/runtime': 7.26.0
@ -8729,6 +8819,8 @@ snapshots:
source-map-support: 0.5.21 source-map-support: 0.5.21
optional: true optional: true
text-encoding-utf-8@1.0.2: {}
text-table@0.2.0: {} text-table@0.2.0: {}
thenify-all@1.6.0: thenify-all@1.6.0:
@ -8852,6 +8944,8 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
whatwg-fetch@2.0.4: {}
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from 'react'; import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { ReactFlowProvider, ReactFlow, Background, Node, Edge, Handle, Position, BackgroundVariant } from '@xyflow/react'; import { ReactFlowProvider, ReactFlow, Background, Node, Edge, Handle, Position, BackgroundVariant, NodeProps } from '@xyflow/react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -17,7 +17,8 @@ import {
Clock, Clock,
CheckCircle2, CheckCircle2,
XCircle, XCircle,
AlertTriangle AlertTriangle,
FileText
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
@ -32,6 +33,7 @@ import {
formatDuration, formatDuration,
calculateRunningDuration calculateRunningDuration
} from '../utils/dashboardUtils'; } from '../utils/dashboardUtils';
import DeployNodeLogDialog from './DeployNodeLogDialog';
interface DeployFlowGraphModalProps { interface DeployFlowGraphModalProps {
open: boolean; open: boolean;
@ -39,16 +41,34 @@ interface DeployFlowGraphModalProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
interface CustomNodeData {
nodeName: string;
nodeType: string;
nodeId: string;
status: string;
startTime?: string | null;
endTime?: string | null;
duration?: number | null;
errorMessage?: string | null;
isUnreachable?: boolean;
processInstanceId?: string;
onViewLog?: (nodeId: string, nodeName: string) => void;
}
/** /**
* *
*/ */
const CustomFlowNode: React.FC<any> = ({ data }) => { const CustomFlowNode: React.FC<any> = ({ data }) => {
const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage, isUnreachable } = data; const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage, isUnreachable } = data as CustomNodeData;
const statusColor = getNodeStatusColor(status); const statusColor = getNodeStatusColor(status);
const isNotStarted = status === 'NOT_STARTED'; const isNotStarted = status === 'NOT_STARTED';
const isRunning = status === 'RUNNING'; const isRunning = status === 'RUNNING';
const hasFailed = status === 'FAILED'; const hasFailed = status === 'FAILED';
// 判断是否可以查看日志(具有日志输出能力的节点类型)
const loggableNodeTypes = ['JENKINS_BUILD', 'ServiceTask'];
const canViewLog = loggableNodeTypes.includes(nodeType) && status !== 'NOT_STARTED';
// 计算显示的时长 // 计算显示的时长
const displayDuration = useMemo(() => { const displayDuration = useMemo(() => {
if (duration !== null && duration !== undefined) { if (duration !== null && duration !== undefined) {
@ -69,7 +89,8 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
isNotStarted && 'border-2 border-dashed', isNotStarted && 'border-2 border-dashed',
!isNotStarted && 'border-2 border-solid shadow-sm', !isNotStarted && 'border-2 border-solid shadow-sm',
isRunning && 'animate-pulse', isRunning && 'animate-pulse',
isUnreachable && 'opacity-40' // 不可达节点半透明 isUnreachable && 'opacity-40', // 不可达节点半透明
canViewLog && 'cursor-pointer hover:shadow-md hover:scale-[1.02]' // 可查看日志的节点增加交互效果
)} )}
style={{ style={{
borderColor: statusColor, borderColor: statusColor,
@ -77,7 +98,12 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
}} }}
> >
{/* 节点名称 */} {/* 节点名称 */}
<div className="font-medium text-sm mb-1">{nodeName}</div> <div className="flex items-center justify-between gap-2 mb-1">
<div className="font-medium text-sm">{nodeName}</div>
{canViewLog && (
<FileText className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
</div>
{/* 节点状态 */} {/* 节点状态 */}
<div <div
@ -108,6 +134,13 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
<span className="text-xs"></span> <span className="text-xs"></span>
</div> </div>
)} )}
{/* 查看日志提示 */}
{canViewLog && (
<div className="mt-2 text-xs text-blue-600 flex items-center gap-1">
<span></span>
</div>
)}
</div> </div>
); );
@ -371,6 +404,34 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [flowData, setFlowData] = useState<DeployRecordFlowGraph | null>(null); const [flowData, setFlowData] = useState<DeployRecordFlowGraph | null>(null);
// 日志对话框状态
const [logDialogOpen, setLogDialogOpen] = useState(false);
const [selectedNodeId, setSelectedNodeId] = useState<string>('');
const [selectedNodeName, setSelectedNodeName] = useState<string>('');
// 查看日志处理函数
const handleViewLog = (nodeId: string, nodeName: string) => {
setSelectedNodeId(nodeId);
setSelectedNodeName(nodeName);
setLogDialogOpen(true);
};
// ReactFlow 节点点击事件处理
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
const nodeData = node.data as unknown as CustomNodeData;
// 可以查看日志的节点类型
const loggableNodeTypes = ['JENKINS_BUILD', 'ServiceTask'];
const canViewLog = loggableNodeTypes.includes(nodeData.nodeType) && nodeData.status !== 'NOT_STARTED';
if (canViewLog) {
console.log('Node clicked, opening log dialog:', nodeData.nodeId, nodeData.nodeName);
handleViewLog(nodeData.nodeId, nodeData.nodeName);
} else {
console.log('Node clicked but cannot view log:', nodeData.nodeType, nodeData.status);
}
}, []);
// 加载流程图数据 // 加载流程图数据
useEffect(() => { useEffect(() => {
if (open && deployRecordId) { if (open && deployRecordId) {
@ -492,6 +553,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
data: { data: {
nodeName: node.nodeName, nodeName: node.nodeName,
nodeType: node.nodeType, nodeType: node.nodeType,
nodeId: node.id,
status: instance?.status || 'NOT_STARTED', status: instance?.status || 'NOT_STARTED',
startTime: instance?.startTime, startTime: instance?.startTime,
endTime: instance?.endTime, endTime: instance?.endTime,
@ -499,6 +561,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
errorMessage: instance?.errorMessage, errorMessage: instance?.errorMessage,
// 新增:不可达且未执行的节点标记为置灰 // 新增:不可达且未执行的节点标记为置灰
isUnreachable: isRunning && isNotStarted && !isReachable, isUnreachable: isRunning && isNotStarted && !isReachable,
processInstanceId: flowData.processInstanceId,
}, },
}; };
}); });
@ -595,81 +658,98 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
: null; : null;
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <>
<DialogContent className="!max-w-7xl w-[90vw] h-[85vh] flex flex-col p-0 overflow-hidden"> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogHeader className="px-6 pt-6 pb-4 border-b flex-shrink-0"> <DialogContent className="!max-w-7xl w-[90vw] h-[85vh] flex flex-col p-0 overflow-hidden">
<DialogTitle className="flex items-center gap-2"> <DialogHeader className="px-6 pt-6 pb-4 border-b flex-shrink-0">
{flowData && ( <DialogTitle className="flex items-center gap-2">
<span className="text-muted-foreground"> {flowData && (
#{flowData.deployRecordId} <span className="text-muted-foreground">
</span> #{flowData.deployRecordId}
)} </span>
<span></span> )}
{deployStatusInfo && ( <span></span>
<Badge {deployStatusInfo && (
variant="outline" <Badge
className={cn('flex items-center gap-1', deployStatusInfo.color)} variant="outline"
> className={cn('flex items-center gap-1', deployStatusInfo.color)}
<deployStatusInfo.icon >
className={cn( <deployStatusInfo.icon
'h-3 w-3', className={cn(
flowData?.deployStatus === 'RUNNING' && 'animate-spin' 'h-3 w-3',
)} flowData?.deployStatus === 'RUNNING' && 'animate-spin'
/> )}
{deployStatusInfo.text} />
</Badge> {deployStatusInfo.text}
)} </Badge>
</DialogTitle> )}
</DialogHeader> </DialogTitle>
</DialogHeader>
<div className="flex flex-1 overflow-hidden min-h-0"> <div className="flex flex-1 overflow-hidden min-h-0">
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-full w-full"> <div className="flex items-center justify-center h-full w-full">
<div className="text-center space-y-4"> <div className="text-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" /> <Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
<p className="text-sm text-muted-foreground">...</p> <p className="text-sm text-muted-foreground">...</p>
</div>
</div> </div>
</div> ) : flowData ? (
) : flowData ? ( <>
<> {/* 左侧信息面板 */}
{/* 左侧信息面板 */} <DeployInfoPanel flowData={flowData} />
<DeployInfoPanel flowData={flowData} />
{/* 右侧流程图可视化 */} {/* 右侧流程图可视化 */}
<div className="flex-1 relative"> <div className="flex-1 relative">
<ReactFlowProvider> <ReactFlowProvider>
<ReactFlow <ReactFlow
nodes={flowNodes} nodes={flowNodes}
edges={flowEdges} edges={flowEdges}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
fitView onNodeClick={onNodeClick}
className="bg-muted/10" fitView
fitViewOptions={{ padding: 0.2, maxZoom: 1, minZoom: 0.5 }} className="bg-muted/10"
panOnScroll={true} fitViewOptions={{ padding: 0.2, maxZoom: 1, minZoom: 0.5 }}
zoomOnScroll={true} nodesDraggable={false}
zoomOnPinch={true} nodesConnectable={false}
preventScrolling={false} elementsSelectable={true}
> panOnScroll={true}
<Background zoomOnScroll={true}
variant={BackgroundVariant.Dots} zoomOnPinch={true}
gap={16} preventScrolling={false}
size={1} >
className="opacity-30" <Background
/> variant={BackgroundVariant.Dots}
</ReactFlow> gap={16}
</ReactFlowProvider> size={1}
className="opacity-30"
/>
</ReactFlow>
</ReactFlowProvider>
</div>
</>
) : (
<div className="flex items-center justify-center h-full w-full">
<div className="text-center">
<AlertTriangle className="h-12 w-12 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground"></p>
</div>
</div> </div>
</> )}
) : ( </div>
<div className="flex items-center justify-center h-full w-full"> </DialogContent>
<div className="text-center"> </Dialog>
<AlertTriangle className="h-12 w-12 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground"></p> {/* 节点日志对话框 */}
</div> {flowData && (
</div> <DeployNodeLogDialog
)} open={logDialogOpen}
</div> onOpenChange={setLogDialogOpen}
</DialogContent> processInstanceId={flowData.processInstanceId}
</Dialog> nodeId={selectedNodeId}
nodeName={selectedNodeName}
/>
)}
</>
); );
}; };

View File

@ -39,3 +39,13 @@ export const getMyApprovalTasks = (workflowDefinitionKeys?: string[]) => {
*/ */
export const completeApproval = (data: CompleteApprovalRequest) => export const completeApproval = (data: CompleteApprovalRequest) =>
request.post(`${DEPLOY_URL}/complete`, data); request.post(`${DEPLOY_URL}/complete`, data);
/**
*
* @param processInstanceId ID
* @param nodeId ID
*/
export const getDeployNodeLogs = (processInstanceId: string, nodeId: string) =>
request.get<import('./types').DeployNodeLogDTO>(`${DEPLOY_URL}/logs`, {
params: { processInstanceId, nodeId }
});

View File

@ -255,3 +255,26 @@ export interface DeployRecordFlowGraph {
graph: WorkflowDefinitionGraph; graph: WorkflowDefinitionGraph;
nodeInstances: WorkflowNodeInstance[]; nodeInstances: WorkflowNodeInstance[];
} }
/**
*
*/
export type LogLevel = 'INFO' | 'WARN' | 'ERROR';
/**
*
*/
export interface LogEntry {
sequenceId: number;
timestamp: string;
level: LogLevel;
message: string;
}
/**
*
*/
export interface DeployNodeLogDTO {
expired: boolean;
logs: LogEntry[];
}

View File

@ -44,6 +44,12 @@ export const ApprovalNodeDefinition: ConfigurableNodeDefinition = {
title: "审批配置", title: "审批配置",
description: "配置审批人和审批规则", description: "配置审批人和审批规则",
properties: { properties: {
continueOnFailure: {
type: "boolean",
title: "失败后继续",
description: "当节点执行失败时是否继续执行后续节点。true: 节点失败时标记为 FAILURE但流程继续执行后续节点false: 节点失败时抛出 BpmnError终止流程",
default: true
},
approvalMode: { approvalMode: {
type: "string", type: "string",
title: "审批模式", title: "审批模式",

View File

@ -44,6 +44,12 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
title: "输入", title: "输入",
description: "当前节点所需数据配置", description: "当前节点所需数据配置",
properties: { properties: {
continueOnFailure: {
type: "boolean",
title: "失败后继续",
description: "当节点执行失败时是否继续执行后续节点。true: 节点失败时标记为 FAILURE但流程继续执行后续节点false: 节点失败时抛出 BpmnError终止流程",
default: true
},
serverId: { serverId: {
type: "number", type: "number",
title: "Jenkins服务器", title: "Jenkins服务器",
@ -58,7 +64,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
// 注意jobName 暂时使用手动输入,因为需要先选择 serverId 才能级联加载 jobs // 注意jobName 暂时使用手动输入,因为需要先选择 serverId 才能级联加载 jobs
// 未来如果需要级联下拉,需要使用 CascadeDataSourceType.JENKINS_SERVER_VIEWS_JOBS // 未来如果需要级联下拉,需要使用 CascadeDataSourceType.JENKINS_SERVER_VIEWS_JOBS
"x-allow-variable": true "x-allow-variable": true
}, }
}, },
required: ["serverId", "jobName"] required: ["serverId", "jobName"]
}, },

View File

@ -44,6 +44,12 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
title: "输入", title: "输入",
description: "当前节点所需数据配置", description: "当前节点所需数据配置",
properties: { properties: {
continueOnFailure: {
type: "boolean",
title: "失败后继续",
description: "当节点执行失败时是否继续执行后续节点。true: 节点失败时标记为 FAILURE但流程继续执行后续节点false: 节点失败时抛出 BpmnError终止流程",
default: true
},
// notificationType: { // notificationType: {
// type: "string", // type: "string",
// title: "通知类型", // title: "通知类型",