Merge branch 'develop' into 913-fe-advanced-settings-page-validation-and-error-handling

This commit is contained in:
Shemy Gan
2024-10-22 09:37:39 -04:00
30 changed files with 1289 additions and 776 deletions

329
Client/package-lock.json generated
View File

@@ -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"

View File

@@ -38,6 +38,7 @@ const SearchAdornment = () => {
);
};
//TODO keep search state inside of component
const Search = ({
id,
options,

View File

@@ -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 (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
>
<ConfigBox>
<Box>
<Typography component="h1">Client settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify client settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
id="apiBaseUrl"
label="API URL Host"
value={localSettings.apiBaseUrl}
onChange={handleChange}
error={errors.apiBaseUrl}
/>
<Select
id="logLevel"
label="Log level"
name="logLevel"
items={logItems}
value={logItemLookup[localSettings.logLevel]}
onChange={handleLogLevel}
error={errors.logLevel}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Email settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Set your host email settings here. These settings are used for
sending system emails.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="systemEmailHost"
label="Email host"
name="systemEmailHost"
value={localSettings.systemEmailHost}
onChange={handleChange}
error={errors.systemEmailHost}
/>
<Field
type="number"
id="systemEmailPort"
label="System email address"
name="systemEmailPort"
value={localSettings.systemEmailPort.toString()}
onChange={handleChange}
error={errors.systemEmailPort}
/>
<Field
type="email"
id="systemEmailAddress"
label="System email address"
name="systemEmailAddress"
value={localSettings.systemEmailAddress}
onChange={handleChange}
error={errors.systemEmailAddress}
/>
<Field
type="text"
id="systemEmailPassword"
label="System email password"
name="systemEmailPassword"
value={localSettings.systemEmailPassword}
onChange={handleChange}
error={errors.systemEmailPassword}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Server settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify server settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="jwtTTL"
label="JWT time to live"
name="jwtTTL"
value={localSettings.jwtTTL}
onChange={handleChange}
error={errors.jwtTTL}
/>
<Field
type="text"
id="dbType"
label="Database type"
name="dbType"
value={localSettings.dbType}
onChange={handleChange}
error={errors.dbType}
/>
<Field
type="text"
id="redisHost"
label="Redis host"
name="redisHost"
value={localSettings.redisHost}
onChange={handleChange}
error={errors.redisHost}
/>
<Field
type="number"
id="redisPort"
label="Redis port"
name="redisPort"
value={localSettings.redisPort.toString()}
onChange={handleChange}
error={errors.redisPort}
/>
<Field
type="text"
id="pagespeedApiKey"
label="PageSpeed API key"
name="pagespeedApiKey"
value={localSettings.pagespeedApiKey}
onChange={handleChange}
error={errors.pagespeedApiKey}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
</Box>
<Box>
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
<Typography
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
>
Developed by Bluewave Labs.
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs"
label="https://github.com/bluewave-labs"
/>
</Box>
</ConfigBox>
<Stack direction="row" justifyContent="flex-end">
<LoadingButton
loading={settings.isLoading || settings.authIsLoading}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
Save
</LoadingButton>
</Stack>
</Stack>
</Box>
);
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
>
<ConfigBox>
<Box>
<Typography component="h1">Client settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify client settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
id="apiBaseUrl"
label="API URL Host"
value={localSettings.apiBaseUrl}
onChange={handleChange}
/>
<Select
id="logLevel"
label="Log level"
name="logLevel"
items={logItems}
value={logItemLookup[localSettings.logLevel]}
onChange={handleLogLevel}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Email settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Set your host email settings here. These settings are used for sending
system emails.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="systemEmailHost"
label="Email host"
name="systemEmailHost"
value={localSettings.systemEmailHost}
onChange={handleChange}
/>
<Field
type="number"
id="systemEmailPort"
label="System email address"
name="systemEmailPort"
value={localSettings.systemEmailPort.toString()}
onChange={handleChange}
/>
<Field
type="email"
id="systemEmailAddress"
label="System email address"
name="systemEmailAddress"
value={localSettings.systemEmailAddress}
onChange={handleChange}
/>
<Field
type="text"
id="systemEmailPassword"
label="System email password"
name="systemEmailPassword"
value={localSettings.systemEmailPassword}
onChange={handleChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Server settings</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Modify server settings here.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="jwtTTL"
label="JWT time to live"
name="jwtTTL"
value={localSettings.jwtTTL}
onChange={handleChange}
/>
<Field
type="text"
id="dbType"
label="Database type"
name="dbType"
value={localSettings.dbType}
onChange={handleChange}
/>
<Field
type="text"
id="redisHost"
label="Redis host"
name="redisHost"
value={localSettings.redisHost}
onChange={handleChange}
/>
<Field
type="number"
id="redisPort"
label="Redis port"
name="redisPort"
value={localSettings.redisPort.toString()}
onChange={handleChange}
/>
<Field
type="text"
id="pagespeedApiKey"
label="PageSpeed API key"
name="pagespeedApiKey"
value={localSettings.pagespeedApiKey}
onChange={handleChange}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">About</Typography>
</Box>
<Box>
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
Developed by Bluewave Labs.
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs"
label="https://github.com/bluewave-labs"
/>
</Box>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<LoadingButton
loading={settings.isLoading || settings.authIsLoading}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
Save
</LoadingButton>
</Stack>
</Stack>
</Box>
);
};
AdvancedSettings.propTypes = {

View File

@@ -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 (
<Box sx={{ ...styles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.text.secondary}
>
No incidents recorded yet.
</Typography>
</Box>
);
};
Empty.propTypes = {
styles: PropTypes.object,
mode: PropTypes.string,
};
export { Empty };

View File

@@ -0,0 +1,21 @@
import { Skeleton /* , Stack */ } from "@mui/material";
const IncidentSkeleton = () => {
return (
<>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={100}
/>
</>
);
};
export { IncidentSkeleton };

View File

@@ -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" ? (
<Box sx={{ ...sharedStyles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.text.secondary}
>
No incidents recorded yet.
</Typography>
</Box>
) : checks?.length === 0 ? (
{isLoading ? (
<IncidentSkeleton />
) : noIncidentsRecordedYet ? (
<Empty
mode={mode}
styles={sharedStyles}
/>
) : noIncidentsForThatMonitor ? (
<Box sx={{ ...sharedStyles }}>
<Box
textAlign="center"

View File

@@ -17,56 +17,62 @@ const Incidents = () => {
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 (
<Stack
className="incidents table-container"
className="incidents"
pt={theme.spacing(6)}
gap={theme.spacing(12)}
>
{loading ? (
{isActuallyLoading ? (
<SkeletonLayout />
) : (
<>

View File

@@ -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];

View File

@@ -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 (
<Box
flex={1}
px={theme.spacing(10)}
py={theme.spacing(8)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
component="h2"
variant="h2"
fontWeight={500}
letterSpacing={-0.2}
>
Actively monitoring
</Typography>
<Box
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{totalMonitors}
</Box>
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitors}
filteredBy="name"
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
</Stack>
<MemoizedMonitorTable
isAdmin={isAdmin}
filter={debouncedFilter}
setIsSearching={setIsSearching}
isSearching={isSearching}
/>
</Box>
);
};
CurrentMonitoring.propTypes = {
totalMonitors: PropTypes.number,
monitors: PropTypes.array,
isAdmin: PropTypes.bool,
};
export { CurrentMonitoring };

View File

@@ -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) => (
<TableRow key={row}>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
</TableRow>
))}
</>
);
};
export { TableBodySkeleton };

View File

@@ -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 (
<>
<Box position="relative">
{isSearching && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.background.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="20%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.other.icon,
}}
/>
</Box>
</>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
@@ -271,77 +313,82 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
</TableRow>
</TableHead>
<TableBody>
{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 (
<TableRow
key={monitor._id}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
}}
>
<TableCell>
<Host
key={monitor._id}
params={params}
/>
</TableCell>
<TableCell>
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
<BarChart checks={monitor.checks.slice().reverse()} />
</TableCell>
<TableCell>
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
</TableCell>
<TableCell>
<ActionsMenu
monitor={monitor}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</TableCell>
</TableRow>
);
})}
return (
<TableRow
key={monitor._id}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
}}
>
<TableCell>
<Host
key={monitor._id}
params={params}
/>
</TableCell>
<TableCell>
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
<BarChart checks={monitor.checks.slice().reverse()} />
</TableCell>
<TableCell>
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
</TableCell>
<TableCell>
<ActionsMenu
monitor={monitor}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</TableCell>
</TableRow>
);
})
) : (
<TableBodySkeleton />
)}
</TableBody>
</Table>
</TableContainer>
@@ -415,14 +462,15 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
}}
/>
</Stack>
</>
</Box>
);
};
MonitorTable.propTypes = {
isAdmin: PropTypes.bool,
filter: PropTypes.string,
setLoading: PropTypes.func,
setIsSearching: PropTypes.func,
isSearching: PropTypes.bool,
};
const MemoizedMonitorTable = memo(MonitorTable);

