From f9a574a5ed6223d987db5372a07df53f0da498cf Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 26 May 2024 19:20:11 +0300 Subject: [PATCH 01/11] chore: first part of migration to typescript --- web/package-lock.json | 476 +++++++----------- web/package.json | 10 +- web/src/App.js | 66 --- web/src/App.tsx | 74 +++ web/src/Layout.js | 41 -- web/src/Layout.tsx | 55 ++ web/src/{Page404.js => Page404.tsx} | 4 +- web/src/Utils.test.ts | 28 ++ web/src/{Utils.js => Utils.ts} | 8 +- web/src/auth.js | 61 --- web/src/auth.ts | 73 +++ web/src/config.js | 9 - web/src/config.ts | 24 + ...oyLockHandler.js => deployLockHandler.tsx} | 47 +- web/src/index.js | 18 - web/src/index.tsx | 24 + web/tsconfig.json | 21 + 17 files changed, 523 insertions(+), 516 deletions(-) delete mode 100644 web/src/App.js create mode 100644 web/src/App.tsx delete mode 100644 web/src/Layout.js create mode 100644 web/src/Layout.tsx rename web/src/{Page404.js => Page404.tsx} (90%) create mode 100644 web/src/Utils.test.ts rename web/src/{Utils.js => Utils.ts} (82%) delete mode 100644 web/src/auth.js create mode 100644 web/src/auth.ts delete mode 100644 web/src/config.js create mode 100644 web/src/config.ts rename web/src/{deployLockHandler.js => deployLockHandler.tsx} (58%) delete mode 100644 web/src/index.js create mode 100644 web/src/index.tsx create mode 100644 web/tsconfig.json diff --git a/web/package-lock.json b/web/package-lock.json index 42a0f250..4edc409e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -30,6 +30,12 @@ "react-scripts": "^5.0.1", "socket.io-client": "^4.7.5", "web-vitals": "^3.5.2" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "jest": "^29.7.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2851,8 +2857,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -2869,8 +2874,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -2917,8 +2921,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -2944,8 +2947,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -2957,15 +2959,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/@jest/core/node_modules/jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -2990,8 +2990,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -3000,8 +2999,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -3021,8 +3019,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -3039,8 +3036,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -3055,8 +3051,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -3070,8 +3065,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -3080,8 +3074,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3096,8 +3089,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -3110,8 +3102,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -3126,8 +3117,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -3140,8 +3130,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "jest-get-type": "^29.6.3" }, @@ -3153,8 +3142,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -3171,8 +3159,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3187,8 +3174,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -3231,8 +3217,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -3258,15 +3243,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -3282,8 +3265,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -3308,8 +3290,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -3318,8 +3299,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -3334,8 +3314,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3350,8 +3329,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -3364,8 +3342,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -3377,8 +3354,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -3392,8 +3368,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -3408,8 +3383,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -3424,8 +3398,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -3450,8 +3423,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -3460,8 +3432,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -3476,8 +3447,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3564,8 +3534,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -4146,15 +4115,13 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "type-detect": "4.0.8" } @@ -4163,8 +4130,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -4684,6 +4650,42 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "devOptional": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "devOptional": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4746,19 +4748,18 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/react": { - "version": "17.0.80", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.80.tgz", - "integrity": "sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA==", + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "^0.16", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.2.23", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.23.tgz", - "integrity": "sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dependencies": { "@types/react": "*" } @@ -4784,11 +4785,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -4851,8 +4847,7 @@ "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/yargs-parser": "*" } @@ -6410,8 +6405,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6740,8 +6734,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -7283,8 +7276,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", - "optional": true, - "peer": true, + "devOptional": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -7473,8 +7465,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -7670,8 +7661,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=12" }, @@ -8649,8 +8639,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -10517,8 +10506,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10544,8 +10532,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -10559,8 +10546,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -10591,8 +10577,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -10604,8 +10589,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -10619,8 +10603,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -10653,8 +10636,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -10666,8 +10648,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -10684,8 +10665,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -10699,8 +10679,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -10745,8 +10724,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -10772,8 +10750,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -10785,8 +10762,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -10807,8 +10783,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -10823,8 +10798,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -10840,15 +10814,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/jest-config/node_modules/jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -10873,8 +10845,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -10883,8 +10854,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -10904,8 +10874,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -10922,8 +10891,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -10938,8 +10906,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -10953,8 +10920,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -10963,8 +10929,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10979,8 +10944,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -10993,8 +10957,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -11009,8 +10972,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -11022,8 +10984,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11037,8 +10998,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "detect-newline": "^3.0.0" }, @@ -11050,8 +11010,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -11067,8 +11026,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -11080,8 +11038,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11228,8 +11185,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -11246,8 +11202,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -11664,8 +11619,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -11678,8 +11632,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -11691,8 +11644,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11706,8 +11658,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -11722,8 +11673,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -11735,8 +11685,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11750,8 +11699,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -11771,8 +11719,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -11784,8 +11731,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11799,8 +11745,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -11858,8 +11803,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -11872,8 +11816,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -11921,8 +11864,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -11954,8 +11896,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -11981,8 +11922,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -11994,15 +11934,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/jest-runner/node_modules/jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -12027,8 +11965,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -12037,8 +11974,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -12058,8 +11994,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -12076,8 +12011,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -12092,8 +12026,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -12107,8 +12040,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -12117,8 +12049,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -12127,8 +12058,7 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -12138,8 +12068,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12154,8 +12083,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -12168,8 +12096,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -12202,8 +12129,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -12229,8 +12155,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -12242,15 +12167,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/jest-runtime/node_modules/jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -12275,8 +12198,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -12285,8 +12207,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -12306,8 +12227,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -12324,8 +12244,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -12340,8 +12259,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -12355,8 +12273,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -12365,8 +12282,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12381,8 +12297,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -12407,8 +12322,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -12439,8 +12353,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -12466,8 +12379,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -12479,15 +12391,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/jest-snapshot/node_modules/jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -12512,8 +12422,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -12522,8 +12431,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -12538,8 +12446,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -12553,8 +12460,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12569,8 +12475,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -12583,8 +12488,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -12648,8 +12552,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -15425,6 +15328,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, "funding": [ { "type": "individual", @@ -15434,9 +15338,7 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ], - "optional": true, - "peer": true + ] }, "node_modules/q": { "version": "1.5.1", @@ -19078,8 +18980,7 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -19093,8 +18994,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/vary": { "version": "1.1.2", @@ -20040,8 +19940,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -20059,8 +19958,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=12" } diff --git a/web/package.json b/web/package.json index 622456f2..6482a18d 100644 --- a/web/package.json +++ b/web/package.json @@ -31,7 +31,7 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "format": "prettier --write \"src/**/*.js\"" + "format": "prettier --write \"src/**/*.{js,ts,tsx}\"" }, "eslintConfig": { "extends": [ @@ -54,5 +54,11 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:8080" + "proxy": "http://localhost:8080", + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "jest": "^29.7.0" + } } diff --git a/web/src/App.js b/web/src/App.js deleted file mode 100644 index a261d88c..00000000 --- a/web/src/App.js +++ /dev/null @@ -1,66 +0,0 @@ -import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import { Box, CircularProgress, createTheme, lighten, ThemeProvider } from '@mui/material'; - -import RecentTasks from './Components/RecentTasks'; -import HistoryTasks from './Components/HistoryTasks'; -import Layout from './Layout'; -import Page404 from './Page404'; -import { ErrorProvider } from './ErrorContext'; -import TaskView from './Components/TaskView'; -import { AuthContext, useAuth } from './auth'; -import { DeployLockProvider } from './deployLockHandler'; - -const theme = createTheme({ - palette: { - primary: { - main: '#2E3B55', - }, - neutral: { - main: 'gray', - }, - reason_color: { - main: lighten('#ff9800', 0.5), - }, - }, - components: { - MuiTableCell: { - styleOverrides: { - root: { - padding: '12px', - }, - }, - }, - }, -}); - -function App() { - const auth = useAuth(); - - if (auth.authenticated === null) return ( - - - - ); - else if (auth.authenticated) return ( - - - - - - - }> - } /> - } /> - } /> - - } /> - - - - - - - ); -} - -export default App; diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 00000000..f892d7fe --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { Box, CircularProgress, createTheme, lighten, ThemeOptions, ThemeProvider } from '@mui/material'; + +import RecentTasks from './Components/RecentTasks'; +import HistoryTasks from './Components/HistoryTasks'; +import Layout from './Layout'; +import Page404 from './Page404'; +import { ErrorProvider } from './ErrorContext'; +import TaskView from './Components/TaskView'; +import { AuthContext, useAuth } from './auth'; +import { DeployLockProvider } from './deployLockHandler'; + +const theme = createTheme({ + palette: { + primary: { + main: '#2E3B55', + }, + neutral: { + main: 'gray', + }, + reason_color: { + main: lighten('#ff9800', 0.5), + }, + }, + components: { + MuiTableCell: { + styleOverrides: { + root: { + padding: '12px', + }, + }, + }, + }, +} as ThemeOptions); + +const App: React.FC = () => { + const auth = useAuth(); + + if (auth.authenticated === null) { + return ( + + + + ); + } + + if (auth.authenticated) { + return ( + + + + + + + }> + } /> + } /> + } /> + + } /> + + + + + + + ); + } + + return null; +}; + +export default App; diff --git a/web/src/Layout.js b/web/src/Layout.js deleted file mode 100644 index 26fdb586..00000000 --- a/web/src/Layout.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; -import MuiAlert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; - -import Navbar from './Components/Navbar'; -import { useErrorContext } from './ErrorContext'; - -const Alert = React.forwardRef(function Alert(props, ref) { - return ; -}); - -function Layout() { - const { messages, clearMessage } = useErrorContext(); - - return ( - <> - - - {messages.length > 0 && - messages.map(message => { - return ( - { - clearMessage(message.status, message.message); - }} - severity={message.status} - sx={{ width: '100%', borderRadius: 0 }} - key={`${message.status} ${message.message}`} - > - {message.message} - - ); - })} - - - - ); -} - -export default Layout; diff --git a/web/src/Layout.tsx b/web/src/Layout.tsx new file mode 100644 index 00000000..e965401b --- /dev/null +++ b/web/src/Layout.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { Outlet } from "react-router-dom"; +import MuiAlert, { AlertProps } from "@mui/material/Alert"; +import Box from "@mui/material/Box"; + +import Navbar from "./Components/Navbar"; +import { useErrorContext } from "./ErrorContext"; + +interface Message { + status: "success" | "error" | "warning" | "info"; + message: string; +} + +interface ErrorContextState { + messages: Message[]; + clearMessage: (status: string, message: string) => void; +} + +const Alert = React.forwardRef( + (props, ref) => { + return ; + } +); + +const Layout: React.FC = () => { + const errorContext = useErrorContext(); + const { messages, clearMessage }: ErrorContextState = errorContext || { + messages: [], + clearMessage: () => {}, + }; + + return ( + <> + + + {messages.length > 0 && + messages.map(message => ( + { + clearMessage(message.status, message.message); + }} + severity={message.status} + sx={{ width: "100%", borderRadius: 0 }} + key={`${message.status} ${message.message}`} + > + {message.message} + + ))} + + + + ); +}; + +export default Layout; diff --git a/web/src/Page404.js b/web/src/Page404.tsx similarity index 90% rename from web/src/Page404.js rename to web/src/Page404.tsx index 5e42ad23..be8c1539 100644 --- a/web/src/Page404.js +++ b/web/src/Page404.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import './Page404.css'; -function Page404() { +const Page404: FunctionComponent = () => { return ( { + it('should return "< 1 minute" when seconds are less than 60', () => { + expect(relativeHumanDuration(59)).toBe('< 1 minute'); + }); + + it('should return number of minutes when seconds are between 60 and 3600', () => { + expect(relativeHumanDuration(600)).toBe('10 minutes'); + }); + + it('should return number of hours when seconds are between 3600 and 86400', () => { + expect(relativeHumanDuration(7200)).toBe('2 hours'); + }); + + it('should return number of days when seconds are between 86400 and 2620800', () => { + expect(relativeHumanDuration(172800)).toBe('2 days'); + }); + + it('should return number of months when seconds are between 2620800 and 31449600', () => { + expect(relativeHumanDuration(5241600)).toBe('2 months'); + }); + + it('should return number of years when seconds are more than 31449600', () => { + expect(relativeHumanDuration(62899200)).toBe('2 years'); + }); +}); diff --git a/web/src/Utils.js b/web/src/Utils.ts similarity index 82% rename from web/src/Utils.js rename to web/src/Utils.ts index 1dea4946..d31b1288 100644 --- a/web/src/Utils.js +++ b/web/src/Utils.ts @@ -1,4 +1,4 @@ -export const relativeTime = oldTimestamp => { +export const relativeTime = (oldTimestamp: number): string => { const timestamp = Date.now(); const difference = Math.round(timestamp / 1000 - oldTimestamp / 1000); if (oldTimestamp === 0) { @@ -7,8 +7,8 @@ export const relativeTime = oldTimestamp => { return relativeHumanDuration(difference) + ' ago'; }; -export const relativeHumanDuration = seconds => { - function numberEnding(number) { +export const relativeHumanDuration = (seconds: number): string => { + function numberEnding(number: number): string { return number > 1 ? 's' : ''; } @@ -37,6 +37,6 @@ export const relativeHumanDuration = seconds => { return `${Math.floor(seconds / 31449600)} years`; }; -export const relativeTimestamp = timeframe => { +export const relativeTimestamp = (timeframe: number): number => { return Math.floor(Date.now() / 1000) - timeframe; }; diff --git a/web/src/auth.js b/web/src/auth.js deleted file mode 100644 index 1b6dbb30..00000000 --- a/web/src/auth.js +++ /dev/null @@ -1,61 +0,0 @@ -import { createContext, useEffect, useState } from 'react'; -import Keycloak from 'keycloak-js'; - -import { fetchConfig } from './config'; - -export const AuthContext = createContext(undefined); - -export function useAuth() { - const [authenticated, setAuthenticated] = useState(null); - const [email, setEmail] = useState(null); - const [groups, setGroups] = useState([]); - const [privilegedGroups, setPrivilegedGroups] = useState([]); - const [keycloakToken, setKeycloakToken] = useState(null); - - const refreshToken = (keycloak, config) => { - setInterval(() => { - keycloak.updateToken(20) - .then(refreshed => { - if (refreshed) { - console.log('Token refreshed, valid for ' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds'); - setKeycloakToken(keycloak.token); - } - }).catch(() => { - console.error('Failed to refresh token'); - }); - }, config.keycloak.token_validation_interval); - }; - - useEffect(() => { - fetchConfig().then(config => { - if (config.keycloak.enabled) { - const keycloak = new Keycloak({ - url: config.keycloak.url, - realm: config.keycloak.realm, - clientId: config.keycloak.client_id, - }); - - keycloak.init({ onLoad: 'login-required' }) - .then(authenticated => { - setAuthenticated(authenticated); - if (authenticated) { - setEmail(keycloak.tokenParsed.email); - setGroups(keycloak.tokenParsed.groups); - setPrivilegedGroups(config.keycloak.privileged_groups); - setKeycloakToken(keycloak.token); - refreshToken(keycloak, config); - } - }) - .catch(() => { - setAuthenticated(false); - }); - } else { - // if keycloak_url is not set, we are not using any authentication - // hence we set authenticated to true by default - setAuthenticated(true); - } - }); - }, []); - - return { authenticated, email, groups, privilegedGroups, keycloakToken }; -} diff --git a/web/src/auth.ts b/web/src/auth.ts new file mode 100644 index 00000000..6d18485d --- /dev/null +++ b/web/src/auth.ts @@ -0,0 +1,73 @@ +import { createContext, useEffect, useState } from 'react'; +import Keycloak from 'keycloak-js'; +import { fetchConfig } from './config'; + +type AuthContextType = { + authenticated: null | boolean; + email: null | string; + groups: string[]; + privilegedGroups: string[]; + keycloakToken: null | string; +}; + +export const AuthContext = createContext(undefined); + +export function useAuth(): AuthContextType { + const [authenticated, setAuthenticated] = useState(null); + const [email, setEmail] = useState(null); + const [groups, setGroups] = useState([]); + const [privilegedGroups, setPrivilegedGroups] = useState([]); + const [keycloakToken, setKeycloakToken] = useState(null); + + const refreshToken = ( + keycloak: any, + config: { keycloak: { token_validation_interval: number } }, + ) => { + setInterval(() => { + keycloak.updateToken(20) + .then((refreshed: boolean) => { + if (refreshed) { + console.log('Token refreshed, valid for ' + + (keycloak.tokenParsed?.exp && keycloak.timeSkew + ? Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + : 'Unknown') + + ' seconds'); + setKeycloakToken(keycloak.token || null); + } + }).catch(() => { + console.error('Failed to refresh token'); + }); + }, config.keycloak.token_validation_interval); + }; + + useEffect(() => { + fetchConfig().then(config => { + if (config.keycloak.enabled) { + const keycloak = new Keycloak({ + url: config.keycloak.url, + realm: config.keycloak.realm, + clientId: config.keycloak.client_id, + }); + + keycloak.init({ onLoad: 'login-required' }) + .then(authenticated => { + setAuthenticated(authenticated); + if (authenticated) { + setEmail(keycloak.tokenParsed?.email || null); + setGroups(keycloak.tokenParsed?.groups || []); + setPrivilegedGroups(config.keycloak.privileged_groups || []); + setKeycloakToken(keycloak.token || null); + refreshToken(keycloak, config); + } + }) + .catch(() => { + setAuthenticated(false); + }); + } else { + setAuthenticated(true); + } + }); + }, []); + + return { authenticated, email, groups, privilegedGroups, keycloakToken }; +} diff --git a/web/src/config.js b/web/src/config.js deleted file mode 100644 index c92c296d..00000000 --- a/web/src/config.js +++ /dev/null @@ -1,9 +0,0 @@ -let config = null; - -export async function fetchConfig() { - if (!config) { - const response = await fetch('/api/v1/config'); - config = await response.json(); - } - return config; -} diff --git a/web/src/config.ts b/web/src/config.ts new file mode 100644 index 00000000..170c7707 --- /dev/null +++ b/web/src/config.ts @@ -0,0 +1,24 @@ +type KeycloakConfig = { + token_validation_interval: number; + enabled: boolean; + url: string; + realm: string; + client_id: string; + privileged_groups: string[]; +}; + +type ConfigType = { + keycloak: KeycloakConfig, + [key: string]: any +}; + +let config: ConfigType | null = null; + +export async function fetchConfig(): Promise { + if (!config) { + const response = await fetch('/api/v1/config'); + const data = await response.json(); + config = data as ConfigType; + } + return config as ConfigType; +} diff --git a/web/src/deployLockHandler.js b/web/src/deployLockHandler.tsx similarity index 58% rename from web/src/deployLockHandler.js rename to web/src/deployLockHandler.tsx index c6a6e8c0..1580b6a2 100644 --- a/web/src/deployLockHandler.js +++ b/web/src/deployLockHandler.tsx @@ -1,19 +1,16 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; -export async function fetchDeployLock() { +export async function fetchDeployLock(): Promise { const response = await fetch('/api/v1/deploy-lock'); return await response.json(); } -export async function releaseDeployLock(keycloakToken) { - let headers = { +export async function releaseDeployLock(keycloakToken: string | null): Promise { + let headers: Record = { 'Content-Type': 'application/json', }; if (keycloakToken !== null) { - // We replaced Authorization with Keycloak-Authorization here - // to simplify JWT implementation and avoid header name overlap headers['Keycloak-Authorization'] = keycloakToken; } @@ -27,14 +24,12 @@ export async function releaseDeployLock(keycloakToken) { } } -export async function setDeployLock(keycloakToken = null) { - let headers = { +export async function setDeployLock(keycloakToken: string | null = null): Promise { + let headers: Record = { 'Content-Type': 'application/json', }; if (keycloakToken !== null) { - // We replaced Authorization with Keycloak-Authorization here - // to simplify JWT implementation and avoid header name overlap headers['Keycloak-Authorization'] = keycloakToken; } @@ -48,11 +43,15 @@ export async function setDeployLock(keycloakToken = null) { } } -export const DeployLockContext = createContext(false); +export const DeployLockContext = createContext(false); -export function DeployLockProvider({ children }) { - const [deployLock, setDeployLock] = useState(false); - const [socket, setSocket] = useState(null); +interface DeployLockProviderProps { + children: ReactNode; +} + +export function DeployLockProvider({ children }: DeployLockProviderProps): JSX.Element { + const [deployLock, setDeployLockState] = useState(false); + const [socket, setSocket] = useState(null); useEffect(() => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; @@ -70,14 +69,14 @@ export function DeployLockProvider({ children }) { if (socket) { socket.onopen = async () => { const lock = await fetchDeployLock(); - setDeployLock(lock); + setDeployLockState(lock); }; socket.onmessage = (event) => { const message = event.data; if (message === 'locked') { - setDeployLock(true); + setDeployLockState(true); } else if (message === 'unlocked') { - setDeployLock(false); + setDeployLockState(false); } }; } @@ -90,10 +89,10 @@ export function DeployLockProvider({ children }) { ); } -DeployLockProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -export function useDeployLock() { - return useContext(DeployLockContext); +export function useDeployLock(): boolean { + const context = useContext(DeployLockContext); + if (context === undefined) { + throw new Error('useDeployLock must be used within a DeployLockProvider'); + } + return context; } diff --git a/web/src/index.js b/web/src/index.js deleted file mode 100644 index e11a4c75..00000000 --- a/web/src/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import CssBaseline from '@mui/material/CssBaseline'; -import '@fontsource/roboto/300.css'; -import '@fontsource/roboto/400.css'; -import '@fontsource/roboto/500.css'; -import '@fontsource/roboto/700.css'; - -import './index.css'; -import App from './App'; - -const root = document.getElementById('root'); -createRoot(root).render( - - - - -); diff --git a/web/src/index.tsx b/web/src/index.tsx new file mode 100644 index 00000000..44300df4 --- /dev/null +++ b/web/src/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import CssBaseline from '@mui/material/CssBaseline'; +import '@fontsource/roboto/300.css'; +import '@fontsource/roboto/400.css'; +import '@fontsource/roboto/500.css'; +import '@fontsource/roboto/700.css'; + +import './index.css'; +import App from './App'; + +const rootElement = document.getElementById('root'); + +if (rootElement) { + const root = createRoot(rootElement); + root.render( + + + + , + ); +} else { + console.error('Failed to find the root element. Ensure that your HTML file has a div with id="root".'); +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 00000000..12ad8644 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} From fb907de3094472497094aaad279fcfdae403e9f0 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 26 May 2024 23:38:41 +0300 Subject: [PATCH 02/11] chore: cleanup and small restructure --- web/src/App.tsx | 4 +- web/src/Components/Sidebar.js | 6 +- web/src/Components/TaskView.js | 6 +- web/src/Components/TasksTable.js | 2 +- web/src/{auth.ts => Services/Auth.ts} | 2 +- web/src/Services/Data.js | 53 --------- web/src/Services/Data.ts | 109 ++++++++++++++++++ .../DeployLockHandler.tsx} | 0 web/src/config.ts | 24 ---- 9 files changed, 119 insertions(+), 87 deletions(-) rename web/src/{auth.ts => Services/Auth.ts} (98%) delete mode 100644 web/src/Services/Data.js create mode 100644 web/src/Services/Data.ts rename web/src/{deployLockHandler.tsx => Services/DeployLockHandler.tsx} (100%) delete mode 100644 web/src/config.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index f892d7fe..3184b6ae 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,8 +8,8 @@ import Layout from './Layout'; import Page404 from './Page404'; import { ErrorProvider } from './ErrorContext'; import TaskView from './Components/TaskView'; -import { AuthContext, useAuth } from './auth'; -import { DeployLockProvider } from './deployLockHandler'; +import { AuthContext, useAuth } from './Services/Auth'; +import { DeployLockProvider } from './Services/DeployLockHandler'; const theme = createTheme({ palette: { diff --git a/web/src/Components/Sidebar.js b/web/src/Components/Sidebar.js index d8486888..260489df 100644 --- a/web/src/Components/Sidebar.js +++ b/web/src/Components/Sidebar.js @@ -16,9 +16,9 @@ import { } from '@mui/material'; import Switch from '@mui/material/Switch'; -import { fetchConfig } from '../config'; -import { releaseDeployLock, setDeployLock, useDeployLock } from '../deployLockHandler'; -import { AuthContext } from '../auth'; +import { fetchConfig } from '../Services/Data'; +import { releaseDeployLock, setDeployLock, useDeployLock } from '../Services/DeployLockHandler'; +import { AuthContext } from '../Services/Auth'; function Sidebar({ open, onClose }) { const [configData, setConfigData] = useState(null); diff --git a/web/src/Components/TaskView.js b/web/src/Components/TaskView.js index 4e48d781..1a75f0bf 100644 --- a/web/src/Components/TaskView.js +++ b/web/src/Components/TaskView.js @@ -23,9 +23,9 @@ import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import { fetchTask } from '../Services/Data'; import { useErrorContext } from '../ErrorContext'; import { formatDateTime, ProjectDisplay, StatusReasonDisplay } from './TasksTable'; -import { AuthContext } from '../auth'; -import { fetchConfig } from '../config'; -import { useDeployLock } from '../deployLockHandler'; +import { AuthContext } from '../Services/Auth'; +import { fetchConfig } from '../Services/Data'; +import { useDeployLock } from '../Services/DeployLockHandler'; export default function TaskView() { const { id } = useParams(); diff --git a/web/src/Components/TasksTable.js b/web/src/Components/TasksTable.js index 41811055..c75482a5 100644 --- a/web/src/Components/TasksTable.js +++ b/web/src/Components/TasksTable.js @@ -22,7 +22,7 @@ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import LaunchIcon from '@mui/icons-material/Launch'; import { fetchTasks } from '../Services/Data'; -import { useDeployLock } from '../deployLockHandler'; +import { useDeployLock } from '../Services/DeployLockHandler'; import { relativeHumanDuration, relativeTime, relativeTimestamp } from '../Utils'; export function ProjectDisplay({ project }) { diff --git a/web/src/auth.ts b/web/src/Services/Auth.ts similarity index 98% rename from web/src/auth.ts rename to web/src/Services/Auth.ts index 6d18485d..3c415563 100644 --- a/web/src/auth.ts +++ b/web/src/Services/Auth.ts @@ -1,6 +1,6 @@ import { createContext, useEffect, useState } from 'react'; import Keycloak from 'keycloak-js'; -import { fetchConfig } from './config'; +import { fetchConfig } from './Data'; type AuthContextType = { authenticated: null | boolean; diff --git a/web/src/Services/Data.js b/web/src/Services/Data.js deleted file mode 100644 index 725aeded..00000000 --- a/web/src/Services/Data.js +++ /dev/null @@ -1,53 +0,0 @@ -export function fetchTasks(fromTimestamp, toTimestamp, application = null) { - let searchParams = {}; - if (fromTimestamp) { - searchParams.from_timestamp = fromTimestamp; - } - if (toTimestamp) { - searchParams.to_timestamp = toTimestamp; - } - if (application) { - searchParams.app = application; - } - return fetch(`/api/v1/tasks?${new URLSearchParams(searchParams)}`) - .then(res => { - if (res.status !== 200) { - throw new Error(res.statusText); - } - return res.json(); - }) - .then(res => { - if (res?.error) { - throw new Error(res.error); - } - if (!res?.tasks) { - return []; - } - return res.tasks; - }); -} - -export function fetchTask(id) { - return fetch(`/api/v1/tasks/${id}`) - .then(res => { - if (res.status !== 200) { - throw new Error(res.statusText); - } - return res.json(); - }) - .then(res => { - if (res?.error) { - throw new Error(res.error); - } - return res; - }); -} - -export function fetchVersion() { - return fetch(`/api/v1/version`).then(res => { - if (res.status !== 200) { - throw new Error(res.statusText); - } - return res.json(); - }); -} diff --git a/web/src/Services/Data.ts b/web/src/Services/Data.ts new file mode 100644 index 00000000..3272d0f1 --- /dev/null +++ b/web/src/Services/Data.ts @@ -0,0 +1,109 @@ +type TaskResponse = { + error?: string; + tasks?: any[]; +}; + +type TaskDetailsResponse = { + error?: string; +} & Record; + +type KeycloakConfig = { + token_validation_interval: number; + enabled: boolean; + url: string; + realm: string; + client_id: string; + privileged_groups: string[]; +}; + +type ConfigType = { + keycloak: KeycloakConfig, + [key: string]: any +}; + +let config: ConfigType | null = null; + +/** + * Fetches tasks from the API endpoint. + * + * @param {number | null} fromTimestamp - The timestamp from which to fetch tasks. + * @param {number | null} toTimestamp - The timestamp up to which to fetch tasks. + * @param {string | null} application - The application for which to fetch tasks. + * @returns {Promise} - An array of tasks fetched from the API. + */ +export async function fetchTasks( + fromTimestamp: number | null, + toTimestamp: number | null, + application: string | null = null +): Promise { + let searchParams: { [index: string]: string } = {}; + if (fromTimestamp) { + searchParams.from_timestamp = fromTimestamp.toString(); + } + if (toTimestamp) { + searchParams.to_timestamp = toTimestamp.toString(); + } + if (application) { + searchParams.app = application; + } + + const res: Response = await fetch(`/api/v1/tasks?${new URLSearchParams(searchParams)}`); + if (res.status !== 200) { + throw new Error(res.statusText); + } + + const data: TaskResponse = await res.json(); + if (data?.error) { + throw new Error(data.error); + } + + return data?.tasks ?? []; +} + +/** + * Fetches a specific task from the API endpoint. + * + * @param {string | number} id - The unique identifier of the task to fetch. + * @returns {Promise<{}>} - A promise that resolves to the fetched task details. + */ +export async function fetchTask(id: string | number): Promise<{}> { + const res: Response = await fetch(`/api/v1/tasks/${id}`); + if (res.status !== 200) { + throw new Error(res.statusText); + } + + const data: TaskDetailsResponse = await res.json(); + if (data?.error) { + throw new Error(data.error); + } + + return data; +} + +/** + * Fetches the version information from the API endpoint. + * + * @returns {Promise} - A promise that resolves to the version information. + */ +export async function fetchVersion(): Promise { + const res: Response = await fetch(`/api/v1/version`); + if (res.status !== 200) { + throw new Error(res.statusText); + } + + return res.json(); +} + +/** + * Fetches the application configuration from the API endpoint. + * + * @returns {Promise} - A promise that resolves to the application configuration. + */ +export async function fetchConfig(): Promise { + if (!config) { + const response = await fetch('/api/v1/config'); + const data = await response.json(); + config = data as ConfigType; + } + return config as ConfigType; +} diff --git a/web/src/deployLockHandler.tsx b/web/src/Services/DeployLockHandler.tsx similarity index 100% rename from web/src/deployLockHandler.tsx rename to web/src/Services/DeployLockHandler.tsx diff --git a/web/src/config.ts b/web/src/config.ts deleted file mode 100644 index 170c7707..00000000 --- a/web/src/config.ts +++ /dev/null @@ -1,24 +0,0 @@ -type KeycloakConfig = { - token_validation_interval: number; - enabled: boolean; - url: string; - realm: string; - client_id: string; - privileged_groups: string[]; -}; - -type ConfigType = { - keycloak: KeycloakConfig, - [key: string]: any -}; - -let config: ConfigType | null = null; - -export async function fetchConfig(): Promise { - if (!config) { - const response = await fetch('/api/v1/config'); - const data = await response.json(); - config = data as ConfigType; - } - return config as ConfigType; -} From 1fbd97144f7a6489af1e38ae4f6e59f905dd901e Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 26 May 2024 23:56:29 +0300 Subject: [PATCH 03/11] chore: migrate sidebar to typescript --- .../Components/{Sidebar.js => Sidebar.tsx} | 115 ++++++++++-------- 1 file changed, 62 insertions(+), 53 deletions(-) rename web/src/Components/{Sidebar.js => Sidebar.tsx} (62%) diff --git a/web/src/Components/Sidebar.js b/web/src/Components/Sidebar.tsx similarity index 62% rename from web/src/Components/Sidebar.js rename to web/src/Components/Sidebar.tsx index 260489df..ff8d14e9 100644 --- a/web/src/Components/Sidebar.js +++ b/web/src/Components/Sidebar.tsx @@ -1,11 +1,11 @@ -import React, { useContext, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { useCallback, useEffect, useState } from 'react'; import { Box, Button, CircularProgress, Drawer, Paper, + Switch, Table, TableBody, TableCell, @@ -14,66 +14,82 @@ import { TableRow, Typography, } from '@mui/material'; -import Switch from '@mui/material/Switch'; - import { fetchConfig } from '../Services/Data'; import { releaseDeployLock, setDeployLock, useDeployLock } from '../Services/DeployLockHandler'; -import { AuthContext } from '../Services/Auth'; +import { useAuth } from '../Services/Auth'; + +interface ConfigData { + [key: string]: any; +} -function Sidebar({ open, onClose }) { - const [configData, setConfigData] = useState(null); +interface SidebarProps { + open: boolean; + onClose: () => void; +} + +const Sidebar: React.FC = ({ open, onClose }) => { + const [configData, setConfigData] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); - const { authenticated, keycloakToken } = useContext(AuthContext); + const { authenticated, keycloakToken } = useAuth(); const deployLock = useDeployLock(); - const toggleDeployLock = async () => { - if (deployLock) { - await releaseDeployLock(authenticated ? keycloakToken : null); - } else { - await setDeployLock(authenticated ? keycloakToken : null); + const toggleDeployLock = useCallback(async () => { + try { + if (deployLock) { + await releaseDeployLock(authenticated ? keycloakToken : null); + } else { + await setDeployLock(authenticated ? keycloakToken : null); + } + } catch (error) { + console.error('Failed to toggle deploy lock:', error); } - }; + }, [deployLock, authenticated, keycloakToken]); useEffect(() => { - fetchConfig() - .then(data => { + const loadConfig = async () => { + try { + const data = await fetchConfig(); setConfigData(data); + } catch (error) { + if (error instanceof Error) { + setError(error.message); + } else { + setError('An unknown error occurred'); + } + } finally { setIsLoading(false); - }) - .catch(error => { - setError(error.message); - setIsLoading(false); - }); + } + }; + + // Handle promise directly within useEffect + loadConfig(); }, []); - const handleCopy = () => { - navigator.clipboard.writeText(JSON.stringify(configData, null, 2)).catch(err => { + const handleCopy = useCallback(() => { + if (configData) { + navigator.clipboard.writeText(JSON.stringify(configData, null, 2)).catch(err => { console.error('Failed to copy config data to clipboard: ', err); }); - }; + } + }, [configData]); - const renderTableCell = (key, value) => { + const renderTableCell = useCallback((key: string, value: any) => { if (key === 'argo_cd_url' && value && typeof value === 'object' && value.constructor === Object) { if ('Scheme' in value && 'Host' in value && 'Path' in value) { return `${value.Scheme}://${value.Host}${value.Path}`; } else { return 'Invalid value'; } - } else if (value && typeof value === 'object' && value.constructor === Object) { - return ( - - {JSON.stringify(value, null, 2)} - - ); } + return ( - {value.toString()} + {typeof value === 'object' ? JSON.stringify(value, null, 2) : value.toString()} ); - }; + }, []); const renderContent = () => { if (isLoading) { @@ -83,9 +99,13 @@ function Sidebar({ open, onClose }) { Loading... ); - } else if (error) { + } + + if (error) { return {error}; - } else if (configData) { + } + + if (configData) { return ( <> @@ -102,9 +122,7 @@ function Sidebar({ open, onClose }) { {key} - - {renderTableCell(key, value)} - + {renderTableCell(key, value)} ))} @@ -117,9 +135,9 @@ function Sidebar({ open, onClose }) { ); - } else { - return No data available; } + + return No data available; }; return ( @@ -132,14 +150,10 @@ function Sidebar({ open, onClose }) { - + Lockdown Mode - + @@ -149,11 +163,6 @@ function Sidebar({ open, onClose }) { ); -} - -Sidebar.propTypes = { - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, }; -export default Sidebar; +export default React.memo(Sidebar); From 7ffe212b90be81dc64482cc58c114be606d2b014 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Mon, 27 May 2024 00:28:56 +0300 Subject: [PATCH 04/11] chore: migrate navbar and error context to typescript --- web/src/Components/{Navbar.js => Navbar.tsx} | 35 +++---- web/src/ErrorContext.js | 92 ------------------- web/src/ErrorContext.tsx | 96 ++++++++++++++++++++ web/src/Layout.tsx | 39 +++----- web/src/Services/Auth.ts | 19 ++++ web/src/Services/DeployLockHandler.tsx | 44 +++++++++ 6 files changed, 189 insertions(+), 136 deletions(-) rename web/src/Components/{Navbar.js => Navbar.tsx} (84%) delete mode 100644 web/src/ErrorContext.js create mode 100644 web/src/ErrorContext.tsx diff --git a/web/src/Components/Navbar.js b/web/src/Components/Navbar.tsx similarity index 84% rename from web/src/Components/Navbar.js rename to web/src/Components/Navbar.tsx index ccff14e7..c9fc1d45 100644 --- a/web/src/Components/Navbar.js +++ b/web/src/Components/Navbar.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import { Link as RouterLink } from 'react-router-dom'; import { AppBar, @@ -20,7 +19,13 @@ import { fetchVersion } from '../Services/Data'; import { useErrorContext } from '../ErrorContext'; import Sidebar from './Sidebar'; -function NavigationButton({ to, children, external = false }) { +interface NavigationButtonProps { + to: string; + children: React.ReactNode; + external?: boolean; +} + +const NavigationButton: React.FC = ({ to, children, external = false }) => { if (external) { return ( ); -} - -NavigationButton.propTypes = { - to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, - children: PropTypes.node.isRequired, - external: PropTypes.bool, }; -function Navbar() { +const Navbar: React.FC = () => { const [version, setVersion] = useState('0.0.0'); const [isSidebarOpen, setSidebarOpen] = useState(false); const { setSuccess, setError } = useErrorContext(); @@ -67,9 +66,13 @@ function Navbar() { setVersion(version); }) .catch(error => { - setError('fetchVersion', error.message); + if (error instanceof Error) { + setError('fetchVersion', error.message); + } else { + setError('fetchVersion', 'An unknown error occurred'); + } }); - }, []); + }, [setSuccess, setError]); return ( @@ -132,11 +135,11 @@ function Navbar() { external > - + - GitHub - - {version} + GitHub + + {version} @@ -147,6 +150,6 @@ function Navbar() { setSidebarOpen(false)} /> ); -} +}; export default Navbar; diff --git a/web/src/ErrorContext.js b/web/src/ErrorContext.js deleted file mode 100644 index 4ac27689..00000000 --- a/web/src/ErrorContext.js +++ /dev/null @@ -1,92 +0,0 @@ -import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import PropTypes from 'prop-types'; - -const context = createContext(null); - -export const ErrorProvider = ({ children }) => { - const [stack, setStack] = useState({}); - const timeouts = useRef([]); - - useEffect(() => { - return () => timeouts.current.forEach(timeout => clearTimeout(timeout)); - }, []); - - const removeStackItem = (id) => { - setStack(stack => { - delete stack[id]; - return { ...stack }; - }); - }; - - const setSuccessTimeout = (id) => { - timeouts.current.push( - setTimeout(() => removeStackItem(id), 5000) - ); - }; - - const clearMessage = (status, message) => { - setStack(stack => { - for (let id in stack) { - if (stack[id].status === status && stack[id].message === message) { - delete stack[id]; - } - } - return { ...stack }; - }); - }; - - const value = useMemo(() => ({ - stack, - messages: Object.keys(stack).reduce((result, key) => { - let item = stack[key]; - let found = false; - for (let searchItem of result) { - if ( - searchItem.message === item.message && - searchItem.status === item.status - ) { - found = true; - break; - } - } - if (!found) { - result.push(item); - } - return result; - }, []), - setError: (id, message) => { - if (!message) { - message = 'Unknown error'; - } - setStack(stack => { - stack[id] = { status: 'error', message }; - return { ...stack }; - }); - }, - setSuccess: (id, message) => { - setStack(stack => { - if (!stack[id]) { - return stack; - } - stack[id] = { status: 'success', message }; - setSuccessTimeout(id); - return { ...stack }; - }); - }, - clearMessage, - }), [stack]); - - return ( - - {children} - - ); -}; - -export const useErrorContext = () => { - return useContext(context); -}; - -ErrorProvider.propTypes = { - children: PropTypes.node.isRequired, -}; diff --git a/web/src/ErrorContext.tsx b/web/src/ErrorContext.tsx new file mode 100644 index 00000000..64781490 --- /dev/null +++ b/web/src/ErrorContext.tsx @@ -0,0 +1,96 @@ +import React, { createContext, useContext, useEffect, useMemo, useRef, useState, ReactNode } from 'react'; + +export interface ErrorItem { + status: 'error' | 'success'; + message: string; +} + +export interface ErrorContextType { + stack: Record; + messages: ErrorItem[]; + setError: (id: string, message?: string) => void; + setSuccess: (id: string, message: string) => void; + clearMessage: (status: 'error' | 'success', message: string) => void; +} + +export const ErrorContext = createContext(undefined); + +export const useErrorContext = (): ErrorContextType => { + const context = useContext(ErrorContext); + if (!context) { + throw new Error('useErrorContext must be used within an ErrorProvider'); + } + return context; +}; + +export const ErrorProvider: React.FC<{children: ReactNode}> = ({ children }) => { + const [stack, setStack] = useState>({}); + const timeouts = useRef([]); + + useEffect(() => { + return () => timeouts.current.forEach(timeout => clearTimeout(timeout)); + }, []); + + const removeStackItem = (id: string) => { + setStack(stack => { + const newStack = { ...stack }; + delete newStack[id]; + return newStack; + }); + }; + + const setSuccessTimeout = (id: string) => { + timeouts.current.push( + setTimeout(() => removeStackItem(id), 5000) + ); + }; + + const clearMessage = (status: 'error' | 'success', message: string) => { + setStack(stack => { + const newStack = { ...stack }; + for (const id in newStack) { + if (newStack[id].status === status && newStack[id].message === message) { + delete newStack[id]; + } + } + return newStack; + }); + }; + + const value = useMemo(() => ({ + stack, + messages: Object.keys(stack).reduce((result, key) => { + const item = stack[key]; + const found = result.some(searchItem => searchItem.message === item.message && searchItem.status === item.status); + if (!found) { + result.push(item); + } + return result; + }, [] as ErrorItem[]), + setError: (id: string, message: string = 'Unknown error') => { + setStack(stack => ({ + ...stack, + [id]: { status: 'error', message }, + })); + }, + setSuccess: (id: string, message: string) => { + setStack(stack => { + if (!stack[id]) { + return stack; + } + return { + ...stack, + [id]: { status: 'success', message }, + }; + }); + setSuccessTimeout(id); + }, + clearMessage, + }), [stack]); + + return ( + + {children} + + ); +}; diff --git a/web/src/Layout.tsx b/web/src/Layout.tsx index e965401b..d3ad96af 100644 --- a/web/src/Layout.tsx +++ b/web/src/Layout.tsx @@ -1,33 +1,16 @@ -import React from "react"; -import { Outlet } from "react-router-dom"; -import MuiAlert, { AlertProps } from "@mui/material/Alert"; -import Box from "@mui/material/Box"; +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Navbar from './Components/Navbar'; +import { useErrorContext, ErrorContextType } from './ErrorContext'; // `ErrorContextType` imported here -import Navbar from "./Components/Navbar"; -import { useErrorContext } from "./ErrorContext"; - -interface Message { - status: "success" | "error" | "warning" | "info"; - message: string; -} - -interface ErrorContextState { - messages: Message[]; - clearMessage: (status: string, message: string) => void; -} - -const Alert = React.forwardRef( - (props, ref) => { - return ; - } -); +const Alert = React.forwardRef((props, ref) => ( + +)); const Layout: React.FC = () => { - const errorContext = useErrorContext(); - const { messages, clearMessage }: ErrorContextState = errorContext || { - messages: [], - clearMessage: () => {}, - }; + const { messages, clearMessage }: ErrorContextType = useErrorContext(); // `ErrorContextType` used here return ( <> @@ -40,7 +23,7 @@ const Layout: React.FC = () => { clearMessage(message.status, message.message); }} severity={message.status} - sx={{ width: "100%", borderRadius: 0 }} + sx={{ width: '100%', borderRadius: 0 }} key={`${message.status} ${message.message}`} > {message.message} diff --git a/web/src/Services/Auth.ts b/web/src/Services/Auth.ts index 3c415563..a0ad6025 100644 --- a/web/src/Services/Auth.ts +++ b/web/src/Services/Auth.ts @@ -12,6 +12,14 @@ type AuthContextType = { export const AuthContext = createContext(undefined); +/** + * Custom React Hook for managing Keycloak authentication state. + * + * This hook fetches the Keycloak configuration and initializes the Keycloak instance. + * It then sets the authentication state based on the result of the initialization. + * + * @returns An object containing the authentication state and related information. + */ export function useAuth(): AuthContextType { const [authenticated, setAuthenticated] = useState(null); const [email, setEmail] = useState(null); @@ -19,6 +27,12 @@ export function useAuth(): AuthContextType { const [privilegedGroups, setPrivilegedGroups] = useState([]); const [keycloakToken, setKeycloakToken] = useState(null); + /** + * Refreshes the Keycloak token periodically. + * + * @param keycloak - The Keycloak instance. + * @param config - The Keycloak configuration. + */ const refreshToken = ( keycloak: any, config: { keycloak: { token_validation_interval: number } }, @@ -40,6 +54,11 @@ export function useAuth(): AuthContextType { }, config.keycloak.token_validation_interval); }; + /** + * Initializes the Keycloak instance and sets the authentication state. + * + * @param config - The Keycloak configuration. + */ useEffect(() => { fetchConfig().then(config => { if (config.keycloak.enabled) { diff --git a/web/src/Services/DeployLockHandler.tsx b/web/src/Services/DeployLockHandler.tsx index 1580b6a2..dca6e768 100644 --- a/web/src/Services/DeployLockHandler.tsx +++ b/web/src/Services/DeployLockHandler.tsx @@ -1,10 +1,22 @@ import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +/** + * Fetches the deploy lock status from the server. + * + * @returns A promise that resolves to a boolean value representing the deploy lock status. + */ export async function fetchDeployLock(): Promise { const response = await fetch('/api/v1/deploy-lock'); return await response.json(); } +/** + * Releases the deploy lock on the server. + * + * @param {string | null} keycloakToken - The token used for authentication with the Keycloak server. + * @returns {Promise} - A promise that resolves when the deploy lock is successfully released. + * @throws {Error} - If the server returns a status code other than 200. + */ export async function releaseDeployLock(keycloakToken: string | null): Promise { let headers: Record = { 'Content-Type': 'application/json', @@ -24,6 +36,13 @@ export async function releaseDeployLock(keycloakToken: string | null): Promise} - A promise that resolves when the deploy lock is successfully set. + * @throws {Error} - If the server returns a status code other than 200. + */ export async function setDeployLock(keycloakToken: string | null = null): Promise { let headers: Record = { 'Content-Type': 'application/json', @@ -49,10 +68,24 @@ interface DeployLockProviderProps { children: ReactNode; } +/** + * DeployLockProvider component provides the deploy lock state to its children components. + * It establishes a WebSocket connection to the server to listen for deploy lock status changes. + * + * @param {ReactNode} children - The children components that will receive the deploy lock state. + * @returns {JSX.Element} - The DeployLockProvider component. + */ export function DeployLockProvider({ children }: DeployLockProviderProps): JSX.Element { const [deployLock, setDeployLockState] = useState(false); const [socket, setSocket] = useState(null); + /** + * Establishes a WebSocket connection to the server. + * + * @param {string} wsUrl - The URL of the WebSocket server. + * @param {WebSocket} newSocket - The newly created WebSocket instance. + * @returns {void} - No return value. + */ useEffect(() => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; @@ -65,6 +98,12 @@ export function DeployLockProvider({ children }: DeployLockProviderProps): JSX.E }; }, []); + /** + * Listens for deploy lock status changes from the server. + * + * @param {WebSocket} socket - The WebSocket instance that listens for messages. + * @returns {void} - No return value. + */ useEffect(() => { if (socket) { socket.onopen = async () => { @@ -89,6 +128,11 @@ export function DeployLockProvider({ children }: DeployLockProviderProps): JSX.E ); } +/** + * Custom hook to retrieve the deploy lock status from the context. + * + * @returns The deploy lock status as a boolean value. + */ export function useDeployLock(): boolean { const context = useContext(DeployLockContext); if (context === undefined) { From be10b500c532743a3ed279bd02241a8291a87d47 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Mon, 27 May 2024 18:41:20 +0300 Subject: [PATCH 05/11] chore: migrate TaskView to typescript --- web/src/Components/Navbar.tsx | 17 +++- web/src/Components/Sidebar.tsx | 9 ++ .../Components/{TaskView.js => TaskView.tsx} | 97 +++++++++++++------ 3 files changed, 92 insertions(+), 31 deletions(-) rename web/src/Components/{TaskView.js => TaskView.tsx} (83%) diff --git a/web/src/Components/Navbar.tsx b/web/src/Components/Navbar.tsx index c9fc1d45..917ba0e4 100644 --- a/web/src/Components/Navbar.tsx +++ b/web/src/Components/Navbar.tsx @@ -25,6 +25,15 @@ interface NavigationButtonProps { external?: boolean; } +/** + * A navigation button component that is used for internal and external links. + * @component + * @param {Object} props - The props object for the NavigationButton component. + * @param {string} props.to - The URL link for the button. + * @param {boolean} [props.external=false] - Whether the link is an external link. + * @param {string|ReactNode} props.children - The content of the button. + * @returns {ReactNode} The rendered NavigationButton component. + */ const NavigationButton: React.FC = ({ to, children, external = false }) => { if (external) { return ( @@ -52,7 +61,13 @@ const NavigationButton: React.FC = ({ to, children, exter ); }; -const Navbar: React.FC = () => { +/** + * Navbar component for the application. + * + * @component + * @returns {JSX.Element} The rendered Navbar component. + */ +const Navbar: React.FC = (): JSX.Element => { const [version, setVersion] = useState('0.0.0'); const [isSidebarOpen, setSidebarOpen] = useState(false); const { setSuccess, setError } = useErrorContext(); diff --git a/web/src/Components/Sidebar.tsx b/web/src/Components/Sidebar.tsx index ff8d14e9..1df0fecd 100644 --- a/web/src/Components/Sidebar.tsx +++ b/web/src/Components/Sidebar.tsx @@ -27,6 +27,15 @@ interface SidebarProps { onClose: () => void; } +/** + * Sidebar component that displays configuration data and provides functionality to toggle deploy lock. + * + * @component + * @param {Object} props - The props for the Sidebar component. + * @param {boolean} props.open - Indicates whether the sidebar is open or closed. + * @param {Function} props.onClose - The callback function to handle closing the sidebar. + * @returns {JSX.Element} The rendered Sidebar component. + */ const Sidebar: React.FC = ({ open, onClose }) => { const [configData, setConfigData] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/web/src/Components/TaskView.js b/web/src/Components/TaskView.tsx similarity index 83% rename from web/src/Components/TaskView.js rename to web/src/Components/TaskView.tsx index 1a75f0bf..13fc5557 100644 --- a/web/src/Components/TaskView.js +++ b/web/src/Components/TaskView.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { Box, Button, + CircularProgress, Container, Dialog, DialogActions, @@ -12,27 +13,61 @@ import { Divider, Grid, Paper, + Tooltip, Typography, - CircularProgress, - Tooltip } from '@mui/material'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import { fetchTask } from '../Services/Data'; +import { fetchConfig, fetchTask } from '../Services/Data'; import { useErrorContext } from '../ErrorContext'; import { formatDateTime, ProjectDisplay, StatusReasonDisplay } from './TasksTable'; import { AuthContext } from '../Services/Auth'; -import { fetchConfig } from '../Services/Data'; import { useDeployLock } from '../Services/DeployLockHandler'; +interface Task { + id: string; + created: string; + updated: string; + app: string; + author: string; + project: string; + status: string; + status_reason?: string; + images: Array<{ image: string; tag: string }>; +} + +interface ConfigData { + argo_cd_url_alias?: string; + argo_cd_url?: { Scheme: string; Host: string; Path: string }; +} + +interface ConfigType { + keycloak: { + enabled: boolean; + url: string; + realm: string; + client_id: string; + privileged_groups: string[]; + token_validation_interval: number; + }; + argo_cd_url_alias?: string; + argo_cd_url?: { Scheme: string; Host: string; Path: string }; +} + export default function TaskView() { - const { id } = useParams(); - const [task, setTask] = useState(null); + const { id } = useParams<{ id: string }>(); + const [task, setTask] = useState(null); const { setError, setSuccess } = useErrorContext(); - const { authenticated, email, groups, privilegedGroups, keycloakToken } = useContext(AuthContext); - const [configData, setConfigData] = useState(null); + const authContext = useContext(AuthContext); + + if (!authContext) { + throw new Error('AuthContext must be used within an AuthProvider'); + } + + const { authenticated, email, groups, privilegedGroups, keycloakToken } = authContext; + const [configData, setConfigData] = useState(null); const navigate = useNavigate(); const [open, setOpen] = useState(false); @@ -52,40 +87,42 @@ export default function TaskView() { const deployLock = useDeployLock(); useEffect(() => { - fetchConfig().then(config => { + fetchConfig().then((config: ConfigType) => { setConfigData(config); + }).catch(error => { + setError('fetchConfig', error.message); }); - }, []); + }, [setError]); const getArgoCDUrl = () => { if (configData?.argo_cd_url_alias) { - return `${configData.argo_cd_url_alias}/applications/${task.app}`; + return `${configData.argo_cd_url_alias}/applications/${task?.app}`; } else if (configData?.argo_cd_url) { - return `${configData.argo_cd_url.Scheme}://${configData.argo_cd_url.Host}${configData.argo_cd_url.Path}/applications/${task.app}`; + return `${configData.argo_cd_url.Scheme}://${configData.argo_cd_url.Host}${configData.argo_cd_url.Path}/applications/${task?.app}`; } return ''; }; useEffect(() => { - fetchTask(id) + fetchTask(id!) .then(item => { setSuccess('fetchTask', 'Fetched task successfully'); - setTask(item); + setTask(item as Task); }) - .catch(error => { + .catch((error: Error) => { setError('fetchTasks', error.message); }); - }, [id]); + }, [id, setError, setSuccess]); useEffect(() => { - let intervalId; + let intervalId: NodeJS.Timeout; const fetchTaskStatus = async () => { console.log('Fetching task status...'); try { - const updatedTask = await fetchTask(id); - setTask(updatedTask); - } catch (error) { + const updatedTask = await fetchTask(id!); + setTask(updatedTask as Task); + } catch (error: any) { setError('fetchTaskStatus', error.message); } }; @@ -99,14 +136,16 @@ export default function TaskView() { clearInterval(intervalId); } }; - }, [task]); + }, [task, id, setError]); - const userIsPrivileged = groups && privilegedGroups && groups.some(group => privilegedGroups.includes(group)); + const userIsPrivileged = groups && privilegedGroups && groups.some((group: string) => privilegedGroups.includes(group)); const rollbackToVersion = async () => { + if (!task) return; + const updatedTask = { ...task, - author: email, + author: email!, }; try { @@ -114,23 +153,21 @@ export default function TaskView() { method: 'POST', headers: { 'Content-Type': 'application/json', - // We replaced Authorization with Keycloak-Authorization here - // to simplify JWT implementation and avoid header name overlap - 'Keycloak-Authorization': keycloakToken, + 'Keycloak-Authorization': keycloakToken || '', }, body: JSON.stringify(updatedTask), }); - if (response.status === 401) { // HTTP 401 Unauthorized + if (response.status === 401) { throw new Error('You are not authorized to perform this action!'); - } else if (response.status === 406) { // HTTP 406 Not Acceptable + } else if (response.status === 406) { throw new Error('Lockdown is active. Deployments are forbidden!'); - } else if (response.status !== 202) { // HTTP 202 Accepted + } else if (response.status !== 202) { throw new Error(`Received unexpected status code: ${response.status}`); } navigate('/'); - } catch (error) { + } catch (error: any) { setError('fetchTasks', error.message); } }; From baf71b681aac7f276ef902b342b06a3af66a696d Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Mon, 27 May 2024 19:01:35 +0300 Subject: [PATCH 06/11] checkpoint --- web/package-lock.json | 12 +++ web/package.json | 1 + ...ationsFilter.js => ApplicationsFilter.tsx} | 25 ++--- .../{HistoryTasks.js => HistoryTasks.tsx} | 91 ++++++++++--------- web/types/react-datepicker.d.ts | 82 +++++++++++++++++ 5 files changed, 156 insertions(+), 55 deletions(-) rename web/src/Components/{ApplicationsFilter.js => ApplicationsFilter.tsx} (53%) rename web/src/Components/{HistoryTasks.js => HistoryTasks.tsx} (65%) create mode 100644 web/types/react-datepicker.d.ts diff --git a/web/package-lock.json b/web/package-lock.json index 4edc409e..f8afc3c5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -34,6 +34,7 @@ "devDependencies": { "@types/jest": "^29.5.12", "@types/react": "^18.3.3", + "@types/react-datepicker": "^6.2.0", "@types/react-dom": "^18.3.0", "jest": "^29.7.0" } @@ -4756,6 +4757,17 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "dev": true, + "dependencies": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + } + }, "node_modules/@types/react-dom": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", diff --git a/web/package.json b/web/package.json index 6482a18d..ef25985c 100644 --- a/web/package.json +++ b/web/package.json @@ -58,6 +58,7 @@ "devDependencies": { "@types/jest": "^29.5.12", "@types/react": "^18.3.3", + "@types/react-datepicker": "^6.2.0", "@types/react-dom": "^18.3.0", "jest": "^29.7.0" } diff --git a/web/src/Components/ApplicationsFilter.js b/web/src/Components/ApplicationsFilter.tsx similarity index 53% rename from web/src/Components/ApplicationsFilter.js rename to web/src/Components/ApplicationsFilter.tsx index 393494cf..c029ad3f 100644 --- a/web/src/Components/ApplicationsFilter.js +++ b/web/src/Components/ApplicationsFilter.tsx @@ -1,22 +1,29 @@ import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import Autocomplete from '@mui/material/Autocomplete'; import TextField from '@mui/material/TextField'; -function ApplicationsFilter({ value, onChange, appNames }) { - const [applications, setApplications] = useState([]); +interface ApplicationsFilterProps { + value: string | null; + onChange: (newValue: string | null) => void; + appNames: string[]; +} + +const ApplicationsFilter: React.FC = ({ value, onChange, appNames }) => { + const [applications, setApplications] = useState([]); useEffect(() => { setApplications(appNames); }, [appNames]); - const handleApplicationsChange = (_event, newValue) => { - onChange?.(newValue); + const handleApplicationsChange = (_event: React.ChangeEvent<{}>, newValue: string | null) => { + if (onChange) { + onChange(newValue); + } }; return ( ); -} - -ApplicationsFilter.propTypes = { - value: PropTypes.any, - onChange: PropTypes.func, - appNames: PropTypes.array.isRequired }; export default ApplicationsFilter; diff --git a/web/src/Components/HistoryTasks.js b/web/src/Components/HistoryTasks.tsx similarity index 65% rename from web/src/Components/HistoryTasks.js rename to web/src/Components/HistoryTasks.tsx index df18564f..bea8bcc8 100644 --- a/web/src/Components/HistoryTasks.js +++ b/web/src/Components/HistoryTasks.tsx @@ -1,5 +1,4 @@ import React, { forwardRef, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import { useSearchParams } from 'react-router-dom'; import Box from '@mui/material/Box'; import Container from '@mui/material/Container'; @@ -16,45 +15,48 @@ import ApplicationsFilter from './ApplicationsFilter'; import TasksTable, { useTasks } from './TasksTable'; import { useErrorContext } from '../ErrorContext'; -const DateRangePickerCustomInput = forwardRef(({ value, onClick }, ref) => ( - -)); +interface DateRangePickerCustomInputProps { + value: string; + onClick: () => void; +} -DateRangePickerCustomInput.propTypes = { - value: PropTypes.string, - onClick: PropTypes.func.isRequired, -}; +const DateRangePickerCustomInput = forwardRef( + ({ value, onClick }, ref) => ( + + ) +); + +DateRangePickerCustomInput.displayName = 'DateRangePickerCustomInput'; -function HistoryTasks() { +interface HistoryTasksProps {} + +const HistoryTasks: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); const { setError, setSuccess } = useErrorContext(); const { tasks, sortField, setSortField, appNames, refreshTasksInRange, clearTasks } = useTasks({ setError, setSuccess }); - const [currentApplication, setCurrentApplication] = useState( - searchParams.get('app') ?? null, + const [currentApplication, setCurrentApplication] = useState( + searchParams.get('app') ); - const [dateRange, setDateRange] = useState([ - Number(searchParams.get('start')) - ? new Date(Number(searchParams.get('start')) * 1000) - : startOfDay(new Date()), - Number(searchParams.get('end')) - ? new Date(Number(searchParams.get('end')) * 1000) - : startOfDay(new Date()), + const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([ + searchParams.get('start') ? new Date(Number(searchParams.get('start')) * 1000) : startOfDay(new Date()), + searchParams.get('end') ? new Date(Number(searchParams.get('end')) * 1000) : startOfDay(new Date()), ]); const [startDate, endDate] = dateRange; - const [currentPage, setCurrentPage] = useState( - searchParams.get('page') ? Number(searchParams.get('page')) : 1, + const [currentPage, setCurrentPage] = useState( + searchParams.get('page') ? Number(searchParams.get('page')) : 1 ); - const updateSearchParameters = (start, end, application, page) => { - const params = { + const updateSearchParameters = (start: Date, end: Date, application: string | null, page: number) => { + const params: Record = { start: Math.floor(start.getTime() / 1000), end: Math.floor(end.getTime() / 1000), }; @@ -70,12 +72,12 @@ function HistoryTasks() { setSearchParams(params); }; - const refreshWithFilters = (start, end, application, page) => { + const refreshWithFilters = (start: Date | null, end: Date | null, application: string | null, page: number) => { if (start && end) { refreshTasksInRange( Math.floor(startOfDay(start).getTime() / 1000), Math.floor(endOfDay(end).getTime() / 1000), - application, + application ); updateSearchParameters(start, end, application, page); } else { @@ -84,7 +86,7 @@ function HistoryTasks() { }; useEffect(() => { - refreshWithFilters(startDate, endDate, currentApplication, currentPage); + refreshWithFilters(startDate!, endDate!, currentApplication, currentPage); }, [startDate, endDate, currentApplication, currentPage]); return ( @@ -117,26 +119,29 @@ function HistoryTasks() { onChange={value => { setCurrentApplication(value); setCurrentPage(1); - refreshWithFilters(startDate, endDate, value, 1); + refreshWithFilters(startDate!, endDate!, value, 1); }} - setError={setError} - setSuccess={setSuccess} appNames={appNames} /> { + onChange={(update: [Date | null, Date | null]) => { setDateRange(update); setCurrentPage(1); refreshWithFilters(update[0], update[1], currentApplication, 1); }} maxDate={new Date()} isClearable={false} - customInput={} + customInput={ + {}} + /> + } required /> @@ -147,7 +152,7 @@ function HistoryTasks() { title="Reload table" onClick={() => { setCurrentPage(1); - refreshWithFilters(startDate, endDate, currentApplication, 1); + refreshWithFilters(startDate!, endDate!, currentApplication, 1); }} > @@ -165,16 +170,16 @@ function HistoryTasks() { onPageChange={page => { setCurrentPage(page); updateSearchParameters( - startDate, - endDate, + startDate!, + endDate!, currentApplication, - page, + page ); }} /> ); -} +}; export default HistoryTasks; diff --git a/web/types/react-datepicker.d.ts b/web/types/react-datepicker.d.ts new file mode 100644 index 00000000..f2b0831e --- /dev/null +++ b/web/types/react-datepicker.d.ts @@ -0,0 +1,82 @@ +declare module 'react-datepicker' { + import * as React from 'react'; + import { Locale } from 'date-fns'; + + interface ReactDatePickerProps { + selected?: Date | null; + onChange?: (date: Date | [Date | null, Date | null], event: React.SyntheticEvent | undefined) => void; + onSelect?: (date: Date, event: React.SyntheticEvent | undefined) => void; + onClickOutside?: (event: React.SyntheticEvent) => void; + onChangeRaw?: (event: React.SyntheticEvent) => void; + onFocus?: (event: React.SyntheticEvent) => void; + onBlur?: (event: React.SyntheticEvent) => void; + onKeyDown?: (event: React.SyntheticEvent) => void; + dateFormat?: string | string[]; + dateFormatCalendar?: string; + className?: string; + wrapperClassName?: string; + calendarClassName?: string; + todayButton?: React.ReactNode; + customInput?: React.ReactNode; + customInputRef?: string; + placeholderText?: string; + id?: string; + name?: string; + autoComplete?: string; + disabled?: boolean; + disabledKeyboardNavigation?: boolean; + open?: boolean; + openToDate?: Date; + minDate?: Date; + maxDate?: Date; + selectsStart?: boolean; + selectsEnd?: boolean; + startDate?: Date; + endDate?: Date; + excludeDates?: Date[]; + filterDate?: (date: Date) => boolean; + fixedHeight?: boolean; + formatWeekNumber?: (date: Date) => string | number; + highlightDates?: Date[] | { [className: string]: Date[] }; + includeDates?: Date[]; + includeTimes?: Date[]; + injectTimes?: Date[]; + inline?: boolean; + locale?: string | Locale; + peekNextMonth?: boolean; + showMonthDropdown?: boolean; + showPreviousMonths?: boolean; + showYearDropdown?: boolean; + dropdownMode?: 'scroll' | 'select'; + timeCaption?: string; + timeFormat?: string; + timeIntervals?: number; + minTime?: Date; + maxTime?: Date; + excludeTimes?: Date[]; + useWeekdaysShort?: boolean; + showTimeSelect?: boolean; + showTimeSelectOnly?: boolean; + utcOffset?: number; + weekLabel?: string; + withPortal?: boolean; + showWeekNumbers?: boolean; + forceShowMonthNavigation?: boolean; + showDisabledMonthNavigation?: boolean; + scrollableYearDropdown?: boolean; + scrollableMonthYearDropdown?: boolean; + yearDropdownItemNumber?: number; + previousMonthButtonLabel?: React.ReactNode; + nextMonthButtonLabel?: React.ReactNode; + previousYearButtonLabel?: string; + nextYearButtonLabel?: string; + timeInputLabel?: string; + inlineFocusSelectedMonth?: boolean; + shouldCloseOnSelect?: boolean; + useShortMonthInDropdown?: boolean; + } + + class ReactDatePicker extends React.Component {} + + export default ReactDatePicker; +} From 6339f2ab4ec33bcdf93f9c94c83da9c148172e17 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Mon, 27 May 2024 19:29:20 +0300 Subject: [PATCH 07/11] chore: migrate RecentTasks to typescript --- .../{RecentTasks.js => RecentTasks.tsx} | 73 +++++++------------ 1 file changed, 25 insertions(+), 48 deletions(-) rename web/src/Components/{RecentTasks.js => RecentTasks.tsx} (68%) diff --git a/web/src/Components/RecentTasks.js b/web/src/Components/RecentTasks.tsx similarity index 68% rename from web/src/Components/RecentTasks.js rename to web/src/Components/RecentTasks.tsx index b351183e..fd150be0 100644 --- a/web/src/Components/RecentTasks.js +++ b/web/src/Components/RecentTasks.tsx @@ -8,6 +8,7 @@ import { InputLabel, MenuItem, Select, + SelectChangeEvent, Stack, Typography, } from '@mui/material'; @@ -17,7 +18,7 @@ import ApplicationsFilter from './ApplicationsFilter'; import TasksTable, { useTasks } from './TasksTable'; import { useErrorContext } from '../ErrorContext'; -const autoRefreshIntervals = { +const autoRefreshIntervals: { [key: string]: number } = { '5s': 5, '10s': 10, '30s': 30, @@ -25,7 +26,7 @@ const autoRefreshIntervals = { off: 0, }; -function RecentTasks() { +const RecentTasks: React.FC = () => { const { setError, setSuccess } = useErrorContext(); const [searchParams, setSearchParams] = useSearchParams(); const { tasks, sortField, setSortField, appNames, refreshTasksInTimeframe } = useTasks({ @@ -33,37 +34,33 @@ function RecentTasks() { setSuccess, }); - const [currentAutoRefresh, setCurrentAutoRefresh] = useState( - () => localStorage.getItem('refresh') || autoRefreshIntervals['30s'], + const [currentAutoRefresh, setCurrentAutoRefresh] = useState(() => + Number(localStorage.getItem('refresh')) || autoRefreshIntervals['30s'] ); - const autoRefreshIntervalRef = useRef(null); - const [currentApplication, setCurrentApplication] = useState( - searchParams.get('app') ?? null, + const autoRefreshIntervalRef = useRef(null); + const [currentApplication, setCurrentApplication] = useState( + searchParams.get('app') ); const currentTimeframe = 9 * 60 * 60; - const [currentPage, setCurrentPage] = useState( - searchParams.get('page') ? Number(searchParams.get('page')) : 1, + const [currentPage, setCurrentPage] = useState( + searchParams.get('page') ? Number(searchParams.get('page')) : 1 ); - const updateSearchParameters = (application, page) => { - const params = {}; + const updateSearchParameters = (application: string | null, page: number) => { + const params = new URLSearchParams(); if (application) { - params.app = application; + params.set('app', application); } if (page !== 1) { - params.page = page; + params.set('page', page.toString()); } setSearchParams(params); }; - // Initial load useEffect(() => { - // If the application has been changed or the timeframe has been changed, a new tasks fetching action is executed refreshTasksInTimeframe(currentTimeframe, currentApplication); - // Current page is reset to the first one const initialPage = searchParams.get('page') ? Number(searchParams.get('page')) : 1; - // Save search parameters - application name and auto-refresh interval - to Local Storage for preservation across sessions updateSearchParameters(currentApplication, initialPage); }, []); @@ -71,25 +68,19 @@ function RecentTasks() { localStorage.setItem('refresh', currentAutoRefresh.toString()); }, [currentAutoRefresh]); - // Reset the interval on any state change (because we use the state variables for data retrieval) useEffect(() => { - // Reset the current interval if (autoRefreshIntervalRef.current !== null) { clearInterval(autoRefreshIntervalRef.current); } if (!currentAutoRefresh) { - // Value is 0 for "off" return; } - // Set interval autoRefreshIntervalRef.current = setInterval(() => { - // Again fetch the tasks in current timeframe refreshTasksInTimeframe(currentTimeframe, currentApplication); }, currentAutoRefresh * 1000); - // Clear interval on exit return () => { if (autoRefreshIntervalRef.current !== null) { clearInterval(autoRefreshIntervalRef.current); @@ -97,10 +88,9 @@ function RecentTasks() { }; }, [currentAutoRefresh, currentApplication, currentTimeframe]); - const handleAutoRefreshChange = event => { - // Change the value - setCurrentAutoRefresh(event.target.value); - // Save to URL + const handleAutoRefreshChange = (event: SelectChangeEvent) => { + const value = Number(event.target.value); + setCurrentAutoRefresh(value); updateSearchParameters(currentApplication, 1); }; @@ -121,7 +111,7 @@ function RecentTasks() { display: 'flex', gap: '10px', m: 0, - alignItems: 'center', // Center text vertically + alignItems: 'center', }} > Recent tasks @@ -134,18 +124,14 @@ function RecentTasks() { onChange={value => { setCurrentApplication(value); refreshTasksInTimeframe(currentTimeframe, value); - // Reset page setCurrentPage(1); - // Update URL updateSearchParameters(value, 1); }} - setError={setError} - setSuccess={setSuccess} appNames={appNames} /> - + Auto-Refresh