diff --git a/web/package-lock.json b/web/package-lock.json index 42a0f250..f8afc3c5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -30,6 +30,13 @@ "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-datepicker": "^6.2.0", + "@types/react-dom": "^18.3.0", + "jest": "^29.7.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2851,8 +2858,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 +2875,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 +2922,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 +2948,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 +2960,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 +2991,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 +3000,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 +3020,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 +3037,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 +3052,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 +3066,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 +3075,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 +3090,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 +3103,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 +3118,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 +3131,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 +3143,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 +3160,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 +3175,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 +3218,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 +3244,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 +3266,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 +3291,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 +3300,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 +3315,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 +3330,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 +3343,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 +3355,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 +3369,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 +3384,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 +3399,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 +3424,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 +3433,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 +3448,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 +3535,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 +4116,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 +4131,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 +4651,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 +4749,29 @@ "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-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.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 +4797,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 +4859,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 +6417,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 +6746,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 +7288,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 +7477,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 +7673,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 +8651,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 +10518,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 +10544,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 +10558,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 +10589,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 +10601,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 +10615,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 +10648,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 +10660,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 +10677,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 +10691,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 +10736,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 +10762,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 +10774,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 +10795,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 +10810,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 +10826,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 +10857,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 +10866,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 +10886,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 +10903,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 +10918,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 +10932,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 +10941,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 +10956,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 +10969,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 +10984,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 +10996,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 +11010,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 +11022,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 +11038,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 +11050,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 +11197,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 +11214,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 +11631,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 +11644,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 +11656,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 +11670,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 +11685,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 +11697,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 +11711,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 +11731,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 +11743,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 +11757,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 +11815,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 +11828,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 +11876,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 +11908,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 +11934,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 +11946,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 +11977,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 +11986,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 +12006,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 +12023,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 +12038,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 +12052,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 +12061,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 +12070,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 +12080,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 +12095,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 +12108,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 +12141,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 +12167,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 +12179,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 +12210,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 +12219,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 +12239,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 +12256,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 +12271,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 +12285,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 +12294,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 +12309,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 +12334,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 +12365,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 +12391,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 +12403,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 +12434,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 +12443,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 +12458,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 +12472,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 +12487,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 +12500,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 +12564,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 +15340,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 +15350,7 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ], - "optional": true, - "peer": true + ] }, "node_modules/q": { "version": "1.5.1", @@ -19078,8 +18992,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 +19006,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 +19952,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 +19970,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..ef25985c 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,12 @@ "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-datepicker": "^6.2.0", + "@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..3184b6ae --- /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 './Services/Auth'; +import { DeployLockProvider } from './Services/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/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/src/Components/Navbar.js b/web/src/Components/Navbar.tsx similarity index 74% rename from web/src/Components/Navbar.js rename to web/src/Components/Navbar.tsx index ccff14e7..917ba0e4 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,22 @@ 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; +} + +/** + * 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 ( ); -} - -NavigationButton.propTypes = { - to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, - children: PropTypes.node.isRequired, - external: PropTypes.bool, }; -function Navbar() { +/** + * 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(); @@ -67,9 +81,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 +150,11 @@ function Navbar() { external > - + - GitHub - - {version} + GitHub + + {version} @@ -147,6 +165,6 @@ function Navbar() { setSidebarOpen(false)} /> ); -} +}; export default Navbar; 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 {Object.keys(autoRefreshIntervals).map(autoRefreshInterval => { - let value = autoRefreshIntervals[autoRefreshInterval]; + const value = autoRefreshIntervals[autoRefreshInterval]; return ( {autoRefreshInterval} @@ -166,17 +152,12 @@ function RecentTasks() { { - // Update tasks refreshTasksInTimeframe(currentTimeframe, currentApplication); - // Reset page setCurrentPage(1); - updateSearchParameters( - currentApplication, - 1, - ); + updateSearchParameters(currentApplication, 1); }} > @@ -184,7 +165,6 @@ function RecentTasks() { - {/* Style the table with Material-UI Paper component */} { setCurrentPage(page); - updateSearchParameters( - currentApplication, - page, - ); + updateSearchParameters(currentApplication, page); }} /> ); -} +}; export default RecentTasks; diff --git a/web/src/Components/Sidebar.js b/web/src/Components/Sidebar.tsx similarity index 58% rename from web/src/Components/Sidebar.js rename to web/src/Components/Sidebar.tsx index d8486888..eb802402 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, useContext, useEffect, useState } from 'react'; import { Box, Button, CircularProgress, Drawer, Paper, + Switch, Table, TableBody, TableCell, @@ -14,66 +14,87 @@ 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 { fetchConfig } from '../config'; -import { releaseDeployLock, setDeployLock, useDeployLock } from '../deployLockHandler'; -import { AuthContext } from '../auth'; +interface ConfigData { + [key: string]: any; +} + +interface SidebarProps { + open: boolean; + onClose: () => void; +} -function Sidebar({ open, onClose }) { - const [configData, setConfigData] = useState(null); +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 authContext = useContext(AuthContext); + if (!authContext) { + throw new Error('AuthContext must be used within an AuthProvider'); + } - const { authenticated, keycloakToken } = useContext(AuthContext); + const { authenticated, keycloakToken } = authContext; 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 +104,13 @@ function Sidebar({ open, onClose }) { Loading... ); - } else if (error) { + } + + if (error) { return {error}; - } else if (configData) { + } + + if (configData) { return ( <> @@ -102,9 +127,7 @@ function Sidebar({ open, onClose }) { {key} - - {renderTableCell(key, value)} - + {renderTableCell(key, value)} ))} @@ -117,9 +140,9 @@ function Sidebar({ open, onClose }) { > ); - } else { - return No data available; } + + return No data available; }; return ( @@ -132,14 +155,10 @@ function Sidebar({ open, onClose }) { - + Lockdown Mode - + @@ -149,11 +168,6 @@ function Sidebar({ open, onClose }) { ); -} - -Sidebar.propTypes = { - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, }; -export default Sidebar; +export default React.memo(Sidebar); diff --git a/web/src/Components/TaskView.js b/web/src/Components/TaskView.tsx similarity index 81% rename from web/src/Components/TaskView.js rename to web/src/Components/TaskView.tsx index 4e48d781..fbb542af 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 '../auth'; -import { fetchConfig } from '../config'; -import { useDeployLock } from '../deployLockHandler'; +import { AuthContext } from '../Services/Auth'; +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); } }; @@ -167,7 +204,7 @@ export default function TaskView() { Created - {formatDateTime(task.created)} + {formatDateTime(Number(task.created))} @@ -175,7 +212,7 @@ export default function TaskView() { Updated - {formatDateTime(task.updated)} + {formatDateTime(Number(task.updated))} diff --git a/web/src/Components/TasksTable.js b/web/src/Components/TasksTable.tsx similarity index 63% rename from web/src/Components/TasksTable.js rename to web/src/Components/TasksTable.tsx index 41811055..41ba7e05 100644 --- a/web/src/Components/TasksTable.js +++ b/web/src/Components/TasksTable.tsx @@ -1,18 +1,21 @@ import React, { useEffect, useState } from 'react'; import { Link as ReactLink } from 'react-router-dom'; import { addMinutes, format } from 'date-fns'; -import PropTypes from 'prop-types'; -import { Link, MenuItem, TextField } from '@mui/material'; -import Box from '@mui/material/Box'; -import Pagination from '@mui/material/Pagination'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; +import { + Box, + MenuItem, + Pagination, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Tooltip, + Typography, + Link, +} from '@mui/material'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import CircularProgress from '@mui/material/CircularProgress'; @@ -22,11 +25,15 @@ 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 }) { - if (project.indexOf('http') === 0) { +interface ProjectDisplayProps { + project: string; +} + +export function ProjectDisplay({ project }: ProjectDisplayProps) { + if (project.startsWith('http')) { return ( {project.replace(/^http(s)?:\/\//, '').replace(/\/+$/, '')} @@ -36,11 +43,11 @@ export function ProjectDisplay({ project }) { return {project}; } -ProjectDisplay.propTypes = { - project: PropTypes.string.isRequired, -}; +interface StatusReasonDisplayProps { + reason: string; +} -export function StatusReasonDisplay({ reason }) { +export function StatusReasonDisplay({ reason }: StatusReasonDisplayProps) { return ( { +const taskDuration = (created: number, updated: number | null): string => { if (!updated) { updated = Math.round(Date.now() / 1000); } @@ -68,7 +71,7 @@ const taskDuration = (created, updated) => { }; const defaultFormatTime = '---'; -export const formatDateTime = timestamp => { +export const formatDateTime = (timestamp: number | null): string => { if (!timestamp) { return defaultFormatTime; } @@ -76,7 +79,7 @@ export const formatDateTime = timestamp => { let dateTime = new Date(timestamp * 1000); return format( addMinutes(dateTime, dateTime.getTimezoneOffset()), - 'yyyy/MM/dd HH:mm:ss', + 'yyyy/MM/dd HH:mm:ss' ); } catch (error) { console.error(error); @@ -84,21 +87,43 @@ export const formatDateTime = timestamp => { } }; -export function useTasks({ setError, setSuccess }) { - const [tasks, setTasks] = useState([]); - const [sortField, setSortField] = useState({ +interface Task { + id: string; + app: string; + project: string; + author: string; + status: string; + created: number; + updated: number; + images: { id: string; image: string; tag: string }[]; + status_reason?: string; +} + +interface SortField { + field: keyof Task; + direction: 'ASC' | 'DESC'; +} + +interface UseTasksParams { + setError: (context: string, message: string) => void; + setSuccess: (context: string, message: string) => void; +} + +export function useTasks({ setError, setSuccess }: UseTasksParams) { + const [tasks, setTasks] = useState([]); + const [sortField, setSortField] = useState({ field: 'created', direction: 'ASC', }); - const [appNames, setAppNames] = useState([]); + const [appNames, setAppNames] = useState([]); - const refreshTasksInTimeframe = (timeframe, application) => { + const refreshTasksInTimeframe = (timeframe: number, application: string | null) => { fetchTasks(relativeTimestamp(timeframe), null, application) .then(items => { setSuccess('fetchTasks', 'Fetched tasks successfully'); - const appNames = [...new Set(items.map(item => item.app))]; + const appNames = Array.from(new Set(items.map((item: Task) => item.app))); setAppNames(appNames); setTasksSorted(items, sortField); @@ -108,12 +133,12 @@ export function useTasks({ setError, setSuccess }) { }); }; - const refreshTasksInRange = (fromTimestamp, toTimestamp, application) => { + const refreshTasksInRange = (fromTimestamp: number, toTimestamp: number, application: string | null) => { fetchTasks(fromTimestamp, toTimestamp, application) .then(items => { setSuccess('fetchTasks', 'Fetched tasks successfully'); - const appNames = [...new Set(items.map(item => item.app))]; + const appNames = Array.from(new Set(items.map((item: Task) => item.app))); setAppNames(appNames); setTasksSorted(items, sortField); @@ -127,14 +152,16 @@ export function useTasks({ setError, setSuccess }) { setTasks([]); }; - const setTasksSorted = (unsortedTasks, sort) => { - // sort tasks + const setTasksSorted = (unsortedTasks: Task[], sort: SortField) => { unsortedTasks.sort((a, b) => { - let aField = a[sort.field]; - let bField = b[sort.field]; + const aField = a[sort.field]; + const bField = b[sort.field]; if (aField === bField) { return 0; } + if (aField === undefined || bField === undefined) { + return 0; + } if (aField > bField) { return sort.direction === 'ASC' ? -1 : 1; } else { @@ -142,11 +169,9 @@ export function useTasks({ setError, setSuccess }) { } }); - // save sorted tasks - setTasks([].concat(unsortedTasks)); + setTasks([...unsortedTasks]); }; - // sort field change hook useEffect(() => { setTasksSorted(tasks, sortField); }, [sortField]); @@ -158,13 +183,19 @@ export function useTasks({ setError, setSuccess }) { refreshTasksInTimeframe, refreshTasksInRange, clearTasks, - appNames + appNames, }; } -function TableCellSorted({ field, sortField, setSortField, children }) { - const triggerSortChange = triggerField => { - // change sort parameters +interface TableCellSortedProps { + field: keyof Task; + sortField: SortField; + setSortField: (sortField: SortField) => void; + children: React.ReactNode; +} + +function TableCellSorted({ field, sortField, setSortField, children }: TableCellSortedProps) { + const triggerSortChange = (triggerField: keyof Task) => { let sortFieldChange = { ...sortField }; if (sortFieldChange.field === triggerField) { sortFieldChange.direction = @@ -199,7 +230,8 @@ function TableCellSorted({ field, sortField, setSortField, children }) { const cacheKeyItemsPerPage = 'items_per_page'; const itemsPerPageList = [10, 25, 50]; const defaultItemsPerPage = itemsPerPageList[0]; -const getCachedItemsPerPage = () => { + +const getCachedItemsPerPage = (): number => { const itemsPerPage = Number(localStorage.getItem(cacheKeyItemsPerPage)); if (itemsPerPageList.includes(itemsPerPage)) { return itemsPerPage; @@ -207,6 +239,15 @@ const getCachedItemsPerPage = () => { return defaultItemsPerPage; }; +interface TasksTableProps { + tasks: Task[]; + sortField: SortField; + setSortField: (sortField: SortField) => void; + relativeDate: boolean; + onPageChange: (page: number) => void; + page?: number; +} + function TasksTable({ tasks, sortField, @@ -214,17 +255,17 @@ function TasksTable({ relativeDate, onPageChange, page = 1, - }) { - const [itemsPerPage, setItemsPerPage] = useState(getCachedItemsPerPage()); - const [visibleReasons, setVisibleReasons] = useState([]); + }: TasksTableProps) { + const [itemsPerPage, setItemsPerPage] = useState(getCachedItemsPerPage()); + const [visibleReasons, setVisibleReasons] = useState([]); const deployLock = useDeployLock(); - const toggleReason = task => { - setVisibleReasons(visibleReasons => { - if (visibleReasons.includes(task?.id)) { - return [...visibleReasons.filter(id => id !== task?.id)]; + const toggleReason = (task: Task) => { + setVisibleReasons((visibleReasons) => { + if (visibleReasons.includes(task.id)) { + return visibleReasons.filter((id) => id !== task.id); } else { - return [...visibleReasons, task?.id]; + return [...visibleReasons, task.id]; } }); }; @@ -232,12 +273,13 @@ function TasksTable({ const pages = Math.ceil(tasks.length / itemsPerPage); const tasksPaginated = tasks.slice( (page - 1) * itemsPerPage, - page * itemsPerPage, + page * itemsPerPage ); - const handleItemsPerPageChange = event => { - setItemsPerPage(event.target.value); - localStorage.setItem(cacheKeyItemsPerPage, event.target.value); + const handleItemsPerPageChange = (event: React.ChangeEvent) => { + const value = Number(event.target.value); + setItemsPerPage(value); + localStorage.setItem(cacheKeyItemsPerPage, value.toString()); }; return ( @@ -246,66 +288,34 @@ function TasksTable({ - + Id - + Application - + Project - + Author - + Status - + Started - + Duration - + Images - {tasksPaginated.map(task => ( + {tasksPaginated.map((task) => ( @@ -361,34 +371,30 @@ function TasksTable({ )} - {relativeDate && ( + {relativeDate ? ( {relativeTime(task.created * 1000)} - )} - {!relativeDate && ( + ) : ( {formatDateTime(task.created)} )} - {task.status === 'in progress' && ( + {task.status === 'in progress' ? ( {taskDuration(task.created, null)} - )} - {task.status !== 'in progress' && ( - {taskDuration(task.created, task?.updated)} + ) : ( + {taskDuration(task.created, task.updated)} )} - {task.images.map((item) => { - return ( - - {item.image}:{item.tag} - - ); - })} + {task.images.map((item) => ( + + {item.image}:{item.tag} + + ))} - {task?.status_reason && visibleReasons.includes(task?.id) && ( + {task.status_reason && visibleReasons.includes(task.id) && ( { - onPageChange?.(value); + onPageChange(value); }} /> - {itemsPerPageList.map(value => { - return ( - - {value} - - ); - })} + {itemsPerPageList.map((value) => ( + + {value} + + ))} {deployLock && ( - + Lockdown is active )} @@ -464,20 +470,4 @@ function TasksTable({ ); } -TableCellSorted.propTypes = { - field: PropTypes.string.isRequired, - sortField: PropTypes.object.isRequired, - setSortField: PropTypes.func.isRequired, - children: PropTypes.node.isRequired, -}; - -TasksTable.propTypes = { - tasks: PropTypes.array.isRequired, - sortField: PropTypes.object.isRequired, - setSortField: PropTypes.func.isRequired, - relativeDate: PropTypes.bool.isRequired, - onPageChange: PropTypes.func.isRequired, - page: PropTypes.number, -}; - export default TasksTable; 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.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..d3ad96af --- /dev/null +++ b/web/src/Layout.tsx @@ -0,0 +1,38 @@ +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 + +const Alert = React.forwardRef((props, ref) => ( + +)); + +const Layout: React.FC = () => { + const { messages, clearMessage }: ErrorContextType = useErrorContext(); // `ErrorContextType` used here + + 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 ( (undefined); + +let keycloak: Keycloak | null = null; + +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 initializeAuth = async () => { + console.log('initializeAuth triggered'); + try { + const config = await fetchConfig(); + + if (config.keycloak.enabled && !keycloak) { + keycloak = new Keycloak({ + url: config.keycloak.url, + realm: config.keycloak.realm, + clientId: config.keycloak.client_id, + }); + + const authenticated = await keycloak.init({ onLoad: 'login-required' }); + setAuthenticated(authenticated); + + if (authenticated) { + setEmail(keycloak.tokenParsed?.email || null); + setGroups(keycloak.tokenParsed?.groups || []); + setPrivilegedGroups(config.keycloak.privileged_groups); + setKeycloakToken(keycloak.token || null); + } + } else if (!config.keycloak.enabled) { + setAuthenticated(true); + } + } catch (err) { + console.error('Initialization failed', err); + setAuthenticated(false); + } + }; + + useEffect(() => { + initializeAuth(); + }, []); + + useEffect(() => { + let intervalId: NodeJS.Timeout | null = null; + + if (authenticated) { + intervalId = setInterval(() => { + if (keycloak && keycloak.isTokenExpired(20)) { + keycloak.updateToken(20) + .then(refreshed => { + if (refreshed) { + console.log('Token refreshed, valid for ' + Math.round((keycloak!.tokenParsed?.exp ?? 0) + (keycloak!.timeSkew ?? 0) - new Date().getTime() / 1000) + ' seconds'); + setKeycloakToken(keycloak!.token || null); + } + }).catch((err) => { + console.error('Failed to refresh token', err); + }); + } + }, 60000); // Check token expiration every 60 seconds + } + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + }, [authenticated]); + + return { authenticated, email, groups, privilegedGroups, keycloakToken }; +} 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/Services/DeployLockHandler.tsx b/web/src/Services/DeployLockHandler.tsx new file mode 100644 index 00000000..dca6e768 --- /dev/null +++ b/web/src/Services/DeployLockHandler.tsx @@ -0,0 +1,142 @@ +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', + }; + + if (keycloakToken !== null) { + headers['Keycloak-Authorization'] = keycloakToken; + } + + const response = await fetch('/api/v1/deploy-lock', { + method: 'DELETE', + headers: headers, + }); + + if (response.status !== 200) { + throw new Error(`Error: ${response.status}`); + } +} + +/** + * Sets 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 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', + }; + + if (keycloakToken !== null) { + headers['Keycloak-Authorization'] = keycloakToken; + } + + const response = await fetch('/api/v1/deploy-lock', { + method: 'POST', + headers: headers, + }); + + if (response.status !== 200) { + throw new Error(`Error: ${response.status}`); + } +} + +export const DeployLockContext = createContext(false); + +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; + const wsUrl = `${protocol}//${host}/ws`; + const newSocket = new WebSocket(wsUrl); + setSocket(newSocket); + + return () => { + newSocket.close(); + }; + }, []); + + /** + * 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 () => { + const lock = await fetchDeployLock(); + setDeployLockState(lock); + }; + socket.onmessage = (event) => { + const message = event.data; + if (message === 'locked') { + setDeployLockState(true); + } else if (message === 'unlocked') { + setDeployLockState(false); + } + }; + } + }, [socket]); + + return ( + + {children} + + ); +} + +/** + * 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) { + throw new Error('useDeployLock must be used within a DeployLockProvider'); + } + return context; +} diff --git a/web/src/Utils.test.ts b/web/src/Utils.test.ts new file mode 100644 index 00000000..6b6b587a --- /dev/null +++ b/web/src/Utils.test.ts @@ -0,0 +1,28 @@ +import { relativeHumanDuration } from './Utils'; +import { describe, expect, it } from '@jest/globals'; + +describe('relativeHumanDuration', () => { + 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/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/deployLockHandler.js b/web/src/deployLockHandler.js deleted file mode 100644 index c6a6e8c0..00000000 --- a/web/src/deployLockHandler.js +++ /dev/null @@ -1,99 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; - -export async function fetchDeployLock() { - const response = await fetch('/api/v1/deploy-lock'); - return await response.json(); -} - -export async function releaseDeployLock(keycloakToken) { - let headers = { - '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; - } - - const response = await fetch('/api/v1/deploy-lock', { - method: 'DELETE', - headers: headers, - }); - - if (response.status !== 200) { - throw new Error(`Error: ${response.status}`); - } -} - -export async function setDeployLock(keycloakToken = null) { - let headers = { - '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; - } - - const response = await fetch('/api/v1/deploy-lock', { - method: 'POST', - headers: headers, - }); - - if (response.status !== 200) { - throw new Error(`Error: ${response.status}`); - } -} - -export const DeployLockContext = createContext(false); - -export function DeployLockProvider({ children }) { - const [deployLock, setDeployLock] = useState(false); - const [socket, setSocket] = useState(null); - - useEffect(() => { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const host = window.location.host; - const wsUrl = `${protocol}//${host}/ws`; - const newSocket = new WebSocket(wsUrl); - setSocket(newSocket); - - return () => { - newSocket.close(); - }; - }, []); - - useEffect(() => { - if (socket) { - socket.onopen = async () => { - const lock = await fetchDeployLock(); - setDeployLock(lock); - }; - socket.onmessage = (event) => { - const message = event.data; - if (message === 'locked') { - setDeployLock(true); - } else if (message === 'unlocked') { - setDeployLock(false); - } - }; - } - }, [socket]); - - return ( - - {children} - - ); -} - -DeployLockProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -export function useDeployLock() { - return useContext(DeployLockContext); -} 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" + ] +} 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; +}