View File

@@ -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 (
<Stack
className="monitors table-container"
className="monitors"
gap={theme.spacing(8)}
>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
<Greeting type="uptime" />
{canAddMonitor && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{loading ? (
<SkeletonLayout />
) : (
<>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Greeting type="uptime" />
{isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
<Fallback isAdmin={isAdmin} />
)}
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
{hasMonitors && (
<>
<Stack
gap={theme.spacing(8)}
@@ -93,88 +88,11 @@ const Monitors = ({ isAdmin }) => {
value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0}
/>
</Stack>
<Box
flex={1}
px={theme.spacing(10)}
py={theme.spacing(8)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
component="h2"
variant="h2"
fontWeight={500}
letterSpacing={-0.2}
>
Actively monitoring
</Typography>
<Box
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{monitorState?.monitorsSummary?.monitorCounts?.total || 0}
</Box>
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitorState?.monitorsSummary?.monitors ?? []}
filteredBy="name"
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
</Stack>
<Box position="relative">
{isSearching && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.background.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="20%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.other.icon,
}}
/>
</Box>
</>
)}
<MonitorTable
isAdmin={isAdmin}
filter={debouncedFilter}
setLoading={setIsSearching}
/>
</Box>
</Box>
<CurrentMonitoring
isAdmin={isAdmin}
monitors={monitorState.monitorsSummary.monitors}
totalMonitors={totalMonitors}
/>
</>
)}
</>

View File

@@ -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));

View File

@@ -22,4 +22,4 @@ const hasValidationErrors = (form, validation, setErrors) => {
}
return false;
};
export { buildErrors, hasValidationErrors };
export { buildErrors, hasValidationErrors };

View File

@@ -166,5 +166,5 @@ export {
monitorValidation,
settingsValidation,
maintenanceWindowValidation,
advancedSettingsValidation,
advancedSettingsValidation
};

View File

@@ -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"
echo "All images built successfully"

View File

@@ -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"
echo "All images built successfully"

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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(

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);

View File

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

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "bluewave-uptime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

6
renovate.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}