diff --git a/Client/package-lock.json b/Client/package-lock.json index 69aec5dd2..b66c62dd1 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -59,12 +59,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" }, "engines": { @@ -72,30 +72,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", - "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", - "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.9", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-module-transforms": "^7.24.9", - "@babel/helpers": "^7.24.8", - "@babel/parser": "^7.24.8", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.9", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -117,29 +117,29 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.24.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", - "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.24.9", + "@babel/types": "^7.25.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", - "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -147,67 +147,29 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", - "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -227,77 +189,65 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", - "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.8" + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -307,10 +257,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", - "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.8" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -363,33 +316,30 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", - "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.8", - "@babel/types": "^7.24.8", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -398,13 +348,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", - "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -992,9 +942,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { @@ -1061,14 +1011,14 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -2396,15 +2346,15 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", + "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, @@ -2700,9 +2650,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "funding": [ { "type": "opencollective", @@ -2719,10 +2669,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -2773,9 +2723,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001642", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", - "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", "funding": [ { "type": "opencollective", @@ -3224,9 +3174,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.829", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.829.tgz", - "integrity": "sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==", + "version": "1.5.41", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", + "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", "license": "ISC" }, "node_modules/entities": { @@ -3455,9 +3405,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -3476,17 +3426,18 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -3579,9 +3530,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.8.tgz", - "integrity": "sha512-MIKAclwaDFIiYtVBLzDdm16E+Ty4GwhB6wZlCAG1R3Ur+F9Qbo6PRxpA5DK7XtDgm+WlCoAY2WxAwqhmIDHg6Q==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.13.tgz", + "integrity": "sha512-f1EppwrpJRWmqDTyvAyomFVDYRtrS7iTEqv3nokETnMiMzs2SSTmKRTACce4O2p4jYyowiSMvpdwC/RLcMFhuQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4766,15 +4717,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -5006,9 +4957,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz", - "integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "license": "MIT" }, "node_modules/object-assign": { @@ -6212,9 +6163,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "funding": [ { "type": "opencollective", @@ -6231,8 +6182,8 @@ ], "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx index a71732e3f..3de2cbe07 100644 --- a/Client/src/Components/Inputs/Search/index.jsx +++ b/Client/src/Components/Inputs/Search/index.jsx @@ -38,6 +38,7 @@ const SearchAdornment = () => { ); }; +//TODO keep search state inside of component const Search = ({ id, options, diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx index 12016889f..ce1c89250 100644 --- a/Client/src/Pages/AdvancedSettings/index.jsx +++ b/Client/src/Pages/AdvancedSettings/index.jsx @@ -12,47 +12,45 @@ import { useNavigate } from "react-router"; import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice"; import { useState, useEffect } from "react"; import Select from "../../Components/Inputs/Select"; -import { advancedSettingsValidation } from "../../Validation/validation"; -import { buildErrors, hasValidationErrors } from "../../Validation/error"; const AdvancedSettings = ({ isAdmin }) => { - const navigate = useNavigate(); - useEffect(() => { - if (!isAdmin) { - navigate("/"); - } - }, [navigate, isAdmin]); + const navigate = useNavigate(); - const theme = useTheme(); - const { authToken } = useSelector((state) => state.auth); - const dispatch = useDispatch(); - const settings = useSelector((state) => state.settings); - const [localSettings, setLocalSettings] = useState({ - apiBaseUrl: "", - logLevel: "debug", - systemEmailHost: "", - systemEmailPort: "", - systemEmailAddress: "", - systemEmailPassword: "", - jwtTTL: "", - dbType: "", - redisHost: "", - redisPort: "", - pagespeedApiKey: "", - }); - const [errors, setErrors] = useState({}); + useEffect(() => { + if (!isAdmin) { + navigate("/"); + } + }, [navigate, isAdmin]); - useEffect(() => { - const getSettings = async () => { - const action = await dispatch(getAppSettings({ authToken })); - if (action.payload?.success) { - setLocalSettings(action.payload.data); - } else { - createToast({ body: "Failed to get settings" }); - } - }; - getSettings(); - }, [authToken, dispatch]); + const theme = useTheme(); + const { authToken } = useSelector((state) => state.auth); + const dispatch = useDispatch(); + const settings = useSelector((state) => state.settings); + const [localSettings, setLocalSettings] = useState({ + apiBaseUrl: "", + logLevel: "debug", + systemEmailHost: "", + systemEmailPort: "", + systemEmailAddress: "", + systemEmailPassword: "", + jwtTTL: "", + dbType: "", + redisHost: "", + redisPort: "", + pagespeedApiKey: "", + }); + + useEffect(() => { + const getSettings = async () => { + const action = await dispatch(getAppSettings({ authToken })); + if (action.payload.success) { + setLocalSettings(action.payload.data); + } else { + createToast({ body: "Failed to get settings" }); + } + }; + getSettings(); + }, [authToken, dispatch]); const logItems = [ { _id: 1, name: "none" }, @@ -74,210 +72,189 @@ const AdvancedSettings = ({ isAdmin }) => { setLocalSettings({ ...localSettings, logLevel: newLogLevel }); }; - const handleChange = (event) => { - const { value, id } = event.target; - setLocalSettings({ ...localSettings, [id]: value }); - const { error } = advancedSettingsValidation.validate( - { [id]: value }, - { - abortEarly: false, - } - ); - setErrors((prev) => { - return buildErrors(prev, id, error); - }); - }; + const handleChange = (event) => { + const { value, id } = event.target; + setLocalSettings({ ...localSettings, [id]: value }); + }; - const handleSave = async () => { - if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) - return; - const action = await dispatch( - updateAppSettings({ settings: localSettings, authToken }) - ); - let body = ""; - if (action.payload.success) { - console.log(action.payload.data); - setLocalSettings(action.payload.data); - body = "Settings saved successfully"; - } else { - body = "Failed to save settings"; - } - createToast({ body }); - }; + const handleSave = async () => { + const action = await dispatch( + updateAppSettings({ settings: localSettings, authToken }) + ); + let body = ""; + if (action.payload.success) { + console.log(action.payload.data); + setLocalSettings(action.payload.data); + body = "Settings saved successfully"; + } else { + body = "Failed to save settings"; + } + createToast({ body }); + }; - return ( - - - - - Client settings - - Modify client settings here. - - - - - + + + + + Email settings + + Set your host email settings here. These settings are used for sending + system emails. + + + + + + + + + + + + Server settings + + Modify server settings here. + + + + + + + + + + + + + About + + + BlueWave Uptime v1.0.0 + + Developed by Bluewave Labs. + + + + + + + Save + + + + + ); }; AdvancedSettings.propTypes = { diff --git a/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx b/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx new file mode 100644 index 000000000..b8f6a0986 --- /dev/null +++ b/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx @@ -0,0 +1,32 @@ +import { useTheme } from "@emotion/react"; +import PlaceholderLight from "../../../../assets/Images/data_placeholder.svg?react"; +import PlaceholderDark from "../../../../assets/Images/data_placeholder_dark.svg?react"; +import { Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; + +const Empty = ({ styles, mode }) => { + const theme = useTheme(); + return ( + + + {mode === "light" ? : } + + + No incidents recorded yet. + + + ); +}; + +Empty.propTypes = { + styles: PropTypes.object, + mode: PropTypes.string, +}; + +export { Empty }; diff --git a/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx b/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx new file mode 100644 index 000000000..a3a23c14c --- /dev/null +++ b/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx @@ -0,0 +1,21 @@ +import { Skeleton /* , Stack */ } from "@mui/material"; +const IncidentSkeleton = () => { + return ( + <> + + + + ); +}; + +export { IncidentSkeleton }; diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index 1558084a4..da533c7ba 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -24,6 +24,8 @@ import { useTheme } from "@emotion/react"; import { formatDateWithTz } from "../../../Utils/timeUtils"; import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; +import { Empty } from "./Empty/Empty"; +import { IncidentSkeleton } from "./Skeleton/Skeleton"; const IncidentTable = ({ monitors, selectedMonitor, filter }) => { const uiTimezone = useSelector((state) => state.ui.timezone); @@ -37,6 +39,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { page: 0, rowsPerPage: 14, }); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { setPaginationController((prevPaginationController) => ({ @@ -51,6 +54,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { return; } try { + setIsLoading(true); let res; if (selectedMonitor === "0") { res = await networkService.getChecksByTeam({ @@ -79,6 +83,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { setChecksCount(res.data.data.checksCount); } catch (error) { logger.error(error); + } finally { + setIsLoading(false); } }; fetchPage(); @@ -129,24 +135,20 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { p: theme.spacing(30), }; + const hasChecks = checks?.length === 0; + const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0"; + const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0"; + return ( <> - {checks?.length === 0 && selectedMonitor === "0" ? ( - - - {mode === "light" ? : } - - - No incidents recorded yet. - - - ) : checks?.length === 0 ? ( + {isLoading ? ( + + ) : noIncidentsRecordedYet ? ( + + ) : noIncidentsForThatMonitor ? ( { const [monitors, setMonitors] = useState({}); const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [loading, setLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); // TODO do something with these filters const [filter, setFilter] = useState("all"); useEffect(() => { const fetchMonitors = async () => { - setLoading(true); - const res = await networkService.getMonitorsByTeamId({ - authToken: authState.authToken, - teamId: authState.user.teamId, - limit: -1, - types: null, - status: null, - checkOrder: null, - normalize: null, - page: null, - rowsPerPage: null, - filter: null, - field: null, - order: null, - }); - // Reduce to a lookup object for 0(1) lookup - if (res?.data?.data?.monitors?.length > 0) { - const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => { - acc[monitor._id] = monitor; - return acc; - }, {}); - setMonitors(monitorLookup); - monitorId !== undefined && setSelectedMonitor(monitorId); + try { + setIsLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken: authState.authToken, + teamId: authState.user.teamId, + limit: -1, + types: null, + status: null, + checkOrder: null, + normalize: null, + page: null, + rowsPerPage: null, + filter: null, + field: null, + order: null, + }); + // Reduce to a lookup object for 0(1) lookup + if (res?.data?.data?.monitors?.length > 0) { + const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => { + acc[monitor._id] = monitor; + return acc; + }, {}); + setMonitors(monitorLookup); + monitorId !== undefined && setSelectedMonitor(monitorId); + } + } catch (error) { + console.info(error); + } finally { + setIsLoading(false); } - setLoading(false); }; - fetchMonitors(); }, [authState]); - useEffect(() => {}, []); + useEffect(() => {}, [monitors]); const handleSelect = (event) => { setSelectedMonitor(event.target.value); }; + const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0; + return ( - {loading ? ( + {isActuallyLoading ? ( ) : ( <> diff --git a/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx index d165523f9..4dbf8f747 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -28,7 +28,6 @@ import { MS_PER_WEEK, } from "../../../Utils/timeUtils"; import { useNavigate, useParams } from "react-router-dom"; -import { buildErrors, hasValidationErrors } from "../../../Validation/error"; const getDurationAndUnit = (durationInMs) => { if (durationInMs % MS_PER_DAY === 0) { @@ -146,36 +145,46 @@ const CreateMaintenance = () => { return; } - const res = await networkService.getMaintenanceWindowById({ - authToken: authToken, - maintenanceWindowId: maintenanceWindowId, - }); - const maintenanceWindow = res.data.data; - const { name, start, end, repeat, monitorId } = maintenanceWindow; - const startTime = dayjs(start); - const endTime = dayjs(end); - const durationInMs = endTime.diff(startTime, "milliseconds").toString(); - const { duration, durationUnit } = getDurationAndUnit(durationInMs); - const monitor = monitors.find((monitor) => monitor._id === monitorId); - setForm({ - ...form, - name, - repeat: REVERSE_REPEAT_LOOKUP[repeat], - startDate: startTime, - startTime, - duration, - durationUnit, - monitors: monitor ? [monitor] : [], - }); - } catch (error) { - createToast({ body: "Failed to fetch data" }); - logger.error("Failed to fetch monitors", error); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [authToken, user]); + const res = await networkService.getMaintenanceWindowById({ + authToken: authToken, + maintenanceWindowId: maintenanceWindowId, + }); + const maintenanceWindow = res.data.data; + const { name, start, end, repeat, monitorId } = maintenanceWindow; + const startTime = dayjs(start); + const endTime = dayjs(end); + const durationInMs = endTime.diff(startTime, "milliseconds").toString(); + const { duration, durationUnit } = getDurationAndUnit(durationInMs); + const monitor = monitors.find((monitor) => monitor._id === monitorId); + setForm({ + ...form, + name, + repeat: REVERSE_REPEAT_LOOKUP[repeat], + startDate: startTime, + startTime, + duration, + durationUnit, + monitors: monitor ? [monitor] : [], + }); + } catch (error) { + createToast({ body: "Failed to fetch data" }); + logger.error("Failed to fetch monitors", error); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [authToken, user]); + + const buildErrors = (prev, id, error) => { + const updatedErrors = { ...prev }; + if (error) { + updatedErrors[id] = error.details[0].message; + } else { + delete updatedErrors[id]; + } + return updatedErrors; + }; const handleSearch = (value) => { setSearch(value); @@ -214,17 +223,29 @@ const CreateMaintenance = () => { }); }; - const handleSubmit = async () => { - if(hasValidationErrors(form, maintenanceWindowValidation, setErrors)) - return; - // Build timestamp for maintenance window from startDate and startTime - const start = dayjs(form.startDate) - .set("hour", form.startTime.hour()) - .set("minute", form.startTime.minute()); - // Build end timestamp for maintenance window - const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit]; - const durationInMs = form.duration * MS_MULTIPLIER; - const end = start.add(durationInMs); + const handleSubmit = async () => { + const { error } = maintenanceWindowValidation.validate(form, { + abortEarly: false, + }); + + // If errors, return early + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + logger.error(error); + return; + } + // Build timestamp for maintenance window from startDate and startTime + const start = dayjs(form.startDate) + .set("hour", form.startTime.hour()) + .set("minute", form.startTime.minute()); + // Build end timestamp for maintenance window + const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit]; + const durationInMs = form.duration * MS_MULTIPLIER; + const end = start.add(durationInMs); // Get repeat value in milliseconds const repeat = REPEAT_LOOKUP[form.repeat]; diff --git a/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx b/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx new file mode 100644 index 000000000..613c280a4 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx @@ -0,0 +1,79 @@ +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography } from "@mui/material"; +import Search from "../../../../Components/Inputs/Search"; +import MemoizedMonitorTable from "../MonitorTable"; +import { useState } from "react"; +import useDebounce from "../../../../Utils/debounce"; +import PropTypes from "prop-types"; + +const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => { + const theme = useTheme(); + const [search, setSearch] = useState(""); + const [isSearching, setIsSearching] = useState(false); + const debouncedFilter = useDebounce(search, 500); + const handleSearch = (value) => { + setIsSearching(true); + setSearch(value); + }; + return ( + + + + Actively monitoring + + + {totalMonitors} + + + + + + + + ); +}; + +CurrentMonitoring.propTypes = { + totalMonitors: PropTypes.number, + monitors: PropTypes.array, + isAdmin: PropTypes.bool, +}; + +export { CurrentMonitoring }; diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx new file mode 100644 index 000000000..e30e0a255 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx @@ -0,0 +1,31 @@ +import { Skeleton, TableCell, TableRow } from "@mui/material"; +const ROWS_NUMBER = 7; +const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); + +const TableBodySkeleton = () => { + return ( + <> + {ROWS_ARRAY.map((row) => ( + + + + + + + + + + + + + + + + + + ))} + + ); +}; + +export { TableBodySkeleton }; diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx index 4b0765b76..d9fbc34b2 100644 --- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx +++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx @@ -12,6 +12,7 @@ import { Stack, Typography, Button, + CircularProgress, } from "@mui/material"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; @@ -34,6 +35,7 @@ import RightArrow from "../../../../assets/icons/right-arrow.svg?react"; import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react"; import ActionsMenu from "../actionsMenu"; import useUtils from "../../utils"; +import { TableBodySkeleton } from "./Skeleton"; /** * Component for pagination actions (first, previous, next, last). @@ -107,17 +109,17 @@ TablePaginationActions.propTypes = { onPageChange: PropTypes.func.isRequired, }; -const MonitorTable = ({ isAdmin, filter, setLoading }) => { +const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => { const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); const { determineState } = useUtils(); const { rowsPerPage } = useSelector((state) => state.ui.monitors); + const authState = useSelector((state) => state.auth); const [page, setPage] = useState(0); const [monitors, setMonitors] = useState([]); const [monitorCount, setMonitorCount] = useState(0); - const authState = useSelector((state) => state.auth); const [updateTrigger, setUpdateTrigger] = useState(false); const [sort, setSort] = useState({}); const prevFilter = useRef(filter); @@ -160,15 +162,25 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }); setMonitors(res?.data?.data?.monitors ?? []); setMonitorCount(res?.data?.data?.monitorCount ?? 0); - setLoading(false); } catch (error) { logger.error(error); + } finally { + setIsSearching(false); } - }, [authState, page, rowsPerPage, filter, sort, setLoading]); + }, [authState, page, rowsPerPage, filter, sort, setIsSearching]); useEffect(() => { fetchPage(); - }, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]); + }, [ + updateTrigger, + authState, + page, + rowsPerPage, + filter, + sort, + setIsSearching, + fetchPage, + ]); // Listen for changes in filter, if new value reset the page useEffect(() => { @@ -220,7 +232,37 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }; return ( - <> + + {isSearching && ( + <> + + + + + + )} @@ -271,77 +313,82 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { - {monitors.map((monitor) => { - let uptimePercentage = ""; - let percentageColor = theme.palette.percentage.uptimeExcellent; + {/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */} + {monitors.length > 0 ? ( + monitors.map((monitor) => { + let uptimePercentage = ""; + let percentageColor = theme.palette.percentage.uptimeExcellent; - // Determine uptime percentage and color based on the monitor's uptimePercentage value - if (monitor.uptimePercentage !== undefined) { - uptimePercentage = - monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); + // Determine uptime percentage and color based on the monitor's uptimePercentage value + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 + ? "0" + : (monitor.uptimePercentage * 100).toFixed(2); - percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.percentage.uptimePoor - : monitor.uptimePercentage < 0.5 - ? theme.palette.percentage.uptimeFair - : monitor.uptimePercentage < 0.75 - ? theme.palette.percentage.uptimeGood - : theme.palette.percentage.uptimeExcellent; - } + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.percentage.uptimePoor + : monitor.uptimePercentage < 0.5 + ? theme.palette.percentage.uptimeFair + : monitor.uptimePercentage < 0.75 + ? theme.palette.percentage.uptimeGood + : theme.palette.percentage.uptimeExcellent; + } - const params = { - url: monitor.url, - title: monitor.name, - percentage: uptimePercentage, - percentageColor, - status: determineState(monitor), - }; + const params = { + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + status: determineState(monitor), + }; - return ( - { - navigate(`/monitors/${monitor._id}`); - }} - > - - - - - - - - - - - {monitor.type} - - - - - - ); - })} + return ( + { + navigate(`/monitors/${monitor._id}`); + }} + > + + + + + + + + + + + {monitor.type} + + + + + + ); + }) + ) : ( + + )}
@@ -415,14 +462,15 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }} />
- +
); }; MonitorTable.propTypes = { isAdmin: PropTypes.bool, filter: PropTypes.string, - setLoading: PropTypes.func, + setIsSearching: PropTypes.func, + isSearching: PropTypes.bool, }; const MemoizedMonitorTable = memo(MonitorTable); diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 0a3c1c912..345a0a88e 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -1,79 +1,74 @@ import "./index.css"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack } from "@mui/material"; import PropTypes from "prop-types"; import SkeletonLayout from "./skeleton"; import Fallback from "./fallback"; import StatusBox from "./StatusBox"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import Greeting from "../../../Utils/greeting"; -import MonitorTable from "./MonitorTable"; -import Search from "../../../Components/Inputs/Search"; -import useDebounce from "../../../Utils/debounce"; +import { CurrentMonitoring } from "./CurrentMonitoring"; const Monitors = ({ isAdmin }) => { const theme = useTheme(); const navigate = useNavigate(); const monitorState = useSelector((state) => state.uptimeMonitors); const authState = useSelector((state) => state.auth); - const [search, setSearch] = useState(""); - const [isSearching, setIsSearching] = useState(false); const dispatch = useDispatch({}); - const debouncedFilter = useDebounce(search, 500); - - const handleSearch = (value) => { - setIsSearching(true); - setSearch(value); - }; useEffect(() => { dispatch(getUptimeMonitorsByTeamId(authState.authToken)); }, [authState.authToken, dispatch]); - let loading = - monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0; + //TODO bring fetching to this component, like on pageSpeed + + const loading = monitorState?.isLoading; + + const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total; + + const hasMonitors = totalMonitors > 0; + const noMonitors = !hasMonitors; + const canAddMonitor = isAdmin && hasMonitors; return ( + + + + + {canAddMonitor && ( + + )} + + + {noMonitors && } {loading ? ( ) : ( <> - - - - - {isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && ( - - )} - - - {isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && ( - - )} - - {monitorState?.monitorsSummary?.monitors?.length !== 0 && ( + {hasMonitors && ( <> { value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0} /> - - - - Actively monitoring - - - {monitorState?.monitorsSummary?.monitorCounts?.total || 0} - - - - - - - {isSearching && ( - <> - - - - - - )} - - - + )} diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 8ecc4392f..30e11b2d7 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -19,7 +19,7 @@ const PageSpeed = ({ isAdmin }) => { const navigate = useNavigate(); const { user, authToken } = useSelector((state) => state.auth); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [monitors, setMonitors] = useState([]); useEffect(() => { dispatch(getPageSpeedByTeamId(authToken)); diff --git a/Client/src/Validation/error.js b/Client/src/Validation/error.js index e205ce7d1..28cc0baf9 100644 --- a/Client/src/Validation/error.js +++ b/Client/src/Validation/error.js @@ -22,4 +22,4 @@ const hasValidationErrors = (form, validation, setErrors) => { } return false; }; -export { buildErrors, hasValidationErrors }; +export { buildErrors, hasValidationErrors }; \ No newline at end of file diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 8978f5ffa..b610c5e22 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -166,5 +166,5 @@ export { monitorValidation, settingsValidation, maintenanceWindowValidation, - advancedSettingsValidation, + advancedSettingsValidation }; diff --git a/Docker/dist/build_images.sh b/Docker/dist/build_images.sh index 1df514e7e..a5ec3eec9 100755 --- a/Docker/dist/build_images.sh +++ b/Docker/dist/build_images.sh @@ -1,24 +1,26 @@ - #!/bin/bash # Change directory to root Server directory for correct Docker Context +cd "$(dirname "$0")" cd ../.. -#Client -client="./Docker/dist/client.Dockerfile" +# Define an array of services and their Dockerfiles +declare -A services=( + ["bluewave/uptime_client"]="./Docker/dist/client.Dockerfile" + ["bluewave/database_mongo"]="./Docker/dist/mongoDB.Dockerfile" + ["bluewave/uptime_redis"]="./Docker/dist/redis.Dockerfile" + ["bluewave/uptime_server"]="./Docker/dist/server.Dockerfile" +) -# MongoDB -mongoDB="./Docker/dist/mongoDB.Dockerfile" +# Loop through each service and build the corresponding image +for service in "${!services[@]}"; do + docker build -f "${services[$service]}" -t "$service" . + + # Check if the build succeeded + if [ $? -ne 0 ]; then + echo "Error building $service image. Exiting..." + exit 1 + fi +done -# Redis -redis="./Docker/dist/redis.Dockerfile" - -# Server -server="./Docker/dist/server.Dockerfile" - -docker build -f $client -t dist_uptime_client . -docker build -f $mongoDB -t dist_uptime_database_mongo . -docker build -f $redis -t dist_uptime_redis . -docker build -f $server -t dist_uptime_server . - -echo "All images built" \ No newline at end of file +echo "All images built successfully" \ No newline at end of file diff --git a/Docker/prod/build_images.sh b/Docker/prod/build_images.sh index 98d77e660..c879190bc 100755 --- a/Docker/prod/build_images.sh +++ b/Docker/prod/build_images.sh @@ -1,24 +1,26 @@ - #!/bin/bash # Change directory to root directory for correct Docker Context +cd "$(dirname "$0")" cd ../.. -#Client -client="./Docker/prod/client.Dockerfile" +# Define an array of services and their Dockerfiles +declare -A services=( + ["uptime_client"]="./Docker/prod/client.Dockerfile" + ["uptime_database_mongo"]="./Docker/prod/mongoDB.Dockerfile" + ["uptime_redis"]="./Docker/prod/redis.Dockerfile" + ["uptime_server"]="./Docker/prod/server.Dockerfile" +) -# MongoDB -mongoDB="./Docker/prod/mongoDB.Dockerfile" +# Loop through each service and build the corresponding image +for service in "${!services[@]}"; do + docker build -f "${services[$service]}" -t "$service" . + + # Check if the build succeeded + if [ $? -ne 0 ]; then + echo "Error building $service image. Exiting..." + exit 1 + fi +done -# Redis -redis="./Docker/prod/redis.Dockerfile" - -# Server -server="./Docker/prod/server.Dockerfile" - -docker build -f $client -t uptime_client . -docker build -f $mongoDB -t uptime_database_mongo . -docker build -f $redis -t uptime_redis . -docker build -f $server -t uptime_server . - -echo "All images built" \ No newline at end of file +echo "All images built successfully" \ No newline at end of file diff --git a/Server/db/models/HardwareCheck.js b/Server/db/models/HardwareCheck.js new file mode 100644 index 000000000..b710087bf --- /dev/null +++ b/Server/db/models/HardwareCheck.js @@ -0,0 +1,60 @@ +import mongoose from "mongoose"; + +const cpuSchema = mongoose.Schema({ + physical_core: { type: Number, default: 0 }, + logical_core: { type: Number, default: 0 }, + frequency: { type: Number, default: 0 }, + temperature: { type: Number, default: 0 }, + free_percent: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, +}); + +const memorySchema = mongoose.Schema({ + total_bytes: { type: Number, default: 0 }, + available_bytes: { type: Number, default: 0 }, + used_bytes: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, +}); + +const discSchema = mongoose.Schema({ + read_speed_bytes: { type: Number, default: 0 }, + write_speed_bytes: { type: Number, default: 0 }, + total_bytes: { type: Number, default: 0 }, + free_bytes: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, +}); + +const hostSchema = mongoose.Schema({ + os: { type: String, default: "" }, + platform: { type: String, default: "" }, + kernel_version: { type: String, default: "" }, +}); + +const HardwareCheckSchema = mongoose.Schema( + { + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + }, + cpu: { + type: cpuSchema, + default: () => ({}), + }, + memory: { + type: memorySchema, + default: () => ({}), + }, + disk: { + type: [discSchema], + default: () => [], + }, + host: { + type: hostSchema, + default: () => ({}), + }, + }, + { timestamps: true } +); + +export default mongoose.model("HardwareCheck", HardwareCheckSchema); diff --git a/Server/db/models/Monitor.js b/Server/db/models/Monitor.js index d1f0ef5a7..6d718e9cd 100644 --- a/Server/db/models/Monitor.js +++ b/Server/db/models/Monitor.js @@ -29,7 +29,7 @@ const MonitorSchema = mongoose.Schema( type: { type: String, required: true, - enum: ["http", "ping", "pagespeed"], + enum: ["http", "ping", "pagespeed", "hardware"], }, url: { type: String, diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 61e60e836..9031a741f 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -99,6 +99,11 @@ import { deletePageSpeedChecksByMonitorId, } from "./modules/pageSpeedCheckModule.js"; +//**************************************** +// Hardware Checks +//**************************************** +import { createHardwareCheck } from "./modules/hardwareCheckModule.js"; + //**************************************** // Checks //**************************************** @@ -179,6 +184,7 @@ export default { createPageSpeedCheck, getPageSpeedChecks, deletePageSpeedChecksByMonitorId, + createHardwareCheck, createMaintenanceWindow, getMaintenanceWindowsByTeamId, getMaintenanceWindowById, diff --git a/Server/db/mongo/modules/hardwareCheckModule.js b/Server/db/mongo/modules/hardwareCheckModule.js new file mode 100644 index 000000000..de46828e5 --- /dev/null +++ b/Server/db/mongo/modules/hardwareCheckModule.js @@ -0,0 +1,16 @@ +import HardwareCheck from "../../models/HardwareCheck.js"; +const SERVICE_NAME = "hardwareCheckModule"; +const createHardwareCheck = async (hardwareCheckData) => { + try { + const hardwareCheck = await new HardwareCheck({ + ...hardwareCheckData, + }).save(); + return hardwareCheck; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createHardwareCheck"; + throw error; + } +}; + +export { createHardwareCheck }; diff --git a/Server/index.js b/Server/index.js index 71dc42467..68a6c354a 100644 --- a/Server/index.js +++ b/Server/index.js @@ -34,9 +34,11 @@ import pkg from "handlebars"; const { compile } = pkg; import mjml2html from "mjml"; +// Settings Service and dependencies import SettingsService from "./service/settingsService.js"; +import AppSettings from "../db/models/AppSettings.js"; + import db from "./db/mongo/MongoDB.js"; -import { fetchMonitorCertificate } from "./controllers/controllerUtils.js"; const SERVICE_NAME = "Server"; let cleaningUp = false; @@ -142,7 +144,7 @@ const startApp = async () => { // Create services await connectDbAndRunServer(app, db); - const settingsService = new SettingsService(); + const settingsService = new SettingsService(AppSettings); await settingsService.loadSettings(); const emailService = new EmailService( diff --git a/Server/package-lock.json b/Server/package-lock.json index a226d79b8..eb2364064 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -36,7 +36,7 @@ "chai": "5.1.1", "esm": "3.2.25", "mocha": "10.7.3", - "nodemon": "3.1.0", + "nodemon": "3.1.7", "prettier": "^3.3.3", "sinon": "19.0.2" } @@ -4608,10 +4608,11 @@ } }, "node_modules/nodemon": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", - "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -4636,12 +4637,13 @@ } }, "node_modules/nodemon/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4653,10 +4655,11 @@ } }, "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/nopt": { "version": "5.0.0", diff --git a/Server/package.json b/Server/package.json index c46ee3e49..6d31c2165 100644 --- a/Server/package.json +++ b/Server/package.json @@ -39,7 +39,7 @@ "chai": "5.1.1", "esm": "3.2.25", "mocha": "10.7.3", - "nodemon": "3.1.0", + "nodemon": "3.1.7", "prettier": "^3.3.3", "sinon": "19.0.2" } diff --git a/Server/service/networkService.js b/Server/service/networkService.js index ad372dbce..e90180835 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -24,6 +24,7 @@ class NetworkService { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; + this.TYPE_HARDWARE = "hardware"; this.SERVICE_NAME = "NetworkService"; this.NETWORK_ERROR = 5000; this.axios = axios; @@ -293,6 +294,85 @@ class NetworkService { } } + async handleHardware(job) { + const url = job.data.url; + let isAlive; + //TODO Fetch hardware data + //For now, fake hardware data: + + const hardwareData = { + monitorId: job.data._id, + cpu: { + physical_core: 1, + logical_core: 1, + frequency: 266, + temperature: null, + free_percent: null, + usage_percent: null, + }, + memory: { + total_bytes: 4, + available_bytes: 4, + used_bytes: 2, + usage_percent: 0.5, + }, + disk: [ + { + read_speed_bytes: 3, + write_speed_bytes: 3, + total_bytes: 10, + free_bytes: 2, + usage_percent: 0.8, + }, + ], + host: { + os: "Linux", + platform: "Ubuntu", + kernel_version: "24.04", + }, + }; + try { + isAlive = true; + this.logAndStoreCheck(hardwareData, this.db.createHardwareCheck); + } catch (error) { + isAlive = false; + const nullData = { + monitorId: job.data._id, + cpu: { + physical_core: 0, + logical_core: 0, + frequency: 0, + temperature: 0, + free_percent: 0, + usage_percent: 0, + }, + memory: { + total_bytes: 0, + available_bytes: 0, + used_bytes: 0, + usage_percent: 0, + }, + disk: [ + { + read_speed_bytes: 0, + write_speed_bytes: 0, + total_bytes: 0, + free_bytes: 0, + usage_percent: 0, + }, + ], + host: { + os: "", + platform: "", + kernel_version: "", + }, + }; + this.logAndStoreCheck(nullData, this.db.createHardwareCheck); + } finally { + this.handleStatusUpdate(job, isAlive); + } + } + /** * Retrieves the status of a given job based on its type. * For unsupported job types, it logs an error and returns false. @@ -308,6 +388,8 @@ class NetworkService { return await this.handleHttp(job); case this.TYPE_PAGESPEED: return await this.handlePagespeed(job); + case this.TYPE_HARDWARE: + return await this.handleHardware(job); default: this.logger.error(`Unsupported type: ${job.data.type}`, { service: this.SERVICE_NAME, diff --git a/Server/service/settingsService.js b/Server/service/settingsService.js index 130de2934..9f44f7d07 100644 --- a/Server/service/settingsService.js +++ b/Server/service/settingsService.js @@ -1,4 +1,3 @@ -import AppSettings from "../db/models/AppSettings.js"; const SERVICE_NAME = "SettingsService"; const envConfig = { logLevel: undefined, @@ -30,7 +29,8 @@ class SettingsService { * Constructs a new SettingsService * @constructor * @throws {Error} - */ constructor() { + */ constructor(appSettings) { + this.appSettings = appSettings; this.settings = { ...envConfig }; } /** @@ -40,7 +40,7 @@ class SettingsService { * @throws Will throw an error if settings are not found in the database or if settings have not been loaded. */ async loadSettings() { try { - const dbSettings = await AppSettings.findOne(); + const dbSettings = await this.appSettings.findOne(); if (!this.settings) { throw new Error("Settings not found"); } @@ -51,10 +51,6 @@ class SettingsService { this.settings[key] = dbSettings[key]; } } - - if (!this.settings) { - throw new Error("Settings not found"); - } return this.settings; } catch (error) { error.service === undefined ? (error.service = SERVICE_NAME) : null; diff --git a/Server/tests/services/networkService.test.js b/Server/tests/services/networkService.test.js index 619fa0fc9..5d3e81a22 100644 --- a/Server/tests/services/networkService.test.js +++ b/Server/tests/services/networkService.test.js @@ -625,6 +625,105 @@ describe("networkService - handlePagespeed", () => { }); }); +describe("networkService - handleHardware", () => { + let dbMock, + axiosMock, + jobMock, + emailServiceMock, + pingMock, + loggerMock, + httpMock, + networkService, + logAndStoreCheckStub, + handleStatusUpdateStub; + beforeEach(() => { + jobMock = { + data: { + _id: "12345", + url: "http://example.com", + }, + }; + dbMock = { getMonitorById: sinon.stub() }; + axiosMock = { get: sinon.stub() }; + + emailServiceMock = sinon.stub(); + pingMock = { promise: { probe: sinon.stub() } }; + loggerMock = { error: sinon.stub() }; + httpMock = { + STATUS_CODES: { + 200: "OK", + 500: "Internal Server Error", + }, + }; + networkService = new NetworkService( + dbMock, + emailServiceMock, + axiosMock, + pingMock, + loggerMock, + httpMock + ); + logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck").resolves(); + handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate").resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should handle a successful Hardware response", async () => { + const responseMock = { + monitorId: jobMock.data._id, + cpu: { + physical_core: 1, + logical_core: 1, + frequency: 266, + temperature: null, + free_percent: null, + usage_percent: null, + }, + memory: { + total_bytes: 4, + available_bytes: 4, + used_bytes: 2, + usage_percent: 0.5, + }, + disk: [ + { + read_speed_bytes: 3, + write_speed_bytes: 3, + total_bytes: 10, + free_bytes: 2, + usage_percent: 0.8, + }, + ], + host: { + os: "Linux", + platform: "Ubuntu", + kernel_version: "24.04", + }, + }; + axiosMock.get.resolves(responseMock); + + await networkService.handleHardware(jobMock); + expect(networkService.logAndStoreCheck.calledOnce).to.be.true; + const hardwareData = networkService.logAndStoreCheck.getCall(0).args[0]; + expect(hardwareData.cpu).to.include({ + ...responseMock.cpu, + }); + expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true; + }); + + it("should handle an error Hardware response", async () => { + logAndStoreCheckStub.throws(new Error("Hardware error")); + try { + await networkService.handleHardware(jobMock); + } catch (error) { + expect(error.message).to.equal("Hardware error"); + } + }); +}); + describe("NetworkService - getStatus", () => { let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock, networkService; @@ -685,6 +784,18 @@ describe("NetworkService - getStatus", () => { const result = await networkService.getStatus(job); expect(result).to.be.false; }); + it("should return true if the job type is hardware and handleHardware is successful", async () => { + const job = { data: { type: networkService.TYPE_HARDWARE } }; + sinon.stub(networkService, "handleHardware").resolves(true); + const result = await networkService.getStatus(job); + expect(result).to.be.true; + }); + it("should return false if the job type is hardware and handleHardware is not successful", async () => { + const job = { data: { type: networkService.TYPE_HARDWARE } }; + sinon.stub(networkService, "handleHardware").resolves(false); + const result = await networkService.getStatus(job); + expect(result).to.be.false; + }); it("should log an error and return false if the job type is unknown", async () => { const job = { data: { type: "unknown" } }; const result = await networkService.getStatus(job); diff --git a/Server/tests/services/settingsService.test.js b/Server/tests/services/settingsService.test.js new file mode 100644 index 000000000..3060758f7 --- /dev/null +++ b/Server/tests/services/settingsService.test.js @@ -0,0 +1,134 @@ +import sinon from "sinon"; +import SettingsService from "../../service/settingsService.js"; +import { expect } from "chai"; +import NetworkService from "../../service/networkService.js"; +const SERVICE_NAME = "SettingsService"; + +describe("SettingsService", () => { + let sandbox, mockAppSettings; + beforeEach(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(process.env, "CLIENT_HOST").value("http://localhost"); + sandbox.stub(process.env, "JWT_SECRET").value("secret"); + sandbox.stub(process.env, "REFRESH_TOKEN_SECRET").value("refreshSecret"); + sandbox.stub(process.env, "DB_TYPE").value("postgres"); + sandbox + .stub(process.env, "DB_CONNECTION_STRING") + .value("postgres://user:pass@localhost/db"); + sandbox.stub(process.env, "REDIS_HOST").value("localhost"); + sandbox.stub(process.env, "REDIS_PORT").value("6379"); + sandbox.stub(process.env, "TOKEN_TTL").value("3600"); + sandbox.stub(process.env, "REFRESH_TOKEN_TTL").value("86400"); + sandbox.stub(process.env, "PAGESPEED_API_KEY").value("apiKey"); + sandbox.stub(process.env, "SYSTEM_EMAIL_HOST").value("smtp.mailtrap.io"); + sandbox.stub(process.env, "SYSTEM_EMAIL_PORT").value("2525"); + sandbox.stub(process.env, "SYSTEM_EMAIL_ADDRESS").value("test@example.com"); + sandbox.stub(process.env, "SYSTEM_EMAIL_PASSWORD").value("password"); + }); + + mockAppSettings = { + settingOne: 123, + settingTwo: 456, + }; + + afterEach(function () { + sandbox.restore(); + sinon.restore(); + }); + describe("constructor", () => { + it("should construct a new SettingsService", () => { + const settingsService = new SettingsService(mockAppSettings); + expect(settingsService.appSettings).to.equal(mockAppSettings); + }); + }); + describe("loadSettings", () => { + it("should load settings from DB when environment variables are not set", async () => { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = {}; + const result = await settingsService.loadSettings(); + expect(result).to.deep.equal(dbSettings); + }); + it("should throw an error if settings are not found", async function () { + const appSettings = { findOne: sinon.stub().returns(null) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = null; + + try { + await settingsService.loadSettings(); + } catch (error) { + expect(error.message).to.equal("Settings not found"); + expect(error.service).to.equal(SERVICE_NAME); + expect(error.method).to.equal("loadSettings"); + } + }); + + it("should add its method and service name to error if not present", async () => { + const appSettings = { findOne: sinon.stub().throws(new Error("Test error")) }; + const settingsService = new SettingsService(appSettings); + try { + await settingsService.loadSettings(); + } catch (error) { + expect(error.message).to.equal("Test error"); + expect(error.service).to.equal(SERVICE_NAME); + expect(error.method).to.equal("loadSettings"); + } + }); + it("should not add its method and service name to error if present", async () => { + const error = new Error("Test error"); + error.method = "otherMethod"; + error.service = "OTHER_SERVICE"; + const appSettings = { findOne: sinon.stub().throws(error) }; + const settingsService = new SettingsService(appSettings); + try { + await settingsService.loadSettings(); + } catch (error) { + console.log(error); + expect(error.message).to.equal("Test error"); + expect(error.service).to.equal("OTHER_SERVICE"); + expect(error.method).to.equal("otherMethod"); + } + }); + it("should merge DB settings with environment variables", async function () { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + const result = await settingsService.loadSettings(); + expect(result).to.deep.equal(settingsService.settings); + expect(settingsService.settings.logLevel).to.equal("debug"); + expect(settingsService.settings.apiBaseUrl).to.equal("http://localhost"); + }); + }); + describe("reloadSettings", () => { + it("should call loadSettings", async () => { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = {}; + const result = await settingsService.reloadSettings(); + expect(result).to.deep.equal(dbSettings); + }); + }); + describe("getSettings", () => { + it("should return the current settings", () => { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = dbSettings; + const result = settingsService.getSettings(); + expect(result).to.deep.equal(dbSettings); + }); + it("should throw an error if settings have not been loaded", () => { + const appSettings = { findOne: sinon.stub().returns(null) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = null; + + try { + settingsService.getSettings(); + } catch (error) { + expect(error.message).to.equal("Settings have not been loaded"); + } + }); + }); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..f71a78c1f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "bluewave-uptime", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..5db72dd6a --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +}