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
-
-
-
-
- );
+ 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"
+ ]
+}