From e8c8c0d2b35880cb271599c10b3f69a5f6e2c3bb Mon Sep 17 00:00:00 2001 From: Dawid Rusnak Date: Wed, 19 Jul 2023 13:30:58 +0200 Subject: [PATCH 1/2] fix: always use / on end of (#792) Relates to: kubeshop/testkube#2952 --- scripts/inject-base-href.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/inject-base-href.sh b/scripts/inject-base-href.sh index 7a45ba62e..f71310d14 100755 --- a/scripts/inject-base-href.sh +++ b/scripts/inject-base-href.sh @@ -4,5 +4,6 @@ set -e # Include the proper based on the root route -RENDERED="$(sed -e "s|]*>||" index.html)" +BASE_URL="$(echo "${REACT_APP_ROOT_ROUTE:-""}" | sed 's|\/*$||')" +RENDERED="$(sed -e "s|]*>||" index.html)" echo "$RENDERED" > index.html From 02b7e5040f5771410ce272169d795181978b2635 Mon Sep 17 00:00:00 2001 From: Pavlo Burchak Date: Fri, 21 Jul 2023 13:44:56 +0300 Subject: [PATCH 2/2] feat: Parallel tests (#790) * parallel tests * lockfile * remove array test * remove test suite step card * fix stylelint * remove nodes dragging * fixes after review * more fixed after review * added v2 compatibility * improve edges readability * intersection node improvements * props renaming * test modal improvement * some more fixes * fix: handle case when there are no steps to execute * fix: avoid handle with no edges * fix: block broken behavior of creating edges by user * fix: avoid warning for findDOMNode in Test Suites tooltip * fix: delete broken edges without Handles from AddNode * chore: clean up code a little * fix: add intersection on the beginning * chore: semantics - node is hard deleted, not soft deleted * chore: simplify building position of flow chart items * fix: center test suite steps flow chart * fix: correctly disable focusing edges * fix: react-flow performance issues * fix: Test Suites V2 support * fix: make the whole menu button clickable --------- Co-authored-by: Dawid Rusnak --- package-lock.json | 528 +++++++++++++++--- package.json | 2 +- .../molecules/DragNDropList/DragNDropList.tsx | 86 --- .../DragNDropList/StrictModeDroppable.tsx | 23 - .../molecules/DragNDropList/index.ts | 1 - .../ExecutionStepsList/ExecutionStepsList.tsx | 96 ++-- .../TestSuiteStepCard.styled.tsx | 53 -- .../TestSuiteStepCard/TestSuiteStepCard.tsx | 56 -- .../molecules/TestSuiteStepCard/index.ts | 1 - src/components/molecules/index.ts | 2 - .../Settings/Settings.tsx | 8 +- .../Settings/SettingsTests/DelayModal.tsx | 81 --- .../SettingsTests/Modals/DelayModal.tsx | 88 +++ .../SettingsTests/Modals/TestModal.tsx | 101 ++++ .../Settings/SettingsTests/Nodes/AddNode.tsx | 25 + .../SettingsTests/Nodes/AddStepDropdown.tsx | 34 ++ .../SettingsTests/Nodes/IntersectionNode.tsx | 29 + .../Settings/SettingsTests/Nodes/StepNode.tsx | 49 ++ .../SettingsTests/SettingsTests.styled.tsx | 51 ++ .../Settings/SettingsTests/SettingsTests.tsx | 342 +++++------- .../SettingsTests/TestSuiteStepsFlow.tsx | 147 +++++ src/index.tsx | 1 + src/models/testSuite.ts | 8 +- src/styles/globalStyles.ts | 41 ++ src/utils/array.spec.ts | 12 - src/utils/array.ts | 7 - src/utils/formatMilliseconds.ts | 16 + src/utils/testSuites.ts | 8 +- 28 files changed, 1213 insertions(+), 683 deletions(-) delete mode 100644 src/components/molecules/DragNDropList/DragNDropList.tsx delete mode 100644 src/components/molecules/DragNDropList/StrictModeDroppable.tsx delete mode 100644 src/components/molecules/DragNDropList/index.ts delete mode 100644 src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.styled.tsx delete mode 100644 src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.tsx delete mode 100644 src/components/molecules/TestSuiteStepCard/index.ts delete mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/DelayModal.tsx create mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/DelayModal.tsx create mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/TestModal.tsx create mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddNode.tsx create mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddStepDropdown.tsx create mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/IntersectionNode.tsx create mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/StepNode.tsx create mode 100644 src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/TestSuiteStepsFlow.tsx delete mode 100644 src/utils/array.spec.ts delete mode 100644 src/utils/array.ts create mode 100644 src/utils/formatMilliseconds.ts diff --git a/package-lock.json b/package-lock.json index d698c208f..ab41d666b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "node-fetch": "^2.6.12", "query-string": "7.1.1", "react": "18.1.0", - "react-beautiful-dnd": "^13.1.0", "react-dom": "18.1.0", "react-gtm-module": "^2.0.11", "react-helmet": "^6.1.0", @@ -40,6 +39,7 @@ "react-transition-group": "4.4.2", "react-use": "17.4.0", "react-use-websocket": "^4.2.0", + "reactflow": "^11.7.4", "semver": "^7.5.4", "strip-ansi": "^7.0.1", "styled-components": "5.3.5", @@ -4101,6 +4101,102 @@ "version": "2.3.2", "license": "MIT" }, + "node_modules/@reactflow/background": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.2.4.tgz", + "integrity": "sha512-SYQbCRCU0GuxT/40Tm7ZK+l5wByGnNJSLtZhbL9C/Hl7JhsJXV3UGXr0vrlhVZUBEtkWA7XhZM/5S9XEA5XSFA==", + "dependencies": { + "@reactflow/core": "11.7.4", + "classcat": "^5.0.3", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.1.15.tgz", + "integrity": "sha512-//33XfBYu8vQ6brfmlZwKrDoh+8hh93xO2d88XiqfIbrPEEb32SYjsb9mS9VuHKNlSIW+eB27fBA1Gt00mEj5w==", + "dependencies": { + "@reactflow/core": "11.7.4", + "classcat": "^5.0.3", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.7.4.tgz", + "integrity": "sha512-nt0T8ERp8TE7YCDQViaoEY9lb0StDPrWHVx3zBjhStFYET3wc88t8QRasZdf99xRTmyNtI3U3M40M5EBLNUpMw==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.5.4", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.5.4.tgz", + "integrity": "sha512-1tDBj2zX2gxu2oHU6qvH5RGNrOWRfRxu8369KhDotuuBN5yJrGXJzWIKikwhzjsNsQJYOB+B0cS44yWAfwSwzw==", + "dependencies": { + "@reactflow/core": "11.7.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.1.1.tgz", + "integrity": "sha512-5Q+IBmZfpp/bYsw3+KRVJB1nUbj6W3XAp5ycx4uNWH+K98vbssymyQsW0vvKkIhxEPg6tkiMzO4UWRWvwBwt1g==", + "dependencies": { + "@reactflow/core": "^11.6.0", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.2.3.tgz", + "integrity": "sha512-uFQy9xpog92s0G1wsPLniwV9nyH4i/MmL7QoMsWdnKaOi7XMhd8SJcCzUdHC3imR21HltsuQITff/XQ51ApMbg==", + "dependencies": { + "@reactflow/core": "11.7.4", + "classcat": "^5.0.3", + "zustand": "^4.3.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@reduxjs/toolkit": { "version": "1.9.1", "license": "MIT", @@ -4757,11 +4853,227 @@ "@types/node": "*" } }, - "node_modules/@types/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", - "dev": true + "node_modules/@types/d3": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz", + "integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.5.tgz", + "integrity": "sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.2.tgz", + "integrity": "sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.2.tgz", + "integrity": "sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.2.tgz", + "integrity": "sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.2.tgz", + "integrity": "sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz", + "integrity": "sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.2.tgz", + "integrity": "sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz", + "integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.2.tgz", + "integrity": "sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.4.tgz", + "integrity": "sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" + }, + "node_modules/@types/d3-geo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.3.tgz", + "integrity": "sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", + "integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz", + "integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz", + "integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz", + "integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.5.tgz", + "integrity": "sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.1.tgz", + "integrity": "sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz", + "integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz", + "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.3.tgz", + "integrity": "sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.3.tgz", + "integrity": "sha512-OWk1yYIIWcZ07+igN6BeoG6rqhnJ/pYe+R1qWFM2DtW49zsoSjgb9G5xB0ZXA8hh2jAzey1XuRmMSoXdKw8MDA==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } }, "node_modules/@types/eslint": { "version": "8.44.0", @@ -4825,6 +5137,11 @@ "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", "dev": true }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -5037,17 +5354,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-redux": { - "version": "7.1.25", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", - "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/react-router": { "version": "5.1.20", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", @@ -7059,6 +7365,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/classcat": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz", + "integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==" + }, "node_modules/classnames": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", @@ -7661,13 +7972,6 @@ "postcss": "^8.4" } }, - "node_modules/css-box-model": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -8069,6 +8373,102 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "dev": true, @@ -13487,10 +13887,6 @@ "node": ">= 4.0.0" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "license": "MIT" - }, "node_modules/meow": { "version": "8.1.2", "dev": true, @@ -16039,10 +16435,6 @@ "performance-now": "^2.1.0" } }, - "node_modules/raf-schd": { - "version": "4.0.3", - "license": "MIT" - }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -16668,50 +17060,6 @@ "node": ">=14" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-beautiful-dnd/node_modules/react-is": { - "version": "17.0.2", - "license": "MIT" - }, - "node_modules/react-beautiful-dnd/node_modules/react-redux": { - "version": "7.2.9", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/react-dev-utils": { "version": "12.0.1", "dev": true, @@ -17137,6 +17485,23 @@ "version": "2.2.1", "license": "MIT" }, + "node_modules/reactflow": { + "version": "11.7.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.7.4.tgz", + "integrity": "sha512-QI6+oc1Ft6oFeLSdHlp+SmgymbI5Tm49wj5JyE84O4A54yN/ImfYaBhLit9Cmfzxn9Tz6tDqmGMGbk4bdtB8/w==", + "dependencies": { + "@reactflow/background": "11.2.4", + "@reactflow/controls": "11.1.15", + "@reactflow/core": "11.7.4", + "@reactflow/minimap": "11.5.4", + "@reactflow/node-resizer": "2.1.1", + "@reactflow/node-toolbar": "1.2.3" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/read-cache": { "version": "1.0.0", "dev": true, @@ -19722,10 +20087,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tiny-invariant": { - "version": "1.3.1", - "license": "MIT" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -20261,13 +20622,6 @@ } } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index aebed9da3..23999a89b 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "node-fetch": "^2.6.12", "query-string": "7.1.1", "react": "18.1.0", - "react-beautiful-dnd": "^13.1.0", "react-dom": "18.1.0", "react-gtm-module": "^2.0.11", "react-helmet": "^6.1.0", @@ -102,6 +101,7 @@ "react-use": "17.4.0", "react-use-websocket": "^4.2.0", "semver": "^7.5.4", + "reactflow": "^11.7.4", "strip-ansi": "^7.0.1", "styled-components": "5.3.5", "zustand": "^4.3.7" diff --git a/src/components/molecules/DragNDropList/DragNDropList.tsx b/src/components/molecules/DragNDropList/DragNDropList.tsx deleted file mode 100644 index 3a4c2c78b..000000000 --- a/src/components/molecules/DragNDropList/DragNDropList.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, {PropsWithChildren, memo} from 'react'; -import {DragDropContext, Draggable, DropResult, DroppableProvided, DroppableStateSnapshot} from 'react-beautiful-dnd'; - -import {reorder} from '@utils/array'; - -import StrictModeDroppable from './StrictModeDroppable'; - -interface ItemComponentProps { - index: number; - isDragging: boolean; - onDelete: (index: number) => void; - disabled: boolean; -} - -type DragNDropListProps = { - value: any[]; - onChange: (steps: any[]) => void; - ContainerComponent: React.FC< - PropsWithChildren<{ - isDragging: DroppableStateSnapshot['isDraggingOver']; - ref: DroppableProvided['innerRef']; - }> - >; - ItemComponent: React.FC; - onDelete: (index: number) => void; - scrollRef: React.RefObject | undefined; - disabled?: boolean; -}; - -const DragNDropList: React.FC = props => { - const {value = [], onChange, onDelete, scrollRef, ItemComponent, ContainerComponent, disabled = false} = props; - - const onDragEnd = (result: DropResult) => { - if (!result.destination || result.source.index === result.destination.index) { - return; - } - - const reorderedItems = reorder(value, result.source.index, result.destination.index); - - onChange(reorderedItems); - }; - - const onDragStart = () => { - // Add a little vibration if the browser supports it. - // Add's a nice little physical feedback - if (window.navigator.vibrate) { - window.navigator.vibrate(100); - } - }; - - return ( - - - {(provided, snapshot) => ( - - {value.map((item, index) => ( - - {(providedDraggable, snapshotDraggable) => ( -
- -
- )} -
- ))} - <> -
- {provided.placeholder} - - - )} - - - ); -}; - -export default memo(DragNDropList); diff --git a/src/components/molecules/DragNDropList/StrictModeDroppable.tsx b/src/components/molecules/DragNDropList/StrictModeDroppable.tsx deleted file mode 100644 index 5be94013d..000000000 --- a/src/components/molecules/DragNDropList/StrictModeDroppable.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {useEffect, useState} from 'react'; -import {Droppable, DroppableProps} from 'react-beautiful-dnd'; - -const StrictModeDroppable = ({children, ...props}: DroppableProps) => { - const [enabled, setEnabled] = useState(false); - - useEffect(() => { - const animation = requestAnimationFrame(() => setEnabled(true)); - - return () => { - cancelAnimationFrame(animation); - setEnabled(false); - }; - }, []); - - if (!enabled) { - return null; - } - - return {children}; -}; - -export default StrictModeDroppable; diff --git a/src/components/molecules/DragNDropList/index.ts b/src/components/molecules/DragNDropList/index.ts deleted file mode 100644 index 091dd654e..000000000 --- a/src/components/molecules/DragNDropList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default} from './DragNDropList'; diff --git a/src/components/molecules/ExecutionStepsList/ExecutionStepsList.tsx b/src/components/molecules/ExecutionStepsList/ExecutionStepsList.tsx index cf1156911..e6cf04c56 100644 --- a/src/components/molecules/ExecutionStepsList/ExecutionStepsList.tsx +++ b/src/components/molecules/ExecutionStepsList/ExecutionStepsList.tsx @@ -36,56 +36,58 @@ const ExecutionStepsList: FC = props => { const executors = useAppSelector(selectExecutors); const elements = useMemo(() => { - return executionSteps?.map(step => { - const groupKey = step.execute.map(item => item.execution?.id || Math.random()).join('-'); - - const items = step.execute - .map(({execution}, index) => ({execution, result: step.step.execute[index]})) - .filter(({result}) => result.delay || result.test) - .map(({execution, result}) => { - const status = execution.executionResult?.status; - if (result.delay) { + return executionSteps + ?.filter(step => step.execute?.length) + .map(step => { + const groupKey = step.execute.map(item => item.execution?.id || Math.random()).join('-'); + + const items = step.execute + .map(({execution}, index) => ({execution, result: step.step.execute[index]})) + .filter(({result}) => result.delay || result.test) + .map(({execution, result}) => { + const status = execution.executionResult?.status; + if (result.delay) { + return { + status, + icon: , + name: `Delay - ${result.delay}`, + url: null, + }; + } return { status, - icon: , - name: `Delay - ${result.delay}`, - url: null, + icon: , + name: result.test!, + url: + status !== 'queued' && status !== 'aborted' + ? execution?.id + ? `/tests/executions/${result.test}/execution/${execution.id}` + : `/tests/executions/${result.test}` + : null, }; - } - return { - status, - icon: , - name: result.test!, - url: - status !== 'queued' && status !== 'aborted' - ? execution?.id - ? `/tests/executions/${result.test}/execution/${execution.id}` - : `/tests/executions/${result.test}` - : null, - }; - }) - .map(({status, icon, name, url}, index) => ( - navigate(url) : undefined} - > - - {status ? : null} - {icon} - - {url ? :
} - - - )); - - return ( -
  • - {items} -
  • - ); - }); + }) + .map(({status, icon, name, url}, index) => ( + navigate(url) : undefined} + > + + {status ? : null} + {icon} + + {url ? :
    } + + + )); + + return ( +
  • + {items} +
  • + ); + }); }, [executionSteps]); return {elements}; diff --git a/src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.styled.tsx b/src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.styled.tsx deleted file mode 100644 index dc277bc5b..000000000 --- a/src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.styled.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import styled from 'styled-components'; - -import Colors from '@styles/Colors'; - -export const StyledContainer = styled.div<{$isDragging?: boolean; $disabled: boolean}>` - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; - - padding: 20px; - margin-bottom: 12px; - - background: transparent; - border: 1px solid ${Colors.slate800}; - border-radius: 2px; - - transition: 0.3s all; - - ${props => - props.$disabled - ? '' - : ` - &:hover { - background: ${Colors.slate900}; - border: 1px solid ${Colors.indigo400}; - }`} - - ${props => - props.$isDragging - ? ` - background: ${Colors.slate900}; - border: 1px solid ${Colors.indigo400};` - : ''} - - svg { - width: 24px; - height: 24px; - } -`; - -export const StyledNameContainer = styled.div` - flex: 1; -`; - -export const StyledIconsWrapper = styled.div` - &:hover { - svg { - transition: 0.3s all; - color: ${Colors.indigo400}; - } - } -`; diff --git a/src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.tsx b/src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.tsx deleted file mode 100644 index a9bf1138f..000000000 --- a/src/components/molecules/TestSuiteStepCard/TestSuiteStepCard.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; - -import {ClockCircleOutlined, DeleteOutlined} from '@ant-design/icons'; - -import {ExecutorIcon} from '@atoms'; - -import {Text} from '@custom-antd'; - -import '@models/testSuite'; - -import Colors from '@styles/Colors'; - -import {StyledContainer, StyledIconsWrapper, StyledNameContainer} from './TestSuiteStepCard.styled'; - -type TestSuiteStepCardProps = { - test?: string; - type?: string; - delay?: string; - onDelete: (index: number) => void; - index: number; - isDragging?: boolean; - disabled?: boolean; -}; - -const TestSuiteStepCard: React.FC = props => { - const {test, type, delay, onDelete, index, isDragging, disabled = false} = props; - - let renderName; - let renderType; - - if (delay) { - renderName = typeof delay === 'string' ? delay : `${delay}ms`; - renderType = 'delay'; - } else if (test) { - renderName = test; - renderType = type; - } - - return ( - - {renderType === 'delay' ? : } - - - {renderName} - - - {disabled ? null : ( - - onDelete(index)} /> - - )} - - ); -}; - -export default React.memo(TestSuiteStepCard); diff --git a/src/components/molecules/TestSuiteStepCard/index.ts b/src/components/molecules/TestSuiteStepCard/index.ts deleted file mode 100644 index e3426a448..000000000 --- a/src/components/molecules/TestSuiteStepCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default} from './TestSuiteStepCard'; diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index be7488877..0b823d882 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -10,9 +10,7 @@ export {notificationCall, NotificationContent} from './Notification'; export {default as EndpointModal} from './EndpointModal'; export {default as ConfigurationCard} from './ConfigurationCard'; export {TestExecutionDetailsTabs, TestSuiteExecutionDetailsTabs} from './ExecutionDetails'; -export {default as DragNDropList} from './DragNDropList'; export {default as ArtifactsList} from './ArtifactsList'; -export {default as TestSuiteStepCard} from './TestSuiteStepCard'; export {default as HelpCard} from './HelpCard'; export {default as EmptyListContent} from './EmptyListContent'; export {default as MetricsBarChart} from './MetricsBarChart'; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/Settings.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/Settings.tsx index 7e7c03263..93e010b9d 100644 --- a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/Settings.tsx +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/Settings.tsx @@ -1,4 +1,4 @@ -import React, {FC, ReactElement, useCallback, useContext} from 'react'; +import React, {ReactElement, useContext} from 'react'; import {EntityDetailsContext} from '@contexts'; @@ -27,15 +27,11 @@ const testSettings = ( /> ); -const WrappedSettingsTests: FC<{setId(id: string): void}> = ({setId}) => ( - setId('definition'), [])} /> -); - const testSuiteSettings = ( }, - {id: 'tests', label: 'Tests', children: WrappedSettingsTests}, + {id: 'tests', label: 'Tests', children: }, {id: 'variables', label: 'Variables & Secrets', children: }, {id: 'scheduling', label: 'Scheduling', children: }, {id: 'definition', label: 'Definition', children: }, diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/DelayModal.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/DelayModal.tsx deleted file mode 100644 index 05f817fe3..000000000 --- a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/DelayModal.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, {useEffect, useMemo, useRef, useState} from 'react'; - -import {InputNumber} from 'antd'; - -import {Button, Modal, Text} from '@custom-antd'; - -import usePressEnter from '@hooks/usePressEnter'; - -import {StyledDelayModalContent} from './SettingsTests.styled'; - -type DelayModalProps = { - isDelayModalVisible: boolean; - setIsDelayModalVisible: (flag: boolean) => void; - addDelay: (delay: number) => void; -}; - -const DelayModal: React.FC = props => { - const {isDelayModalVisible, setIsDelayModalVisible, addDelay} = props; - - const [delayValue, setDelayValue] = useState(null); - - const isDelayInteger = useMemo(() => Number.isInteger(delayValue), [delayValue]); - - const delayInputRef = useRef(null); - - const onEvent = usePressEnter(); - - const onConfirm = () => { - if (isDelayInteger) { - addDelay(Number(delayValue)); - } - - setDelayValue(null); - }; - - useEffect(() => { - setTimeout(() => { - if (isDelayModalVisible && delayInputRef.current) { - // @ts-ignore - delayInputRef.current!.focus(); - } - }, 100); - }, [isDelayModalVisible]); - - return ( - - - - - } - content={ - { - onEvent(event, onConfirm); - }} - > - Delay in ms - setDelayValue(value)} - /> - - } - /> - ); -}; - -export default DelayModal; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/DelayModal.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/DelayModal.tsx new file mode 100644 index 000000000..5e5b5ee4e --- /dev/null +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/DelayModal.tsx @@ -0,0 +1,88 @@ +import React, {useEffect, useRef, useState} from 'react'; + +import {Input} from 'antd'; + +import {Button, Modal} from '@custom-antd'; + +import usePressEnter from '@hooks/usePressEnter'; + +import {notificationCall} from '@src/components/molecules'; + +import {StyledDelayModalContent} from '../SettingsTests.styled'; + +type DelayModalProps = { + visible: boolean; + onClose: () => void; + onSubmit: (delay: string) => void; +}; + +const DelayModal: React.FC = props => { + const {visible, onClose, onSubmit} = props; + + const [delayValue, setDelayValue] = useState(''); + + const delayInputRef = useRef(null); + + const onEvent = usePressEnter(); + + const onConfirm = () => { + if (/^([0-9]|([1-9][0-9]+))(ms|s|m|h)?$/.test(delayValue)) { + onSubmit(delayValue); + + setDelayValue(''); + onClose(); + } else { + notificationCall('failed', 'Invalid delay format'); + } + }; + + useEffect(() => { + setTimeout(() => { + if (visible && delayInputRef.current) { + // @ts-ignore + delayInputRef.current!.focus(); + } + }, 100); + }, [visible]); + + return ( + + + + + } + content={ + { + onEvent(event, onConfirm); + }} + > + setDelayValue(e.target.value)} + /> + + } + /> + ); +}; + +export default DelayModal; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/TestModal.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/TestModal.tsx new file mode 100644 index 000000000..d8259b19e --- /dev/null +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Modals/TestModal.tsx @@ -0,0 +1,101 @@ +import React, {useMemo, useState} from 'react'; + +import {Select} from 'antd'; + +import {ExecutorIcon} from '@atoms'; + +import {Button, Modal, Text} from '@custom-antd'; + +import usePressEnter from '@hooks/usePressEnter'; + +import {TestSuiteStepTest} from '@models/test'; + +import {useAppSelector} from '@redux/hooks'; +import {selectExecutors} from '@redux/reducers/executorsSlice'; +import {getTestExecutorIcon} from '@redux/utils/executorIcon'; + +import {useGetAllTestsQuery} from '@services/tests'; + +import {StyledDelayModalContent, StyledOptionWrapper} from '../SettingsTests.styled'; + +type TestModalProps = { + visible: boolean; + onClose: () => void; + onSubmit: (test: {test: string; type: string}) => void; +}; +const {Option} = Select; + +const TestModal: React.FC = props => { + const {visible, onClose, onSubmit} = props; + + const {data: allTestsList} = useGetAllTestsQuery(); + + const executors = useAppSelector(selectExecutors); + + const [testValue, setTestValue] = useState(null); + + const onEvent = usePressEnter(); + + const onConfirm = () => { + const testData = allTestsData.find(x => x.name === testValue!); + if (testData) { + onSubmit({test: testData.name, type: testData.type!}); + } + + setTestValue(null); + onClose(); + }; + + const allTestsData: TestSuiteStepTest[] = useMemo(() => { + return (allTestsList || []).map(item => ({ + name: item.test.name, + namespace: item.test.namespace, + type: getTestExecutorIcon(executors, item.test.type), + })); + }, [allTestsList]); + + return ( + + + + + } + content={ + { + onEvent(event, onConfirm); + }} + > + + + } + /> + ); +}; + +export default TestModal; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddNode.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddNode.tsx new file mode 100644 index 000000000..c5fea5a36 --- /dev/null +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddNode.tsx @@ -0,0 +1,25 @@ +import {IntersectionContainer} from '../SettingsTests.styled'; + +import AddStepDropdown from './AddStepDropdown'; + +type AddNodeProps = { + data: { + showTestModal: (group: number) => void; + showDelayModal: (group: number) => void; + group: number; + }; +}; + +const AddNode: React.FC = props => { + const {data} = props; + + return ( + <> + + + + + + ); +}; + +export default AddNode; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddStepDropdown.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddStepDropdown.tsx new file mode 100644 index 000000000..69f4edb39 --- /dev/null +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/AddStepDropdown.tsx @@ -0,0 +1,34 @@ +import {ReactNode} from 'react'; + +import {Dropdown} from 'antd'; + +type AddStepDropdownProps = { + children: ReactNode; + before?: boolean; + data: { + showTestModal: (group: number) => void; + showDelayModal: (group: number) => void; + group: number; + }; +}; + +const AddStepDropdown: React.FC = props => { + const {data, before = false, children} = props; + const group = data.group + (before ? -0.5 : 0); + + return ( + data.showTestModal(group)}, + {key: 2, label: 'Add a delay', onClick: () => data.showDelayModal(group)}, + ], + }} + > + {children} + + ); +}; +export default AddStepDropdown; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/IntersectionNode.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/IntersectionNode.tsx new file mode 100644 index 000000000..7d7b8fe38 --- /dev/null +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/IntersectionNode.tsx @@ -0,0 +1,29 @@ +import {Handle, Position} from 'reactflow'; + +import {IntersectionContainer} from '../SettingsTests.styled'; + +import AddStepDropdown from './AddStepDropdown'; + +type IntersectionNodeProps = { + data: { + showTestModal: (group: number) => void; + showDelayModal: (group: number) => void; + group: number; + last: boolean; + }; +}; +const IntersectionNode: React.FC = props => { + const {data} = props; + + return ( + <> + {data.group === 0 ? null : } + + + + + {data.last ? null : } + + ); +}; + +export default IntersectionNode; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/StepNode.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/StepNode.tsx new file mode 100644 index 000000000..1abc05578 --- /dev/null +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/Nodes/StepNode.tsx @@ -0,0 +1,49 @@ +import {Handle, Position} from 'reactflow'; + +import {ClockCircleOutlined} from '@ant-design/icons'; +import {Tooltip} from 'antd'; + +import {ExecutorIcon} from '@atoms'; + +import {Text} from '@custom-antd'; + +import {LocalStep} from '@models/testSuite'; + +import {DotsDropdown} from '@molecules'; + +import {TestNodeContainer, TestNodeNameContainer} from '../SettingsTests.styled'; + +type StepNodeProps = { + data: { + item: LocalStep; + group: number; + deleteNode: (id: string, group: number) => void; + }; + id: string; +}; + +const StepNode: React.FC = props => { + const {data, id} = props; + const {type, test, delay} = data.item; + + const renderText = test ?? (/^[0-9]+$/.test(`${delay}`) ? `${delay}ms` : delay); + return ( + <> + + + {delay ? : } + + + + {renderText} + + + + data.deleteNode(id, data.group)}>Delete}]} /> + + + + ); +}; + +export default StepNode; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.styled.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.styled.tsx index b98ee3e80..ce5c1f9e8 100644 --- a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.styled.tsx +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.styled.tsx @@ -2,6 +2,13 @@ import {ReactNode} from 'react'; import styled from 'styled-components'; +import Colors from '@styles/Colors'; + +export const addWidth = 32; +export const addHeight = 46; +export const itemWidth = 300; +export const itemHeight = 68; + export const EmptyTestsContainer = styled.div` display: flex; flex-direction: column; @@ -16,6 +23,8 @@ export const StyledOptionWrapper = styled.span` align-items: center; gap: 12px; + padding-top: 7px; + svg { width: 24px; height: 24px; @@ -41,3 +50,45 @@ export const StyledStepsList = styled.div<{ display: flex; flex-direction: column; `; + +export const TestNodeContainer = styled.div` + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + gap: 12px; + + width: ${itemWidth}px; + height: ${itemHeight}px; + padding: 20px; + + border-radius: 4px; + + background-color: ${Colors.slate800}; +`; + +export const TestNodeNameContainer = styled.div` + overflow: hidden; +`; + +export const IntersectionContainer = styled.div` + border-radius: 10px; + padding: 12px; + width: ${addWidth}px; + height: ${addHeight}px; + + background-color: ${Colors.slate800}; + + cursor: pointer; + + &:hover { + background-color: ${Colors.slate700}; + } + + transition: 0.3s all; +`; + +export const ReactFlowContainer = styled.div` + width: 100%; + height: 400px; +`; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.tsx index a1ae8a2f1..3bd33e604 100644 --- a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.tsx +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/SettingsTests.tsx @@ -1,22 +1,23 @@ -import {memo, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {memo, useContext, useEffect, useMemo, useState} from 'react'; -import {ClockCircleOutlined, WarningOutlined} from '@ant-design/icons'; -import {Button, Form, Select} from 'antd'; +import {Form} from 'antd'; import {nanoid} from '@reduxjs/toolkit'; -import {ExecutorIcon, ExternalLink} from '@atoms'; +import pick from 'lodash/pick'; + +import {ExternalLink} from '@atoms'; import {EntityDetailsContext, MainContext} from '@contexts'; -import {Text, Title} from '@custom-antd'; +import {Button, FullWidthSpace, Title} from '@custom-antd'; import useClusterVersionMatch from '@hooks/useClusterVersionMatch'; import {TestSuiteStepTest} from '@models/test'; -import {TestSuite, TestSuiteStep} from '@models/testSuite'; +import {LocalStep, TestSuite} from '@models/testSuite'; -import {ConfigurationCard, DragNDropList, TestSuiteStepCard, notificationCall} from '@molecules'; +import {ConfigurationCard, InlineNotification, notificationCall} from '@molecules'; import {Permissions, usePermission} from '@permissions/base'; @@ -25,24 +26,18 @@ import {selectExecutors} from '@redux/reducers/executorsSlice'; import {getTestExecutorIcon} from '@redux/utils/executorIcon'; import {useGetTestsListForTestSuiteQuery, useUpdateTestSuiteMutation} from '@services/testSuites'; -import {useGetAllTestsQuery} from '@services/tests'; import {externalLinks} from '@utils/externalLinks'; +import {formatMilliseconds} from '@utils/formatMilliseconds'; import {displayDefaultNotificationFlow} from '@utils/notification'; import {convertTestSuiteV2ToV3, isTestSuiteV2} from '@utils/testSuites'; -import DelayModal from './DelayModal'; -import {EmptyTestsContainer, StyledOptionWrapper, StyledStepsList} from './SettingsTests.styled'; - -const {Option} = Select; - -interface LocalStep extends TestSuiteStep { - type?: string; - stopTestOnFailure?: boolean; - id?: string; -} +import DelayModal from './Modals/DelayModal'; +import TestModal from './Modals/TestModal'; +import {EmptyTestsContainer} from './SettingsTests.styled'; +import TestSuiteStepsFlow from './TestSuiteStepsFlow'; -const SettingsTests: React.FC<{openDefinition(): void}> = ({openDefinition}) => { +const SettingsTests = () => { const {isClusterAvailable} = useContext(MainContext); const {entityDetails: rawEntityDetails} = useContext(EntityDetailsContext) as {entityDetails: TestSuite}; @@ -52,18 +47,13 @@ const SettingsTests: React.FC<{openDefinition(): void}> = ({openDefinition}) => [rawEntityDetails] ); - const mayEdit = usePermission(Permissions.editEntity); - - const [isDelayModalVisible, setIsDelayModalVisible] = useState(false); - - const scrollRef = useRef(null); - const executors = useAppSelector(selectExecutors); + const mayEdit = usePermission(Permissions.editEntity); + const {data: testsList} = useGetTestsListForTestSuiteQuery(entityDetails.name, { skip: !isClusterAvailable || !entityDetails.name, }); - const {data: allTestsList} = useGetAllTestsQuery(null, {skip: !isClusterAvailable}); const [updateTestSuite] = useUpdateTestSuiteMutation(); const testsData: TestSuiteStepTest[] = useMemo(() => { @@ -74,128 +64,111 @@ const SettingsTests: React.FC<{openDefinition(): void}> = ({openDefinition}) => })); }, [testsList]); - const allTestsData: TestSuiteStepTest[] = useMemo(() => { - return (allTestsList || []).map(item => ({ - name: item.test.name, - namespace: item.test.namespace, - type: getTestExecutorIcon(executors, item.test.type), - })); - }, [allTestsList]); - - const hasParallelSteps = useMemo( - () => entityDetails?.steps?.some(step => step.execute.length > 1), - [entityDetails.steps] - ); - - const initialSteps: LocalStep[] = useMemo( - () => - entityDetails.steps - ? entityDetails.steps.map(step => { - const id = nanoid(); - - const firstItemInStep = step.execute[0]; - - if ('delay' in firstItemInStep) { - return { - ...firstItemInStep, - id, - }; - } + const initialSteps: LocalStep[][] = useMemo(() => { + if (!entityDetails.steps) { + return []; + } + return entityDetails.steps + .filter(step => step.execute?.length) + .map(step => { + return step.execute.map(item => { + const id = nanoid(); + if ('delay' in item) { return { - ...firstItemInStep, + ...item, id, - type: testsData.find(item => item.name === firstItemInStep.test)?.type || '', - stopTestOnFailure: step.stopTestOnFailure, }; - }) - : [], - [entityDetails?.steps, testsData] - ); + } + + return { + ...item, + id, + type: testsData.find(x => x.name === item.test)?.type || '', + }; + }); + }); + }, [testsData]); + + const [isDelayModalVisible, setIsDelayModalVisible] = useState(false); + const [isTestModalVisible, setIsTestModalVisible] = useState(false); + const [currentGroup, setCurrentGroup] = useState(0); - const [currentSteps = initialSteps, setCurrentSteps] = useState([]); + const [steps, setSteps] = useState(initialSteps); - const wasTouched = currentSteps !== initialSteps; + const wasTouched = steps !== initialSteps; useEffect(() => { - if (currentSteps !== initialSteps) { - setCurrentSteps(initialSteps); + setSteps(initialSteps); + }, [testsData]); + + const showTestModal = (group: number) => { + setIsTestModalVisible(true); + setCurrentGroup(group); + }; + + const showDelayModal = (group: number) => { + setIsDelayModalVisible(true); + setCurrentGroup(group); + }; + + // index may be i.e. 3.5, which means inserting between index 3 and 4 + const insertStep = (item: LocalStep, index: number) => { + const prevIndex = Math.floor(index); + const nextIndex = Math.ceil(index); + + if (prevIndex === nextIndex) { + setSteps([...steps.slice(0, index), [...steps[index], item], ...steps.slice(index + 1)]); + } else { + setSteps([...steps.slice(0, nextIndex), [item], ...steps.slice(nextIndex)]); } - }, [initialSteps]); + }; + + const addTest = (test: {test: string; type: string}) => insertStep({id: nanoid(), ...test}, currentGroup); + const addDelay = (delay: string) => insertStep({id: nanoid(), delay}, currentGroup); + + const onSave = () => { + const resultSteps = steps.map((items, i) => { + const stopOnFailure = Boolean(entityDetails?.steps?.[i]?.stopOnFailure); + const execute = items.map(item => pick(item, ['delay', 'test', 'namespace'])); + + if (isV2) { + const step = execute[0]; + if ('test' in step) { + return {execute: {name: step.test}, stopTestOnFailure: stopOnFailure}; + } + return {delay: {duration: formatMilliseconds(step.delay!)}}; + } + return {execute, stopOnFailure}; + }); - const saveSteps = () => { return updateTestSuite({ id: entityDetails.name, data: { ...entityDetails, - steps: currentSteps.map(step => { - return { - stopTestOnFailure: step.stopTestOnFailure, - execute: isV2 - ? step.test - ? {name: step.test} - : {delay: step.delay} - : [step.test ? {test: step.test} : {delay: step.delay}], - }; - }), + steps: resultSteps, }, }) - .then(res => displayDefaultNotificationFlow(res)) + .then(displayDefaultNotificationFlow) .then(() => { notificationCall('passed', 'Steps were successfully updated.'); }); }; - const onSelectStep = (value: string) => { - if (value === 'delay') { - setIsDelayModalVisible(true); - } else { - const {name, type} = JSON.parse(value); - - setCurrentSteps([ - ...currentSteps, - { - test: name, - id: nanoid(), - type, - stopTestOnFailure: false, - }, - ]); - } - }; - - const addDelay = (value: number) => { - setCurrentSteps([ - ...currentSteps, - { - delay: `${value}ms`, - id: nanoid(), - stopTestOnFailure: false, - }, - ]); - setIsDelayModalVisible(false); - }; - - const deleteStep = (index: number) => { - setCurrentSteps([...currentSteps.slice(0, index), ...currentSteps.slice(index + 1)]); - }; - - const scrollToBottom = () => { - if (!scrollRef.current) { - return; - } - - // @ts-ignore - scrollRef.current.scrollIntoView({behavior: 'smooth'}); - }; - - useEffect(() => { - scrollToBottom(); - }, [currentSteps?.length]); - - // TODO: Delete when we will support parallel editor - if (hasParallelSteps) { - return ( + return ( + + {!isV2 ? null : ( + + You are running an older agent on this environment. Updating your Testkube will enable parallel tests and + other new features. + + } + /> + )}
    = ({openDefinition}) => Tests in a test suite } + onConfirm={onSave} + onCancel={() => setSteps(initialSteps)} + isButtonsDisabled={!wasTouched} + isEditable={mayEdit} + enabled={mayEdit} + forceEnableButtons={wasTouched} > - - - <WarningOutlined /> This test suite is using parallel execution. - - - Unfortunately, we do not support visual editor for it yet. - - - We are working hard to deliver it for you soon. Until then, you may use{' '} - - Definition - {' '} - tab, to modify the test suite definition using YAML. - - - - -
    - ); - } - - return ( -
    - - Learn more about Tests in a test suite - - } - onConfirm={saveSteps} - onCancel={() => setCurrentSteps(initialSteps)} - isButtonsDisabled={!wasTouched} - isEditable={mayEdit} - enabled={mayEdit} - forceEnableButtons={wasTouched} - > - <> - {currentSteps?.length === 0 ? ( + {!steps?.length ? ( - Add your tests to this test suite + This test suite doesn't have any tests yet - - Select tests from the dropdown below to add them to this suite - + - ) : null} - - - {mayEdit ? ( - - ) : null} - - -
    + ) : ( + + )} + setIsDelayModalVisible(false)} onSubmit={addDelay} /> + setIsTestModalVisible(false)} onSubmit={addTest} /> + + +
    ); }; diff --git a/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/TestSuiteStepsFlow.tsx b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/TestSuiteStepsFlow.tsx new file mode 100644 index 000000000..3b0e873fb --- /dev/null +++ b/src/components/organisms/EntityDetails/EntityDetailsContent/Settings/SettingsTests/TestSuiteStepsFlow.tsx @@ -0,0 +1,147 @@ +import {useEffect, useMemo, useState} from 'react'; +import ReactFlow, {Controls, Edge, Node} from 'reactflow'; + +import {nanoid} from '@reduxjs/toolkit'; + +import {LocalStep} from '@src/models/testSuite'; + +import AddNode from './Nodes/AddNode'; +import IntersectionNode from './Nodes/IntersectionNode'; +import StepNode from './Nodes/StepNode'; +import {ReactFlowContainer, addHeight, itemHeight, itemWidth} from './SettingsTests.styled'; + +interface ExtendedNode extends Node { + group?: number | string; +} + +type TestSuiteStepsFlowProps = { + steps: LocalStep[][]; + setSteps: (steps: LocalStep[][]) => void; + showTestModal: (group: number) => void; + showDelayModal: (group: number) => void; + isV2: boolean; +}; + +// Configure +const horizontalGap = 150; +const verticalGapBeforeAdd = 20; +const verticalGapBetweenItems = 32; + +const getItemPosition = (group: number, itemIndex: number, offsetY: number) => ({ + x: horizontalGap + itemWidth / 2 + group * (itemWidth + horizontalGap), + y: itemHeight / 2 + itemIndex * (verticalGapBetweenItems + itemHeight) + offsetY, +}); +const getIntersectionPosition = (group: number, offsetY: number) => { + const {x, y} = getItemPosition(group, 0, offsetY); + return {x: x - itemWidth / 2 - horizontalGap / 2, y}; +}; +const getAddPosition = (group: number, groupLength: number, offsetY: number) => { + const {x, y} = getItemPosition(group, groupLength - 1, offsetY); + return {x, y: y + itemHeight / 2 + verticalGapBeforeAdd + addHeight / 2}; +}; +const getHeight = (maxLength: number) => getItemPosition(0, maxLength - 1, 0).y + itemHeight / 2; + +const TestSuiteStepsFlow: React.FC = props => { + const {steps, setSteps, showDelayModal, showTestModal, isV2} = props; + + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + + const deleteNode = (id: string, group: number) => { + if (steps[group].length === 1) { + if (steps[group][0].id === id) { + setSteps([...steps.slice(0, group), ...steps.slice(group + 1)]); + } + } else { + setSteps([...steps.slice(0, group), steps[group].filter(item => item.id !== id), ...steps.slice(group + 1)]); + } + }; + + useEffect(() => { + const newNodes: ExtendedNode[] = []; + const maxItemsLength = Math.max(...steps.map(x => x.length)); + const chartHeight = getHeight(maxItemsLength); + + steps.forEach((step, group) => { + const groupHeight = getHeight(step.length); + const groupOffsetY = (chartHeight - groupHeight) / 2; + + newNodes.push( + ...step.map((item, itemIndex) => ({ + type: 'step', + id: item.id!, + data: {deleteNode, item, last: group === steps.length - 1, group}, + position: getItemPosition(group, itemIndex, groupOffsetY), + })) + ); + + if (!isV2) { + newNodes.push({ + type: 'add', + id: nanoid(), + position: getAddPosition(group, step.length, groupOffsetY), + data: {showDelayModal, showTestModal, group}, + }); + } + }); + + for (let group = 0; group <= steps.length; group += 1) { + newNodes.push({ + type: 'intersection', + id: nanoid(), + position: getIntersectionPosition(group, (chartHeight - itemHeight) / 2), + data: { + showDelayModal, + showTestModal, + last: group === steps.length, + group, + }, + }); + } + + setNodes(newNodes); + }, [steps]); + + // add edges + useEffect(() => { + const newEdges: Edge[] = nodes.reduce((result, node) => { + if (node.type === 'intersection') { + const nextNodes = nodes.filter(x => x.type === 'step' && x.data.group === node.data.group); + return [ + ...result, + ...nextNodes.map(nextNode => ({ + id: `${node.id}-${nextNode.id}`, + source: node.id, + target: nextNode.id, + })), + ]; + } + if (node.type === 'step') { + const nextIntersection = nodes.find(x => x.type === 'intersection' && x.data.group === node.data.group + 1)!; + return [...result, {id: `${node.id}-${nextIntersection.id}`, source: node.id, target: nextIntersection.id}]; + } + return result; + }, [] as Edge[]); + + setEdges(newEdges); + }, [nodes]); + + const flowOptions: Partial[0]> = useMemo( + () => ({ + nodeTypes: {step: StepNode, intersection: IntersectionNode, add: AddNode}, + nodeOrigin: [0.5, 0.5], + defaultEdgeOptions: {interactionWidth: 0}, + }), + [] + ); + + return ( + + + + + + ); +}; + +export default TestSuiteStepsFlow; diff --git a/src/index.tsx b/src/index.tsx index b7ddec8c7..3aab5f01e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; import {Provider as ReduxProvider} from 'react-redux'; import {BrowserRouter} from 'react-router-dom'; +import 'reactflow/dist/style.css'; import {store} from '@redux/store'; diff --git a/src/models/testSuite.ts b/src/models/testSuite.ts index 849d88792..f47fc34b9 100644 --- a/src/models/testSuite.ts +++ b/src/models/testSuite.ts @@ -7,9 +7,15 @@ import {Variables} from './variable'; export interface TestSuiteStep { delay?: string; test?: string; + namespace?: string; +} + +export interface LocalStep extends TestSuiteStep { + type?: string; + id: string; } export interface TestSuiteBatchStep { - stopTestOnFailure: boolean; + stopOnFailure: boolean; execute: TestSuiteStep[]; } diff --git a/src/styles/globalStyles.ts b/src/styles/globalStyles.ts index c45c4ce9a..39d6d7d2a 100644 --- a/src/styles/globalStyles.ts +++ b/src/styles/globalStyles.ts @@ -431,6 +431,47 @@ export const GlobalStyle = createGlobalStyle` background-color: transparent; border: 1px dashed ${Colors.slate500}; } + + /* stylelint-disable-next-line selector-class-pattern */ + .react-flow__edge-path, .react-flow__connection-path { + stroke: ${Colors.indigo400}; + } + + /* stylelint-disable-next-line selector-class-pattern */ + .react-flow__handle { + border: 2px solid ${Colors.indigo900}; + background: ${Colors.indigo400}; + width: 10px; + height: 10px; + } + + /* stylelint-disable-next-line selector-class-pattern */ + .react-flow__attribution { + display: none; + } + + /* stylelint-disable-next-line selector-class-pattern */ + .react-flow__controls-button { + width: 30px; + height: 30px; + + border-bottom: 0; + background: ${Colors.slate800}; + + &:hover { + background-color: ${Colors.slate700}; + + svg { + fill: ${Colors.indigo400} + } + } + + transition: 0.3s all; + + svg { + fill: ${Colors.whitePure}; + } + } `; /* Media Sizes */ diff --git a/src/utils/array.spec.ts b/src/utils/array.spec.ts deleted file mode 100644 index 2801fba7d..000000000 --- a/src/utils/array.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {reorder} from './array'; - -describe('array', () => { - describe('reorder', () => { - it('should correctly reorder an array', () => { - const list = ['a', 'b', 'c']; - const result = reorder(list, 0, 2); - - expect(result).toEqual(['b', 'c', 'a']); - }); - }); -}); diff --git a/src/utils/array.ts b/src/utils/array.ts deleted file mode 100644 index 9bc228ea9..000000000 --- a/src/utils/array.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const reorder = (list: Array, startIndex: number, endIndex: number) => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - - return result; -}; diff --git a/src/utils/formatMilliseconds.ts b/src/utils/formatMilliseconds.ts new file mode 100644 index 000000000..024f17cf8 --- /dev/null +++ b/src/utils/formatMilliseconds.ts @@ -0,0 +1,16 @@ +export function formatMilliseconds(duration: number | string): number { + if (typeof duration === 'number' || /^[0-9]+$/.test(duration)) { + return Number(duration); + } + const [, value, unit] = `${duration}`.match(/^([0-9]+)(ms|s|m|h)?$/)!; + if (unit === 's') { + return Number(value) * 1000; + } + if (unit === 'm') { + return Number(value) * 60_000; + } + if (unit === 'h') { + return Number(value) * 3_600_000; + } + return Number(value); +} diff --git a/src/utils/testSuites.ts b/src/utils/testSuites.ts index 5a0cf7aa7..ab66af3ea 100644 --- a/src/utils/testSuites.ts +++ b/src/utils/testSuites.ts @@ -11,7 +11,11 @@ function mapExecute(execute: any): TestSuiteStep[] { if (!execute) { return []; } - return ([] as TestSuiteStep[]).concat(execute!).map(item => ('name' in item ? {test: item.name as string} : item)); + return ([] as TestSuiteStep[]) + .concat(execute!) + .map(item => + 'name' in item ? {test: item.name as string} : 'duration' in item ? {delay: (item as any).duration} : item + ); } export function convertTestSuiteV2ToV3(suite: TestSuite): TestSuite { @@ -19,7 +23,7 @@ export function convertTestSuiteV2ToV3(suite: TestSuite): TestSuite { ...suite, steps: suite.steps?.map(step => ({ ...step, - execute: mapExecute(step.execute!), + execute: mapExecute(step.execute || (step as any).delay!), })), }; }