From 7a386e5c5471ee726fbdf91a7aa2a76e8c31f0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Tue, 10 Oct 2023 04:36:58 +0200 Subject: [PATCH] frontend: clusterActionSlice: Refactor clusterActions into a slice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use new style redux toolkit, and not sagas. - Added some documentation. - Added some tests. Signed-off-by: René Dudfield --- frontend/package-lock.json | 350 ++++++++++-------- frontend/package.json | 4 +- .../PluginSettings/PluginSettings.stories.tsx | 4 +- .../src/components/common/ActionsNotifier.tsx | 3 +- .../common/Resource/CreateButton.tsx | 2 +- .../common/Resource/DeleteButton.tsx | 2 +- .../components/common/Resource/EditButton.tsx | 2 +- .../common/Resource/RestartButton.tsx | 2 +- .../common/Resource/ScaleButton.tsx | 2 +- .../components/common/SimpleTable.stories.tsx | 3 +- frontend/src/components/cronjob/Details.tsx | 2 +- frontend/src/components/node/Details.tsx | 2 +- frontend/src/redux/actions/actions.tsx | 50 --- frontend/src/redux/clusterActionSlice.test.ts | 107 ++++++ frontend/src/redux/clusterActionSlice.ts | 329 ++++++++++++++++ frontend/src/redux/reducers/clusterAction.tsx | 31 -- frontend/src/redux/reducers/reducers.tsx | 2 +- frontend/src/redux/sagas/sagas.tsx | 139 ------- frontend/src/redux/stores/store.tsx | 24 +- plugins/headlamp-plugin/package-lock.json | 347 +++++++++-------- plugins/headlamp-plugin/package.json | 11 +- 21 files changed, 867 insertions(+), 551 deletions(-) create mode 100644 frontend/src/redux/clusterActionSlice.test.ts create mode 100644 frontend/src/redux/clusterActionSlice.ts delete mode 100644 frontend/src/redux/reducers/clusterAction.tsx delete mode 100644 frontend/src/redux/sagas/sagas.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 74ef293d4d5..601c70a17c0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -78,7 +78,6 @@ "react-scripts": "5.0.0", "react-window": "^1.8.7", "recharts": "^2.1.4", - "redux-saga": "^1.1.3", "semver": "^7.3.5", "spacetime": "^7.4.0", "stream-browserify": "^3.0.0", @@ -104,6 +103,8 @@ "@storybook/preset-create-react-app": "^4.1.1", "@storybook/react": "6.4.13", "@testing-library/user-event": "^14.5.1", + "@types/redux-mock-store": "^1.0.4", + "fetch-mock": "^9.11.0", "http-proxy-middleware": "^2.0.1", "husky": "^4.3.8", "i18next-parser": "^4.7.0", @@ -112,6 +113,7 @@ "msw": "^0.47.4", "msw-storybook-addon": "^1.6.3", "prettier": "^2.4.1", + "redux-mock-store": "^1.5.4", "resize-observer-polyfill": "^1.5.1", "typedoc": "0.22.10", "typedoc-hugo-theme": "1.0.0", @@ -4237,53 +4239,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@redux-saga/core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", - "integrity": "sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@redux-saga/deferred": "^1.1.2", - "@redux-saga/delay-p": "^1.1.2", - "@redux-saga/is": "^1.1.2", - "@redux-saga/symbols": "^1.1.2", - "@redux-saga/types": "^1.1.0", - "redux": "^4.0.4", - "typescript-tuple": "^2.2.1" - } - }, - "node_modules/@redux-saga/deferred": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.1.2.tgz", - "integrity": "sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==" - }, - "node_modules/@redux-saga/delay-p": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.1.2.tgz", - "integrity": "sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==", - "dependencies": { - "@redux-saga/symbols": "^1.1.2" - } - }, - "node_modules/@redux-saga/is": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.2.tgz", - "integrity": "sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==", - "dependencies": { - "@redux-saga/symbols": "^1.1.2", - "@redux-saga/types": "^1.1.0" - } - }, - "node_modules/@redux-saga/symbols": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.2.tgz", - "integrity": "sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==" - }, - "node_modules/@redux-saga/types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz", - "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==" - }, "node_modules/@reduxjs/toolkit": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", @@ -13820,6 +13775,15 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, + "node_modules/@types/redux-mock-store": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.4.tgz", + "integrity": "sha512-53nDnXba4M7aOJsRod8HKENDC9M2ccm19yZcXImoP15oDLuBru+Q+WKWOCQwKYOC1S/6AJx58mFp8kd4s8q1rQ==", + "dev": true, + "dependencies": { + "redux": "^4.0.5" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -20611,6 +20575,82 @@ "bser": "2.1.1" } }, + "node_modules/fetch-mock": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "engines": { + "node": ">=4.0.0" + }, + "funding": { + "type": "charity", + "url": "https://www.justgiving.com/refugee-support-europe" + }, + "peerDependencies": { + "node-fetch": "*" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/fetch-mock/node_modules/core-js": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.0.tgz", + "integrity": "sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/fetch-mock/node_modules/path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/fetch-mock/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -23853,6 +23893,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -26212,6 +26258,18 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -32495,12 +32553,13 @@ "@babel/runtime": "^7.9.2" } }, - "node_modules/redux-saga": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz", - "integrity": "sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==", + "node_modules/redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, "dependencies": { - "@redux-saga/core": "^1.1.3" + "lodash.isplainobject": "^4.0.6" } }, "node_modules/redux-thunk": { @@ -36466,27 +36525,6 @@ "node": ">=4.2.0" } }, - "node_modules/typescript-compare": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", - "dependencies": { - "typescript-logic": "^0.0.0" - } - }, - "node_modules/typescript-logic": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" - }, - "node_modules/typescript-tuple": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", - "dependencies": { - "typescript-compare": "^0.0.2" - } - }, "node_modules/uglify-js": { "version": "3.15.5", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", @@ -41652,53 +41690,6 @@ "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", "dev": true }, - "@redux-saga/core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz", - "integrity": "sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==", - "requires": { - "@babel/runtime": "^7.6.3", - "@redux-saga/deferred": "^1.1.2", - "@redux-saga/delay-p": "^1.1.2", - "@redux-saga/is": "^1.1.2", - "@redux-saga/symbols": "^1.1.2", - "@redux-saga/types": "^1.1.0", - "redux": "^4.0.4", - "typescript-tuple": "^2.2.1" - } - }, - "@redux-saga/deferred": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.1.2.tgz", - "integrity": "sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ==" - }, - "@redux-saga/delay-p": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.1.2.tgz", - "integrity": "sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==", - "requires": { - "@redux-saga/symbols": "^1.1.2" - } - }, - "@redux-saga/is": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.2.tgz", - "integrity": "sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==", - "requires": { - "@redux-saga/symbols": "^1.1.2", - "@redux-saga/types": "^1.1.0" - } - }, - "@redux-saga/symbols": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.2.tgz", - "integrity": "sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ==" - }, - "@redux-saga/types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz", - "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg==" - }, "@reduxjs/toolkit": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz", @@ -49053,6 +49044,15 @@ "@types/react": "*" } }, + "@types/redux-mock-store": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.4.tgz", + "integrity": "sha512-53nDnXba4M7aOJsRod8HKENDC9M2ccm19yZcXImoP15oDLuBru+Q+WKWOCQwKYOC1S/6AJx58mFp8kd4s8q1rQ==", + "dev": true, + "requires": { + "redux": "^4.0.5" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -54353,6 +54353,64 @@ "bser": "2.1.1" } }, + "fetch-mock": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "dev": true, + "requires": { + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "dependencies": { + "core-js": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.0.tgz", + "integrity": "sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==", + "dev": true + }, + "path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -56819,6 +56877,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, "is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -58633,6 +58697,18 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -63125,12 +63201,13 @@ "@babel/runtime": "^7.9.2" } }, - "redux-saga": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz", - "integrity": "sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==", + "redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, "requires": { - "@redux-saga/core": "^1.1.3" + "lodash.isplainobject": "^4.0.6" } }, "redux-thunk": { @@ -66231,27 +66308,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==" }, - "typescript-compare": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", - "requires": { - "typescript-logic": "^0.0.0" - } - }, - "typescript-logic": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" - }, - "typescript-tuple": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", - "requires": { - "typescript-compare": "^0.0.2" - } - }, "uglify-js": { "version": "3.15.5", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index a57dc72a408..62681be7db2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -73,7 +73,6 @@ "react-scripts": "5.0.0", "react-window": "^1.8.7", "recharts": "^2.1.4", - "redux-saga": "^1.1.3", "semver": "^7.3.5", "spacetime": "^7.4.0", "stream-browserify": "^3.0.0", @@ -191,6 +190,8 @@ "@storybook/preset-create-react-app": "^4.1.1", "@storybook/react": "6.4.13", "@testing-library/user-event": "^14.5.1", + "@types/redux-mock-store": "^1.0.4", + "fetch-mock": "^9.11.0", "http-proxy-middleware": "^2.0.1", "husky": "^4.3.8", "i18next-parser": "^4.7.0", @@ -199,6 +200,7 @@ "msw": "^0.47.4", "msw-storybook-addon": "^1.6.3", "prettier": "^2.4.1", + "redux-mock-store": "^1.5.4", "resize-observer-polyfill": "^1.5.1", "typedoc": "0.22.10", "typedoc-hugo-theme": "1.0.0", diff --git a/frontend/src/components/App/PluginSettings/PluginSettings.stories.tsx b/frontend/src/components/App/PluginSettings/PluginSettings.stories.tsx index 6b4826633e2..7022e8d793a 100644 --- a/frontend/src/components/App/PluginSettings/PluginSettings.stories.tsx +++ b/frontend/src/components/App/PluginSettings/PluginSettings.stories.tsx @@ -1,6 +1,4 @@ import { Meta, Story } from '@storybook/react/types-6-0'; -import React from 'react'; -import store from '../../../redux/stores/store'; import { TestContext } from '../../../test'; import { PluginSettingsPure, PluginSettingsPureProps } from './PluginSettings'; @@ -10,7 +8,7 @@ export default { } as Meta; const Template: Story = args => ( - + ); diff --git a/frontend/src/components/common/ActionsNotifier.tsx b/frontend/src/components/common/ActionsNotifier.tsx index 30db19ea823..7502606eca7 100644 --- a/frontend/src/components/common/ActionsNotifier.tsx +++ b/frontend/src/components/common/ActionsNotifier.tsx @@ -4,8 +4,7 @@ import { useSnackbar } from 'notistack'; import React from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { CLUSTER_ACTION_GRACE_PERIOD } from '../../lib/util'; -import { ClusterAction } from '../../redux/actions/actions'; +import { CLUSTER_ACTION_GRACE_PERIOD, ClusterAction } from '../../redux/clusterActionSlice'; import { useTypedSelector } from '../../redux/reducers/reducers'; export interface PureActionsNotifierProps { diff --git a/frontend/src/components/common/Resource/CreateButton.tsx b/frontend/src/components/common/Resource/CreateButton.tsx index a3c6357c462..63c4f47d722 100644 --- a/frontend/src/components/common/Resource/CreateButton.tsx +++ b/frontend/src/components/common/Resource/CreateButton.tsx @@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { apply } from '../../../lib/k8s/apiProxy'; import { KubeObjectInterface } from '../../../lib/k8s/cluster'; -import { clusterAction } from '../../../redux/actions/actions'; +import { clusterAction } from '../../../redux/clusterActionSlice'; import ActionButton from '../ActionButton'; import EditorDialog from './EditorDialog'; diff --git a/frontend/src/components/common/Resource/DeleteButton.tsx b/frontend/src/components/common/Resource/DeleteButton.tsx index 2f15926f21f..3925846ccec 100644 --- a/frontend/src/components/common/Resource/DeleteButton.tsx +++ b/frontend/src/components/common/Resource/DeleteButton.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { KubeObject } from '../../../lib/k8s/cluster'; -import { CallbackActionOptions, clusterAction } from '../../../redux/actions/actions'; +import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice'; import ActionButton from '../ActionButton'; import { ConfirmDialog } from '../Dialog'; import AuthVisible from './AuthVisible'; diff --git a/frontend/src/components/common/Resource/EditButton.tsx b/frontend/src/components/common/Resource/EditButton.tsx index 7bf7a35f054..06b1fcab611 100644 --- a/frontend/src/components/common/Resource/EditButton.tsx +++ b/frontend/src/components/common/Resource/EditButton.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { KubeObject, KubeObjectInterface } from '../../../lib/k8s/cluster'; -import { CallbackActionOptions, clusterAction } from '../../../redux/actions/actions'; +import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice'; import ActionButton from '../ActionButton'; import AuthVisible from './AuthVisible'; import EditorDialog from './EditorDialog'; diff --git a/frontend/src/components/common/Resource/RestartButton.tsx b/frontend/src/components/common/Resource/RestartButton.tsx index 3539725a348..5323a2013bf 100644 --- a/frontend/src/components/common/Resource/RestartButton.tsx +++ b/frontend/src/components/common/Resource/RestartButton.tsx @@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { apply } from '../../../lib/k8s/apiProxy'; import { KubeObject } from '../../../lib/k8s/cluster'; -import { clusterAction } from '../../../redux/actions/actions'; +import { clusterAction } from '../../../redux/clusterActionSlice'; import AuthVisible from './AuthVisible'; interface RestartButtonProps { diff --git a/frontend/src/components/common/Resource/ScaleButton.tsx b/frontend/src/components/common/Resource/ScaleButton.tsx index e395b4b5820..bf5cb50d365 100644 --- a/frontend/src/components/common/Resource/ScaleButton.tsx +++ b/frontend/src/components/common/Resource/ScaleButton.tsx @@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { KubeObject } from '../../../lib/k8s/cluster'; -import { CallbackActionOptions, clusterAction } from '../../../redux/actions/actions'; +import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice'; import { LightTooltip } from '../Tooltip'; import AuthVisible from './AuthVisible'; diff --git a/frontend/src/components/common/SimpleTable.stories.tsx b/frontend/src/components/common/SimpleTable.stories.tsx index 422f3f6ac24..96bf478a5d0 100644 --- a/frontend/src/components/common/SimpleTable.stories.tsx +++ b/frontend/src/components/common/SimpleTable.stories.tsx @@ -4,7 +4,6 @@ import { Meta, Story } from '@storybook/react/types-6-0'; import { useLocation } from 'react-router-dom'; import { KubeObjectInterface } from '../../lib/k8s/cluster'; import { useFilterFunc } from '../../lib/util'; -import store from '../../redux/stores/store'; import { TestContext, TestContextProps } from '../../test'; import SectionFilterHeader from './SectionFilterHeader'; import SimpleTable, { SimpleTableProps } from './SimpleTable'; @@ -33,7 +32,7 @@ function TestSimpleTable(props: SimpleTableProps) { } const Template: Story = args => ( - + ); diff --git a/frontend/src/components/cronjob/Details.tsx b/frontend/src/components/cronjob/Details.tsx index 660134f6063..bfc5046cd0f 100644 --- a/frontend/src/components/cronjob/Details.tsx +++ b/frontend/src/components/cronjob/Details.tsx @@ -18,7 +18,7 @@ import { apply } from '../../lib/k8s/apiProxy'; import { KubeObjectInterface } from '../../lib/k8s/cluster'; import CronJob from '../../lib/k8s/cronJob'; import Job from '../../lib/k8s/job'; -import { clusterAction } from '../../redux/actions/actions'; +import { clusterAction } from '../../redux/clusterActionSlice'; import { ActionButton, ObjectEventList } from '../common'; import { MainInfoSection } from '../common/Resource'; import AuthVisible from '../common/Resource/AuthVisible'; diff --git a/frontend/src/components/node/Details.tsx b/frontend/src/components/node/Details.tsx index 3663ffa0d81..f17e5f0c1fe 100644 --- a/frontend/src/components/node/Details.tsx +++ b/frontend/src/components/node/Details.tsx @@ -11,7 +11,7 @@ import { apply, drainNode, drainNodeStatus } from '../../lib/k8s/apiProxy'; import { KubeMetrics } from '../../lib/k8s/cluster'; import Node from '../../lib/k8s/node'; import { getCluster, timeAgo } from '../../lib/util'; -import { clusterAction } from '../../redux/actions/actions'; +import { clusterAction } from '../../redux/clusterActionSlice'; import { CpuCircularChart, MemoryCircularChart } from '../cluster/Charts'; import { ActionButton, ObjectEventList, StatusLabelProps } from '../common'; import { HeaderLabel, StatusLabel, ValueLabel } from '../common/Label'; diff --git a/frontend/src/redux/actions/actions.tsx b/frontend/src/redux/actions/actions.tsx index 022d173e108..6e8b45e34fe 100644 --- a/frontend/src/redux/actions/actions.tsx +++ b/frontend/src/redux/actions/actions.tsx @@ -1,4 +1,3 @@ -import { OptionsObject as SnackbarProps } from 'notistack'; import { AppLogoType } from '../../components/App/AppLogo'; import { ClusterChooserType } from '../../components/cluster/ClusterChooser'; import { ResourceTableProps } from '../../components/common/Resource/ResourceTable'; @@ -8,9 +7,6 @@ import { Notification } from '../../lib/notification'; import { Route } from '../../lib/router'; import { UIState } from '../reducers/ui'; -export const CLUSTER_ACTION = 'CLUSTER_ACTION'; -export const CLUSTER_ACTION_UPDATE = 'CLUSTER_ACTION_UPDATE'; -export const CLUSTER_ACTION_CANCEL = 'CLUSTER_ACTION_CANCEL'; export const UI_SIDEBAR_SET_SELECTED = 'UI_SIDEBAR_SET_SELECTED'; export const UI_SIDEBAR_SET_VISIBLE = 'UI_SIDEBAR_SET_VISIBLE'; export const UI_SIDEBAR_SET_ITEM = 'UI_SIDEBAR_SET_ITEM'; @@ -38,41 +34,6 @@ export interface BrandingProps { export const UI_SET_NOTIFICATIONS = 'UI_SET_NOTIFICATIONS'; export const UI_UPDATE_NOTIFICATION = 'UI_UPDATE_NOTIFICATION'; -export interface ClusterActionButton { - label: string; - actionToDispatch: string; -} - -export interface ClusterAction { - id: string; - key?: string; - message?: string; - url?: string; - buttons?: ClusterActionButton[]; - dismissSnackbar?: string; - snackbarProps?: SnackbarProps; -} - -export interface CallbackAction extends CallbackActionOptions { - callback: (...args: any[]) => void; -} - -export interface CallbackActionOptions { - startUrl?: string; - cancelUrl?: string; - errorUrl?: string; - successUrl?: string; - startMessage?: string; - cancelledMessage?: string; - errorMessage?: string; - successMessage?: string; - startOptions?: SnackbarProps; - cancelledOptions?: SnackbarProps; - successOptions?: SnackbarProps; - errorOptions?: SnackbarProps; - cancelCallback?: (...args: any[]) => void; -} - export interface Action { type: string; [propName: string]: any; @@ -95,17 +56,6 @@ export type TableColumnsProcessor = { }) => ResourceTableProps['columns']; }; -export function clusterAction( - callback: CallbackAction['callback'], - actionOptions: CallbackActionOptions = {} -) { - return { type: CLUSTER_ACTION, callback, ...actionOptions }; -} - -export function updateClusterAction(actionOptions: ClusterAction) { - return { type: CLUSTER_ACTION_UPDATE, ...actionOptions }; -} - export function setSidebarSelected(selected: string | null, sidebar: string | null = '') { return { type: UI_SIDEBAR_SET_SELECTED, selected: { item: selected, sidebar } }; } diff --git a/frontend/src/redux/clusterActionSlice.test.ts b/frontend/src/redux/clusterActionSlice.test.ts new file mode 100644 index 00000000000..bf0302d0883 --- /dev/null +++ b/frontend/src/redux/clusterActionSlice.test.ts @@ -0,0 +1,107 @@ +import { AnyAction } from 'redux'; +import configureStore from 'redux-mock-store'; +import thunk, { ThunkDispatch } from 'redux-thunk'; +import clusterActionSliceReducer, { + CallbackAction, + cancelClusterAction, + CLUSTER_ACTION_GRACE_PERIOD, + executeClusterAction, + // initialState, + updateClusterAction, +} from './clusterActionSlice'; +import { RootState } from './stores/store'; + +const middlewares = [thunk]; + +type DispatchType = ThunkDispatch; +const mockStore = configureStore(middlewares); + +jest.setTimeout(10000); + +describe('clusterActionSlice', () => { + let store: ReturnType; + + beforeEach(() => { + // Reset the store after each test + store = mockStore(); + }); + + it('should execute cluster action', async () => { + const callback = jest.fn(() => Promise.resolve()); + + const action: CallbackAction = { + callback, + startMessage: 'Starting', + successMessage: 'Success', + errorMessage: 'Error', + cancelledMessage: 'Cancelled', + startOptions: {}, + successOptions: { variant: 'success' }, + errorOptions: { variant: 'error' }, + cancelledOptions: {}, + }; + + jest.useFakeTimers(); + const dispatchedAction = store.dispatch(executeClusterAction(action)); + jest.advanceTimersByTime(CLUSTER_ACTION_GRACE_PERIOD); + await dispatchedAction; + const actions = store.getActions(); + expect(actions[0].type).toBe('clusterAction/execute/pending'); + expect(callback).toHaveBeenCalledTimes(1); + + // sucess action is done + expect(actions).toContainEqual( + expect.objectContaining({ + type: updateClusterAction.type, + payload: expect.objectContaining({ + message: 'Success', + }), + }) + ); + }); + + it('should dispatch a cancelled action if is cancelled within grace period', async () => { + const callback = jest.fn(() => Promise.resolve()); + + const action: CallbackAction = { + callback, + startMessage: 'Starting', + successMessage: 'Success', + errorMessage: 'Error', + cancelledMessage: 'Cancelled', + startOptions: {}, + successOptions: { variant: 'success' }, + errorOptions: { variant: 'error' }, + cancelledOptions: {}, + }; + + jest.useFakeTimers(); + const dispatchedAction = store.dispatch(executeClusterAction(action)); + + jest.advanceTimersByTime(CLUSTER_ACTION_GRACE_PERIOD / 2); + + const actionKey = store.getActions().find(action => action.payload?.id !== undefined) + ?.payload?.id; + + clusterActionSliceReducer(undefined, cancelClusterAction(actionKey)); + await dispatchedAction; + + // cancelled action is done + const actions = store.getActions(); + expect(actions).toContainEqual( + expect.objectContaining({ + type: updateClusterAction.type, + payload: expect.objectContaining({ + message: 'Cancelled', + }), + }) + ); + + // The callback wasn't called. + expect(callback).not.toHaveBeenCalled(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); +}); diff --git a/frontend/src/redux/clusterActionSlice.ts b/frontend/src/redux/clusterActionSlice.ts new file mode 100644 index 00000000000..c3464a16989 --- /dev/null +++ b/frontend/src/redux/clusterActionSlice.ts @@ -0,0 +1,329 @@ +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import i18next from 'i18next'; +import { OptionsObject as SnackbarProps } from 'notistack'; + +/** + * See components/common/ActionsNotifier.tsx for a user of cluster actions. + */ + +/** + * A button to display on the action. + */ +export interface ClusterActionButton { + /** + * The label to display on the button. + */ + label: string; + /** + * The action to dispatch when the button is clicked. + */ + actionToDispatch: string; +} + +export interface ClusterAction { + /** + * A unique id for the action. + */ + id: string; + /** + * A unique key for the action. + */ + key?: string; + /** + * The message to display on the action. + */ + message?: string; + /** + * The url to navigate to when the action is complete. + */ + url?: string; + /** + * The buttons to display on the action. + */ + buttons?: ClusterActionButton[]; + /** + * The id of the snackbar to dismiss. + */ + dismissSnackbar?: string; + /** + * The props to pass to the snackbar. Could be { variant: 'success' }. + */ + snackbarProps?: SnackbarProps; +} + +export interface CallbackAction extends CallbackActionOptions { + callback: (...args: any[]) => void; +} + +export interface CallbackActionOptions { + /** + * The url to navigate to when the action has started. + */ + startUrl?: string; + /** + * The url to navigate to when it is cancelled. + */ + cancelUrl?: string; + /** + * The url to navigate to when there is an error. + */ + errorUrl?: string; + /** + * The url to navigate to when it is successful. + */ + successUrl?: string; + /** + * The message to display when the action has started. + */ + startMessage?: string; + /** + * The message to display when the action is cancelled. + */ + cancelledMessage?: string; + /** + * The message to display when there is an error. + */ + errorMessage?: string; + /** + * The message to display when it is successful. + */ + successMessage?: string; + /** + * The props to pass to the snackbar when the action has started. + */ + startOptions?: SnackbarProps; + /** + * The props to pass to the snackbar when the action is cancelled. + */ + cancelledOptions?: SnackbarProps; + /** + * The props to pass to the snackbar when there is an error. + */ + successOptions?: SnackbarProps; + /** + * The props to pass to the snackbar when it is successful. + */ + errorOptions?: SnackbarProps; + /** + * A callback to execute when the action is cancelled. + */ + cancelCallback?: (...args: any[]) => void; +} + +/** + * A unique key for each action. + */ +export interface ClusterState { + [id: string]: ClusterAction; +} + +export const initialState: ClusterState = {}; + +const controllers = new Map(); + +/** The amount of time to wait before allowing the action to be cancelled. */ +export const CLUSTER_ACTION_GRACE_PERIOD = 5000; + +/** + * Uses the callback to execute an action and dispatches actions + * to update the UI based on the result. + * + * Gives the user 5 seconds to cancel the action before executing it. + */ +export const executeClusterAction = createAsyncThunk( + 'clusterAction/execute', + async (action: CallbackAction, { dispatch, rejectWithValue }) => { + const actionKey = (new Date().getTime() + Math.random()).toString(); + + /** + * See the handler for clusterAction/cancel/ in extraReducers below. + */ + const uniqueCancelActionType = 'clusterAction/cancel/' + actionKey; + + const controller = new AbortController(); + controllers.set(actionKey, controller); + + const { + callback, + startUrl, + cancelUrl, + successUrl, + startMessage, + cancelledMessage, + errorMessage, + errorUrl, + successMessage, + cancelCallback, + startOptions = {}, + cancelledOptions = {}, + successOptions = { variant: 'success' }, + errorOptions = { variant: 'error' }, + } = action; + + // Dispatch actions for all the states. + + function dispatchStart() { + dispatch( + updateClusterAction({ + id: actionKey, + message: startMessage, + url: startUrl, + buttons: [ + { + label: i18next.t('frequent|Cancel'), + actionToDispatch: uniqueCancelActionType, + }, + ], + snackbarProps: startOptions, + }) + ); + } + function dispatchSuccess() { + dispatch( + updateClusterAction({ + buttons: undefined, + dismissSnackbar: actionKey, + id: actionKey, + message: successMessage, + snackbarProps: successOptions, + url: successUrl, + }) + ); + } + function dispatchCancelled() { + dispatch( + updateClusterAction({ + buttons: undefined, + id: actionKey, + message: cancelledMessage, + dismissSnackbar: actionKey, + url: cancelUrl, + snackbarProps: cancelledOptions, + }) + ); + } + function dispatchError() { + dispatch( + updateClusterAction({ + buttons: undefined, + dismissSnackbar: actionKey, + id: actionKey, + message: errorMessage, + snackbarProps: errorOptions, + url: errorUrl, + }) + ); + } + function dispatchClose() { + dispatch( + updateClusterAction({ + id: actionKey, + }) + ); + } + + async function cancellableActionLogic() { + dispatchStart(); + try { + await new Promise((resolve, reject) => { + const timeoutId = setTimeout(resolve, CLUSTER_ACTION_GRACE_PERIOD); + controller.signal.addEventListener('abort', () => { + clearTimeout(timeoutId); + reject('Action cancelled'); + }); + }); + + if (controller.signal.aborted) { + return rejectWithValue('Action cancelled'); + } + callback(); + dispatchSuccess(); + } catch (err) { + if ((err as Error).message === 'Action cancelled' || controller.signal.aborted) { + dispatchCancelled(); + if (cancelCallback) { + try { + cancelCallback(); + } catch (err) { + console.error(err); + } + } + } else { + dispatchError(); + } + } finally { + controllers.delete(actionKey); + setTimeout(dispatchClose, 3000); + } + } + + await cancellableActionLogic(); + return actionKey; + } +); + +const clusterActionSlice = createSlice({ + name: 'clusterAction', + initialState, + reducers: { + /** + * Updates the state of the action. + * + * If only id is provided, the action is removed from the state. + */ + updateClusterAction: ( + state, + action: PayloadAction & { id: string }> + ) => { + const { id, ...actionOptions } = action.payload; + if (Object.keys(actionOptions).length === 0) { + delete state[id]; + } else { + const { snackbarProps, ...otherActionOptions } = actionOptions; + state[id] = { ...state[id], ...otherActionOptions }; + state[id].snackbarProps = snackbarProps as any; // any because snackbarProps is problematic to ts + } + }, + + /** + * Cancels the action with the given key. + */ + cancelClusterAction: (state, action: PayloadAction) => { + const actionKey = action.payload; + const controller = controllers.get(actionKey); + + if (controller) { + controller.abort(); + controllers.delete(actionKey); + } + delete state[actionKey]; + }, + }, + + extraReducers: builder => { + builder.addMatcher( + action => action.type.startsWith('clusterAction/cancel/'), + (state, action) => { + const actionKey = action.type.split('clusterAction/cancel/')[1]; + clusterActionSlice.caseReducers.cancelClusterAction(state, { + type: 'clusterAction/cancelClusterAction', + payload: actionKey, + }); + } + ); + }, +}); + +export const { updateClusterAction, cancelClusterAction } = clusterActionSlice.actions; + +/** + * Executes the callback action with the given options. + */ +export function clusterAction( + callback: CallbackAction['callback'], + actionOptions: CallbackActionOptions = {} +) { + return executeClusterAction({ callback, ...actionOptions }); +} + +export default clusterActionSlice.reducer; diff --git a/frontend/src/redux/reducers/clusterAction.tsx b/frontend/src/redux/reducers/clusterAction.tsx deleted file mode 100644 index 39251aaa828..00000000000 --- a/frontend/src/redux/reducers/clusterAction.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import _ from 'lodash'; -import { Action, CLUSTER_ACTION_UPDATE, ClusterAction } from '../actions/actions'; - -interface ClusterState { - [id: string]: ClusterAction; // todo: Complete the type. -} - -export const INITIAL_STATE: ClusterState = { - // id: { message, ... } . See the ActionsNotifier. -}; - -function cluster(clusterActions = _.cloneDeep(INITIAL_STATE), action: ClusterAction & Action) { - const { type, id, ...actionOptions } = action; - const newState = { ..._.cloneDeep(clusterActions) }; - switch (type) { - case CLUSTER_ACTION_UPDATE: - if (_.isEmpty(actionOptions)) { - delete newState[id]; - } else { - newState[id] = { ...(action as ClusterAction) }; - } - break; - - default: - break; - } - - return newState; -} - -export default cluster; diff --git a/frontend/src/redux/reducers/reducers.tsx b/frontend/src/redux/reducers/reducers.tsx index 046dd250860..2edf518a849 100644 --- a/frontend/src/redux/reducers/reducers.tsx +++ b/frontend/src/redux/reducers/reducers.tsx @@ -2,10 +2,10 @@ import { TypedUseSelectorHook, useSelector } from 'react-redux'; import { combineReducers } from 'redux'; import pluginsReducer from '../../plugin/pluginsSlice'; import actionButtons from '../actionButtonsSlice'; +import clusterAction from '../clusterActionSlice'; import configReducer from '../configSlice'; import detailsViewSectionsSlice from '../detailsViewSectionsSlice'; import filterReducer from '../filterSlice'; -import clusterAction from './clusterAction'; import uiReducer from './ui'; const reducers = combineReducers({ diff --git a/frontend/src/redux/sagas/sagas.tsx b/frontend/src/redux/sagas/sagas.tsx deleted file mode 100644 index 9aba96b3736..00000000000 --- a/frontend/src/redux/sagas/sagas.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import i18next from 'i18next'; -import { all, call, cancelled, delay, put, race, take, takeEvery } from 'redux-saga/effects'; -import { CLUSTER_ACTION_GRACE_PERIOD } from '../../lib/util'; -import { - Action, - CallbackAction, - CLUSTER_ACTION, - CLUSTER_ACTION_CANCEL, - ClusterAction, - updateClusterAction, -} from '../actions/actions'; - -function newActionKey() { - return (new Date().getTime() + Math.random()).toString(); -} - -function* watchClusterAction() { - yield takeEvery(CLUSTER_ACTION, clusterActionWithCancellation); -} - -function* clusterActionWithCancellation(action: Action & CallbackAction) { - const actionKey = newActionKey(); - // We create a unique action type so we're sure that the cancellation - // is done on the right actions. - const uniqueCancelAction = CLUSTER_ACTION_CANCEL + actionKey; - yield race({ - task: call(doClusterAction, action, actionKey, uniqueCancelAction), - cancel: take(uniqueCancelAction), - }); -} - -function* doClusterAction(action: CallbackAction, actionKey: string, uniqueCancelAction: string) { - const { - callback, - startUrl, - cancelUrl, - successUrl, - startMessage, - cancelledMessage, - errorMessage, - errorUrl, - successMessage, - cancelCallback, - startOptions = {}, - cancelledOptions = {}, - successOptions = { variant: 'success' }, - errorOptions = { variant: 'error' }, - } = action; - - try { - yield put( - updateClusterAction({ - key: actionKey, - id: actionKey, - message: startMessage, - url: startUrl, - buttons: [ - { - label: i18next.t('translation|Cancel'), - actionToDispatch: uniqueCancelAction, - }, - ], - snackbarProps: startOptions, - }) - ); - - yield delay(CLUSTER_ACTION_GRACE_PERIOD); - } finally { - // Check if it's been cancelled. - // @ts-ignore: TS7057 - // @todo: Seems to be an bad-typing issue... this code is idiomatic and did check before. - if (yield cancelled()) { - yield put( - updateClusterAction({ - id: actionKey, - message: cancelledMessage, - dismissSnackbar: actionKey, - url: cancelUrl, - snackbarProps: cancelledOptions, - }) - ); - if (cancelCallback) { - yield call(cancelCallback); - } - } else { - // Actually perform the action. This part is no longer cancellable, - // so it's here instead of within the try block. - let success = false; - try { - yield call(callback); - success = true; - } catch (err) { - // @todo: It'd be interesting to make the errorMessage a callback and - // pass it the error when using the errorMessage below. - } - - let clusterAction: ClusterAction; - - if (success) { - clusterAction = { - id: actionKey, - url: successUrl, - dismissSnackbar: actionKey, - message: successMessage, - snackbarProps: successOptions, - }; - } else { - clusterAction = { - id: actionKey, - url: errorUrl, - dismissSnackbar: actionKey, - message: errorMessage, - snackbarProps: errorOptions, - }; - } - - yield put(updateClusterAction(clusterAction)); - } - - // Reset state if no other deletion happens - const { timeout } = yield race({ - newAction: take(CLUSTER_ACTION), - timeout: delay(3000), - }); - - // Reset the cluster action - if (timeout) { - yield put( - updateClusterAction({ - id: actionKey, - }) - ); - } - } -} - -export default function* rootSaga() { - yield all([watchClusterAction()]); -} diff --git a/frontend/src/redux/stores/store.tsx b/frontend/src/redux/stores/store.tsx index c063a98b2ce..7d28cf3b825 100644 --- a/frontend/src/redux/stores/store.tsx +++ b/frontend/src/redux/stores/store.tsx @@ -1,32 +1,26 @@ import { configureStore } from '@reduxjs/toolkit'; -import createSagaMiddleware from 'redux-saga'; +import { initialState as CLUSTER_ACTIONS_INITIAL_STATE } from '../clusterActionSlice'; import { initialState as CONFIG_INITIAL_STATE } from '../configSlice'; import { initialState as FILTER_INITIAL_STATE } from '../filterSlice'; import reducers from '../reducers/reducers'; import { INITIAL_STATE as UI_INITIAL_STATE } from '../reducers/ui'; -import rootSaga from '../sagas/sagas'; - -const initialState = { - filter: FILTER_INITIAL_STATE, - ui: UI_INITIAL_STATE, - config: CONFIG_INITIAL_STATE, -}; - -const sagaMiddleware = createSagaMiddleware(); const store = configureStore({ reducer: reducers, - preloadedState: initialState, + preloadedState: { + filter: FILTER_INITIAL_STATE, + ui: UI_INITIAL_STATE, + config: CONFIG_INITIAL_STATE, + clusterAction: CLUSTER_ACTIONS_INITIAL_STATE, + }, middleware: getDefaultMiddleware => getDefaultMiddleware({ serializableCheck: false, - thunk: false, - }).concat(sagaMiddleware), + thunk: true, + }), }); export default store; -sagaMiddleware.run(rootSaga); - export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType; diff --git a/plugins/headlamp-plugin/package-lock.json b/plugins/headlamp-plugin/package-lock.json index 57ac68bc7c5..1131ffebb79 100644 --- a/plugins/headlamp-plugin/package-lock.json +++ b/plugins/headlamp-plugin/package-lock.json @@ -51,6 +51,7 @@ "@types/react-redux": "^7.1.19", "@types/react-router-dom": "^5.3.1", "@types/react-window": "^1.8.5", + "@types/redux-mock-store": "^1.0.4", "@types/semver": "^7.3.8", "@types/webpack-env": "^1.16.2", "@typescript-eslint/eslint-plugin": "^4.33.0", @@ -72,6 +73,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-unused-imports": "^1.1.5", + "fetch-mock": "^9.11.0", "file-loader": "^6.2.0", "filemanager-webpack-plugin": "^7.0.0", "fs-extra": "^9.1.0", @@ -114,7 +116,7 @@ "react-scripts": "5.0.0", "react-window": "^1.8.7", "recharts": "^2.1.4", - "redux-saga": "^1.1.3", + "redux-mock-store": "^1.5.4", "resize-observer-polyfill": "^1.5.1", "semver": "^7.3.5", "shx": "^0.3.4", @@ -139,6 +141,11 @@ }, "bin": { "headlamp-plugin": "bin/headlamp-plugin.js" + }, + "devDependencies": { + "@types/redux-mock-store": "^1.0.4", + "fetch-mock": "^9.11.0", + "redux-mock-store": "^1.5.4" } }, "node_modules/@adobe/css-tools": { @@ -5099,57 +5106,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@redux-saga/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.1.tgz", - "integrity": "sha512-ABCxsZy9DwmNoYNo54ZlfuTvh77RXx8ODKpxOHeWam2dOaLGQ7vAktpfOtqSeTdYrKEORtTeWnxkGJMmPOoukg==", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@redux-saga/deferred": "^1.2.1", - "@redux-saga/delay-p": "^1.2.1", - "@redux-saga/is": "^1.1.3", - "@redux-saga/symbols": "^1.1.3", - "@redux-saga/types": "^1.2.1", - "redux": "^4.0.4", - "typescript-tuple": "^2.2.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/redux-saga" - } - }, - "node_modules/@redux-saga/deferred": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", - "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" - }, - "node_modules/@redux-saga/delay-p": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", - "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", - "dependencies": { - "@redux-saga/symbols": "^1.1.3" - } - }, - "node_modules/@redux-saga/is": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", - "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", - "dependencies": { - "@redux-saga/symbols": "^1.1.3", - "@redux-saga/types": "^1.2.1" - } - }, - "node_modules/@redux-saga/symbols": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", - "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" - }, - "node_modules/@redux-saga/types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", - "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" - }, "node_modules/@reduxjs/toolkit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -13985,6 +13941,15 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, + "node_modules/@types/redux-mock-store": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.4.tgz", + "integrity": "sha512-53nDnXba4M7aOJsRod8HKENDC9M2ccm19yZcXImoP15oDLuBru+Q+WKWOCQwKYOC1S/6AJx58mFp8kd4s8q1rQ==", + "dev": true, + "dependencies": { + "redux": "^4.0.5" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -21065,6 +21030,82 @@ "bser": "2.1.1" } }, + "node_modules/fetch-mock": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "engines": { + "node": ">=4.0.0" + }, + "funding": { + "type": "charity", + "url": "https://www.justgiving.com/refugee-support-europe" + }, + "peerDependencies": { + "node-fetch": "*" + }, + "peerDependenciesMeta": { + "node-fetch": { + "optional": true + } + } + }, + "node_modules/fetch-mock/node_modules/core-js": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.0.tgz", + "integrity": "sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/fetch-mock/node_modules/path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/fetch-mock/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/fetch-mock/node_modules/whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -24244,6 +24285,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -28974,6 +29021,12 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -35654,12 +35707,13 @@ "@babel/runtime": "^7.9.2" } }, - "node_modules/redux-saga": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.1.tgz", - "integrity": "sha512-fVCicLlf4hLP+KB6H7RHfZlZ8LdYckhaemXBB3wh//a2ESyz/z/l8ygxlm0OqPjS/PARdsQ2hIdAltxEB+NgvA==", + "node_modules/redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, "dependencies": { - "@redux-saga/core": "^1.2.1" + "lodash.isplainobject": "^4.0.6" } }, "node_modules/redux-thunk": { @@ -39337,27 +39391,6 @@ "node": ">=4.2.0" } }, - "node_modules/typescript-compare": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", - "dependencies": { - "typescript-logic": "^0.0.0" - } - }, - "node_modules/typescript-logic": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" - }, - "node_modules/typescript-tuple": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", - "dependencies": { - "typescript-compare": "^0.0.2" - } - }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -45214,53 +45247,6 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" }, - "@redux-saga/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.1.tgz", - "integrity": "sha512-ABCxsZy9DwmNoYNo54ZlfuTvh77RXx8ODKpxOHeWam2dOaLGQ7vAktpfOtqSeTdYrKEORtTeWnxkGJMmPOoukg==", - "requires": { - "@babel/runtime": "^7.6.3", - "@redux-saga/deferred": "^1.2.1", - "@redux-saga/delay-p": "^1.2.1", - "@redux-saga/is": "^1.1.3", - "@redux-saga/symbols": "^1.1.3", - "@redux-saga/types": "^1.2.1", - "redux": "^4.0.4", - "typescript-tuple": "^2.2.1" - } - }, - "@redux-saga/deferred": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", - "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" - }, - "@redux-saga/delay-p": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", - "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", - "requires": { - "@redux-saga/symbols": "^1.1.3" - } - }, - "@redux-saga/is": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", - "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", - "requires": { - "@redux-saga/symbols": "^1.1.3", - "@redux-saga/types": "^1.2.1" - } - }, - "@redux-saga/symbols": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", - "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" - }, - "@redux-saga/types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", - "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" - }, "@reduxjs/toolkit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -51918,6 +51904,15 @@ "@types/react": "*" } }, + "@types/redux-mock-store": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.4.tgz", + "integrity": "sha512-53nDnXba4M7aOJsRod8HKENDC9M2ccm19yZcXImoP15oDLuBru+Q+WKWOCQwKYOC1S/6AJx58mFp8kd4s8q1rQ==", + "dev": true, + "requires": { + "redux": "^4.0.5" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -57370,6 +57365,64 @@ "bser": "2.1.1" } }, + "fetch-mock": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", + "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", + "dev": true, + "requires": { + "@babel/core": "^7.0.0", + "@babel/runtime": "^7.0.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "dependencies": { + "core-js": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.0.tgz", + "integrity": "sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==", + "dev": true + }, + "path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -59765,6 +59818,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, "is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -63328,6 +63387,12 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -68008,12 +68073,13 @@ "@babel/runtime": "^7.9.2" } }, - "redux-saga": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.1.tgz", - "integrity": "sha512-fVCicLlf4hLP+KB6H7RHfZlZ8LdYckhaemXBB3wh//a2ESyz/z/l8ygxlm0OqPjS/PARdsQ2hIdAltxEB+NgvA==", + "redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, "requires": { - "@redux-saga/core": "^1.2.1" + "lodash.isplainobject": "^4.0.6" } }, "redux-thunk": { @@ -70847,27 +70913,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==" }, - "typescript-compare": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", - "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", - "requires": { - "typescript-logic": "^0.0.0" - } - }, - "typescript-logic": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" - }, - "typescript-tuple": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", - "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", - "requires": { - "typescript-compare": "^0.0.2" - } - }, "uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", diff --git a/plugins/headlamp-plugin/package.json b/plugins/headlamp-plugin/package.json index 96eaa103fad..52ea9c5d3df 100644 --- a/plugins/headlamp-plugin/package.json +++ b/plugins/headlamp-plugin/package.json @@ -55,6 +55,7 @@ "@types/react-redux": "^7.1.19", "@types/react-router-dom": "^5.3.1", "@types/react-window": "^1.8.5", + "@types/redux-mock-store": "^1.0.4", "@types/semver": "^7.3.8", "@types/webpack-env": "^1.16.2", "@typescript-eslint/eslint-plugin": "^4.33.0", @@ -76,6 +77,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-unused-imports": "^1.1.5", + "fetch-mock": "^9.11.0", "file-loader": "^6.2.0", "filemanager-webpack-plugin": "^7.0.0", "fs-extra": "^9.1.0", @@ -118,7 +120,7 @@ "react-scripts": "5.0.0", "react-window": "^1.8.7", "recharts": "^2.1.4", - "redux-saga": "^1.1.3", + "redux-mock-store": "^1.5.4", "resize-observer-polyfill": "^1.5.1", "semver": "^7.3.5", "shx": "^0.3.4", @@ -158,5 +160,10 @@ "plugins" ], "author": "Kinvolk GmbH", - "license": "Apache 2.0" + "license": "Apache 2.0", + "devDependencies": { + "@types/redux-mock-store": "^1.0.4", + "fetch-mock": "^9.11.0", + "redux-mock-store": "^1.5.4" + } }