diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
index 3d9b31e2d..b966a8cc6 100644
--- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -1,6 +1,8 @@
+**(when sending your PR, only remove this line. Do not remove any lines below. Check all the items before sending your PR)**
+
## Describe your changes
-Provide a brief description of the changes you’ve made and their purpose.
+Briefly describe the changes you made and their purpose.
## Issue number
@@ -8,7 +10,13 @@ Mention the issue number(s) this PR addresses (e.g., #123).
## Please ensure all items are checked off before requesting a review:
-- [ ] I have performed a self-review of my code.
+- [ ] I deployed the application locally.
+- [ ] I have performed a self-review and testing of my code.
- [ ] I have included the issue # in the PR.
- [ ] I have labelled the PR correctly.
-- [ ] My PR is granular and targeted to one specific feature only.
+- [ ] The issue I am working on is assigned to me.
+- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
+- [ ] I made sure font sizes, color choices etc are all referenced from the theme.
+- [ ] My PR is granular and targeted to one specific feature.
+- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
+
diff --git a/Client/package-lock.json b/Client/package-lock.json
index 135c3aab7..603e30ae0 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -11,12 +11,12 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
- "@mui/icons-material": "6.1.10",
- "@mui/lab": "6.0.0-beta.18",
- "@mui/material": "6.1.10",
+ "@mui/icons-material": "6.2.0",
+ "@mui/lab": "6.0.0-beta.19",
+ "@mui/material": "6.2.0",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.23.1",
- "@mui/x-date-pickers": "7.23.1",
+ "@mui/x-data-grid": "7.23.2",
+ "@mui/x-date-pickers": "7.23.2",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
@@ -28,7 +28,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
- "recharts": "2.14.1",
+ "recharts": "2.15.0",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
@@ -1071,15 +1071,15 @@
}
},
"node_modules/@mui/base": {
- "version": "5.0.0-beta.64",
- "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.64.tgz",
- "integrity": "sha512-nu663PoZs/Pee0fkPYkjUADfT+AAi2QWvvHghDhLeSx8sa3i+GGaOoUsFmB4CPlyYqWfq9hRGA7H1T3d6VrGgw==",
+ "version": "5.0.0-beta.66",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.66.tgz",
+ "integrity": "sha512-1SzcNbtIms0o/Dx+599B6QbvR5qUMBUjwc2Gs47h1HsF7RcEFXxqaq7zrWkIWbvGctIIPx0j330oGx/SkF+UmA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@floating-ui/react-dom": "^2.1.1",
"@mui/types": "^7.2.19",
- "@mui/utils": "^6.1.10",
+ "@mui/utils": "^6.2.0",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
@@ -1092,9 +1092,9 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0",
- "react": "^17.0.0 || ^18.0.0",
- "react-dom": "^17.0.0 || ^18.0.0"
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -1103,9 +1103,9 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "6.1.10",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.10.tgz",
- "integrity": "sha512-LY5wdiLCBDY7u+Od8UmFINZFGN/5ZU90fhAslf/ZtfP+5RhuY45f679pqYIxe0y54l6Gkv9PFOc8Cs10LDTBYg==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.0.tgz",
+ "integrity": "sha512-Nn5PSkUqbDrvezpiiiYZiAbX4SFEiy3CbikUL6pWOXEUsq+L1j50OOyyUIHpaX2Hr+5V5UxTh+fPeC4nsGNhdw==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -1113,9 +1113,9 @@
}
},
"node_modules/@mui/icons-material": {
- "version": "6.1.10",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.10.tgz",
- "integrity": "sha512-G6P1BCSt6EQDcKca47KwvKjlqgOXFbp2I3oWiOlFgKYTANBH89yk7ttMQ5ysqNxSYAB+4TdM37MlPYp4+FkVrQ==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.0.tgz",
+ "integrity": "sha512-WR1EEhGOSvxAsoTSzWZBlrWFjul8wziDrII4rC3PvMBHhBYBqEc2n/0aamfFbwkH5EiYb96aqc6kYY6tB310Sw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0"
@@ -1128,7 +1128,7 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@mui/material": "^6.1.10",
+ "@mui/material": "^6.2.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -1139,16 +1139,16 @@
}
},
"node_modules/@mui/lab": {
- "version": "6.0.0-beta.18",
- "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.18.tgz",
- "integrity": "sha512-O7jNn36Jb0530NOZeFLj33RGB57x3kfyiYOaj5sL/j/Pmq9T0tonKMkoW/AUCucmBa7RuEzEYMyqBpfqminebA==",
+ "version": "6.0.0-beta.19",
+ "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.19.tgz",
+ "integrity": "sha512-t7iub8kjpLdA5uDGwGnNRjtGc1vYEUnDwSROjKrnYqjOlCQhBajFa8uoQtaST6jE/VEk6cxpDMnN5MalC6YpCg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/base": "5.0.0-beta.64",
- "@mui/system": "^6.1.10",
+ "@mui/base": "5.0.0-beta.66",
+ "@mui/system": "^6.2.0",
"@mui/types": "^7.2.19",
- "@mui/utils": "^6.1.10",
+ "@mui/utils": "^6.2.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
@@ -1162,8 +1162,8 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@mui/material": "^6.1.10",
- "@mui/material-pigment-css": "^6.1.10",
+ "@mui/material": "^6.2.0",
+ "@mui/material-pigment-css": "^6.2.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1184,22 +1184,22 @@
}
},
"node_modules/@mui/material": {
- "version": "6.1.10",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.10.tgz",
- "integrity": "sha512-txnwYObY4N9ugv5T2n5h1KcbISegZ6l65w1/7tpSU5OB6MQCU94YkP8n/3slDw2KcEfRk4+4D8EUGfhSPMODEQ==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.0.tgz",
+ "integrity": "sha512-7FXXUPIyYzP02a7GvqwJ7ocmdP+FkvLvmy/uxG1TDmTlsr8nEClQp75uxiVznJqAY/jJy4d+Rj/fNWNxwidrYQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/core-downloads-tracker": "^6.1.10",
- "@mui/system": "^6.1.10",
+ "@mui/core-downloads-tracker": "^6.2.0",
+ "@mui/system": "^6.2.0",
"@mui/types": "^7.2.19",
- "@mui/utils": "^6.1.10",
+ "@mui/utils": "^6.2.0",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
- "react-is": "^18.3.1",
+ "react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
@@ -1212,7 +1212,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^6.1.10",
+ "@mui/material-pigment-css": "^6.2.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1232,14 +1232,20 @@
}
}
},
+ "node_modules/@mui/material/node_modules/react-is": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
+ "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
+ "license": "MIT"
+ },
"node_modules/@mui/private-theming": {
- "version": "6.1.10",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.10.tgz",
- "integrity": "sha512-DqgsH0XFEweeG3rQfVkqTkeXcj/E76PGYWag8flbPdV8IYdMo+DfVdFlZK8JEjsaIVD2Eu1kJg972XnH5pfnBQ==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.0.tgz",
+ "integrity": "sha512-lYd2MrVddhentF1d/cMXKnwlDjr/shbO3A2eGq22PCYUoZaqtAGZMc0U86KnJ/Sh5YzNYePqTOaaowAN8Qea8A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/utils": "^6.1.10",
+ "@mui/utils": "^6.2.0",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1260,9 +1266,9 @@
}
},
"node_modules/@mui/styled-engine": {
- "version": "6.1.10",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.10.tgz",
- "integrity": "sha512-+NV9adKZYhslJ270iPjf2yzdVJwav7CIaXcMlPSi1Xy1S/zRe5xFgZ6BEoMdmGRpr34lIahE8H1acXP2myrvRw==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.0.tgz",
+ "integrity": "sha512-rV4YCu6kcCjMnHFXU/tQcL6XfYVfFVR8n3ZVNGnk2rpXnt/ctOPJsF+eUQuhkHciueLVKpI06+umr1FxWWhVmQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
@@ -1294,16 +1300,16 @@
}
},
"node_modules/@mui/system": {
- "version": "6.1.10",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.10.tgz",
- "integrity": "sha512-5YNIqxETR23SIkyP7MY2fFnXmplX/M4wNi2R+10AVRd3Ub+NLctWY/Vs5vq1oAMF0eSDLhRTGUjaUe+IGSfWqg==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.0.tgz",
+ "integrity": "sha512-DCeqev9Cd4f4pm3O1lqSGW/DIHHBG6ZpqMX9iIAvN4asYv+pPWv2/lKov9kWk5XThhxFnGSv93SRNE1kNRRg5Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/private-theming": "^6.1.10",
- "@mui/styled-engine": "^6.1.10",
+ "@mui/private-theming": "^6.2.0",
+ "@mui/styled-engine": "^6.2.0",
"@mui/types": "^7.2.19",
- "@mui/utils": "^6.1.10",
+ "@mui/utils": "^6.2.0",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -1348,17 +1354,17 @@
}
},
"node_modules/@mui/utils": {
- "version": "6.1.10",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.10.tgz",
- "integrity": "sha512-1ETuwswGjUiAf2dP9TkBy8p49qrw2wXa+RuAjNTRE5+91vtXJ1HKrs7H9s8CZd1zDlQVzUcUAPm9lpQwF5ogTw==",
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.0.tgz",
+ "integrity": "sha512-77CaFJi+OIi2SjbPwCis8z5DXvE0dfx9hBz5FguZHt1VYFlWEPCWTHcMsQCahSErnfik5ebLsYK8+D+nsjGVfw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/types": "^7.2.19",
- "@types/prop-types": "^15.7.13",
+ "@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
- "react-is": "^18.3.1"
+ "react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
@@ -1377,10 +1383,16 @@
}
}
},
+ "node_modules/@mui/utils/node_modules/react-is": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
+ "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
+ "license": "MIT"
+ },
"node_modules/@mui/x-charts": {
- "version": "7.23.1",
- "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.23.1.tgz",
- "integrity": "sha512-hIVRitRSFxf60BHDr+T7Tarj4lLwOV8flebeIsyEPJJHvSlaFNFwr6rqYFb8SGULYU7ppKMhiESD1+YqgJSU1Q==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.23.2.tgz",
+ "integrity": "sha512-wLeogvQZZtyrAOdG06mDzIQSHBSAB09Uy16AYRUcMxVObi7Fs0i3TJUMpQHMYz1/1DvE1u8zstDgVpVfk8/iCA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1436,9 +1448,9 @@
}
},
"node_modules/@mui/x-data-grid": {
- "version": "7.23.1",
- "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.1.tgz",
- "integrity": "sha512-1CumfhjGA+WgNtnN3ZD3gCOw6RVf/cEy2X2gS0iyXtVsFNaE+qtyMz9KesQsIzi9u13Wj3RNuhBdH0C5qXtAhQ==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.2.tgz",
+ "integrity": "sha512-Xhm4Bh+WiU5MSLFIZ8mE1egHoS0xPEVlwvjYvairPIkxNPubB7f+gtLkuAJ2+m+BYbgO7yDte+Pp7dfjkU+vOA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -1473,9 +1485,9 @@
}
},
"node_modules/@mui/x-date-pickers": {
- "version": "7.23.1",
- "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.1.tgz",
- "integrity": "sha512-Jr4beZ7r2lvWBaFnkIAg9BgiNFcfeJy4AUe3MbG10BBSZyB++odGqhOUAIGqkP7MpUzEGlTv4NUaaD7gYTAQPg==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.2.tgz",
+ "integrity": "sha512-Kt9VsEnShaBKiaastTYku66UIWptgc9UMA16d0G/0TkfIsvZrAD3iacQR6HHAXWspaFshdfsRmW2JAoFhzKZsg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -2354,15 +2366,15 @@
"license": "MIT"
},
"node_modules/@types/prop-types": {
- "version": "15.7.13",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
- "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "18.3.16",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz",
- "integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==",
+ "version": "18.3.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.17.tgz",
+ "integrity": "sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -2370,9 +2382,9 @@
}
},
"node_modules/@types/react-dom": {
- "version": "18.3.4",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.4.tgz",
- "integrity": "sha512-HMqjZ3RJxmfoOn3sVc0T0fO6qSrLDnUQYB+azHKdBBz1eOt0Sq4wUu2Sf1/Dt18gqhECg0fZ33tkh/6YdwrSCw==",
+ "version": "18.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
+ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -5442,9 +5454,9 @@
}
},
"node_modules/recharts": {
- "version": "2.14.1",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.14.1.tgz",
- "integrity": "sha512-xtWulflkA+/xu4/QClBdtZYN30dbvTHjxjkh5XTMrH/CQ3WGDDPHHa/LLKCbgoqz0z3UaSH2/blV1i6VNMeh1g==",
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
+ "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
@@ -5460,8 +5472,8 @@
"node": ">=14"
},
"peerDependencies": {
- "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts-scale": {
diff --git a/Client/package.json b/Client/package.json
index cece1c8e3..b1a8409cf 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -14,12 +14,12 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
- "@mui/icons-material": "6.1.10",
- "@mui/lab": "6.0.0-beta.18",
- "@mui/material": "6.1.10",
+ "@mui/icons-material": "6.2.0",
+ "@mui/lab": "6.0.0-beta.19",
+ "@mui/material": "6.2.0",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.23.1",
- "@mui/x-date-pickers": "7.23.1",
+ "@mui/x-data-grid": "7.23.2",
+ "@mui/x-date-pickers": "7.23.2",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
@@ -31,7 +31,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
- "recharts": "2.14.1",
+ "recharts": "2.15.0",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
diff --git a/Client/src/App.jsx b/Client/src/App.jsx
index a248dd5b0..3b28e285a 100644
--- a/Client/src/App.jsx
+++ b/Client/src/App.jsx
@@ -1,37 +1,8 @@
import { useEffect } from "react";
-import { Routes, Route, Navigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
-import NotFound from "./Pages/NotFound";
-import Login from "./Pages/Auth/Login/Login";
-import Register from "./Pages/Auth/Register/Register";
-import Account from "./Pages/Account";
-import Monitors from "./Pages/Monitors/Home";
-import CreateMonitor from "./Pages/Monitors/CreateMonitor";
-import CreateInfrastructureMonitor from "./Pages/Infrastructure/CreateMonitor";
-import Incidents from "./Pages/Incidents";
-import Status from "./Pages/Status";
-import Integrations from "./Pages/Integrations";
-import Settings from "./Pages/Settings";
-import ForgotPassword from "./Pages/Auth/ForgotPassword";
-import CheckEmail from "./Pages/Auth/CheckEmail";
-import SetNewPassword from "./Pages/Auth/SetNewPassword";
-import NewPasswordConfirmed from "./Pages/Auth/NewPasswordConfirmed";
-import ProtectedRoute from "./Components/ProtectedRoute";
-import Details from "./Pages/Monitors/Details";
-import AdvancedSettings from "./Pages/AdvancedSettings";
-import Maintenance from "./Pages/Maintenance";
-import Configure from "./Pages/Monitors/Configure";
-import PageSpeed from "./Pages/PageSpeed";
-import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
-import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenance";
-import PageSpeedDetails from "./Pages/PageSpeed/Details";
-import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
-import HomeLayout from "./Components/Layouts/HomeLayout";
-import withAdminCheck from "./Components/HOC/withAdminCheck";
-import withAdminProp from "./Components/HOC/withAdminProp";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
@@ -39,18 +10,9 @@ import { CssBaseline, GlobalStyles } from "@mui/material";
import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
-import { Infrastructure } from "./Pages/Infrastructure";
-import InfrastructureDetails from "./Pages/Infrastructure/Details";
+import { Routes } from "./Routes";
+
function App() {
- const AdminCheckedRegister = withAdminCheck(Register);
- const MonitorsWithAdminProp = withAdminProp(Monitors);
- const MonitorDetailsWithAdminProp = withAdminProp(Details);
- const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
- const PageSpeedDetailsWithAdminProp = withAdminProp(PageSpeedDetails);
- const MaintenanceWithAdminProp = withAdminProp(Maintenance);
- const SettingsWithAdminProp = withAdminProp(Settings);
- const AdvancedSettingsWithAdminProp = withAdminProp(AdvancedSettings);
- const InfrastructureDetailsWithAdminProp = withAdminProp(InfrastructureDetails);
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
@@ -82,161 +44,7 @@ function App() {
};
}}
/>
- {/* Extract Routes to Routes */}
-
- }
- >
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
-
- }
- />
-
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
-
- }
- />
-
- }
- />
-
- }
- />
-
-
- }
- />
-
- }
- />
-
- }
- />
-
- }
- />
-
- }
- />
- }
- />
- }
- />
- }
- />
-
+
);
diff --git a/Client/src/Components/HOC/withAdminProp.jsx b/Client/src/Components/HOC/withAdminProp.jsx
deleted file mode 100644
index b55893119..000000000
--- a/Client/src/Components/HOC/withAdminProp.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useSelector } from "react-redux";
-
-const withAdminProp = (WrappedComponent) => {
- const WithAdminProp = (props) => {
- const { user } = useSelector((state) => state.auth);
- const isAdmin =
- (user?.role?.includes("admin") ?? false) ||
- (user?.role?.includes("superadmin") ?? false);
-
- return (
-
- );
- };
-
- const wrappedComponentName =
- WrappedComponent.displayName || WrappedComponent.name || "Component";
- WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`;
-
- return WithAdminProp;
-};
-
-export default withAdminProp;
diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx
index 2f89d86df..054f43b92 100644
--- a/Client/src/Components/Inputs/Checkbox/index.jsx
+++ b/Client/src/Components/Inputs/Checkbox/index.jsx
@@ -3,33 +3,50 @@ import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
import { useTheme } from "@emotion/react";
import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react";
import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react";
-
import "./index.css";
/**
- * @param {Object} props
- * @param {string} props.id - The id attribute for the checkbox input.
- * @param {string} props.label - The label to display next to the checkbox.
- * @param {('small' | 'medium' | 'large')} - The size of the checkbox.
- * @param {boolean} props.isChecked - Whether the checkbox is checked or not.
- * @param {string} [props.value] - The value of the checkbox input.
- * @param {function} [props.onChange] - The function to call when the checkbox value changes.
- * @param {boolean} [props.isDisabled] - Whether the checkbox is disabled or not.
+ * Checkbox Component
*
- * @returns {JSX.Element}
+ * A customized checkbox component using Material-UI that supports custom sizing,
+ * disabled states, and custom icons.
+ *
+ * @component
+ * @param {Object} props - Component properties
+ * @param {string} props.id - Unique identifier for the checkbox input
+ * @param {string} [props.name] - Optional name attribute for the checkbox
+ * @param {(string|React.ReactNode)} props.label - Label text or node for the checkbox
+ * @param {('small'|'medium'|'large')} [props.size='medium'] - Size of the checkbox icon
+ * @param {boolean} props.isChecked - Current checked state of the checkbox
+ * @param {string} [props.value] - Optional value associated with the checkbox
+ * @param {Function} [props.onChange] - Callback function triggered when checkbox state changes
+ * @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled
+ *
+ * @returns {React.ReactElement} Rendered Checkbox component
*
* @example
+ * // Basic usage
*
+ *
+ * @example
+ * // With custom size and disabled state
+ *
*/
-
const Checkbox = ({
id,
+ name,
label,
size = "medium",
isChecked,
@@ -46,6 +63,7 @@ const Checkbox = ({
control={
}
@@ -54,7 +72,7 @@ const Checkbox = ({
"aria-label": "controlled checkbox",
id: id,
}}
- sx={{
+ sx={{
"&:hover": { backgroundColor: "transparent" },
"& svg": { width: sizes[size], height: sizes[size] },
alignSelf: "flex-start",
@@ -89,6 +107,7 @@ const Checkbox = ({
Checkbox.propTypes = {
id: PropTypes.string.isRequired,
+ name: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
size: PropTypes.oneOf(["small", "medium", "large"]),
isChecked: PropTypes.bool.isRequired,
diff --git a/Client/src/Components/ProtectedRoute/index.jsx b/Client/src/Components/ProtectedRoute/index.jsx
index 5b1032ab8..7a71aef7e 100644
--- a/Client/src/Components/ProtectedRoute/index.jsx
+++ b/Client/src/Components/ProtectedRoute/index.jsx
@@ -3,21 +3,20 @@ import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
- * ProtectedRoute is a higher-order component that wraps the `Route` component from `react-router-dom`
- * to create a protected route. It uses Redux to check if the user is authenticated. If the user is
- * authenticated, it renders the component passed to it; otherwise, it redirects the user to the login page.
+ * ProtectedRoute is a wrapper component that ensures only authenticated users
+ * can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
+ * If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
*
* @param {Object} props - The props passed to the ProtectedRoute component.
- * @param {React.ComponentType} props.Component - The component to render if the user is authenticated.
- * @param {Object} rest - The remaining props passed to the Route component.
- * @returns {React.ReactElement} A Route component that conditionally renders the passed Component or redirects to the login page.
+ * @param {React.ReactNode} props.children - The children to render if the user is authenticated.
+ * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
*/
-const ProtectedRoute = ({ Component, ...rest }) => {
+const ProtectedRoute = ({ children }) => {
const authState = useSelector((state) => state.auth);
return authState.authToken ? (
-
+ children
) : (
{
};
ProtectedRoute.propTypes = {
- Component: PropTypes.elementType.isRequired,
+ children: PropTypes.elementType.isRequired,
};
export default ProtectedRoute;
diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx
index 00a32a8d3..95abe1d20 100644
--- a/Client/src/Components/Sidebar/index.jsx
+++ b/Client/src/Components/Sidebar/index.jsx
@@ -47,7 +47,7 @@ import Folder from "../../assets/icons/folder.svg?react";
import "./index.css";
const menu = [
- { name: "Monitors", path: "monitors", icon: },
+ { name: "Uptime", path: "uptime", icon: },
{ name: "Pagespeed", path: "pagespeed", icon: },
{ name: "Infrastructure", path: "infrastructure", icon: },
{ name: "Incidents", path: "incidents", icon: },
@@ -262,10 +262,10 @@ function Sidebar() {
Menu
}
- sx={{
+ sx={{
px: theme.spacing(6),
height: "100%",
- overflow: "hidden"
+ overflow: "hidden",
}}
>
{menu.map((item) =>
diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
index 13b16df00..9e7953128 100644
--- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
+++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
@@ -363,7 +363,7 @@ const ProfilePanel = () => {
component="p"
sx={{ opacity: 0.6 }}
>
- Note that deleting your account will remove all data from our system. This
+ Note that deleting your account will remove all data from the server. This
is permanent and non-recoverable.
diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx
index 90bcf21d6..433829f28 100644
--- a/Client/src/Pages/AdvancedSettings/index.jsx
+++ b/Client/src/Pages/AdvancedSettings/index.jsx
@@ -14,10 +14,11 @@ import { useState, useEffect } from "react";
import Select from "../../Components/Inputs/Select";
import { advancedSettingsValidation } from "../../Validation/validation";
import { buildErrors, hasValidationErrors } from "../../Validation/error";
+import { useIsAdmin } from "../../Hooks/useIsAdmin";
-const AdvancedSettings = ({ isAdmin }) => {
+const AdvancedSettings = () => {
const navigate = useNavigate();
-
+ const isAdmin = useIsAdmin();
useEffect(() => {
if (!isAdmin) {
navigate("/");
diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx
new file mode 100644
index 000000000..a7554a7c1
--- /dev/null
+++ b/Client/src/Pages/Auth/Login.jsx
@@ -0,0 +1,562 @@
+import { useState, useEffect, useRef } from "react";
+import { useNavigate } from "react-router-dom";
+import { Box, Button, Stack, Typography } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import { credentials } from "../../Validation/validation";
+import { login } from "../../Features/Auth/authSlice";
+import LoadingButton from "@mui/lab/LoadingButton";
+import { useDispatch, useSelector } from "react-redux";
+import { createToast } from "../../Utils/toastUtils";
+import { networkService } from "../../main";
+import TextInput from "../../Components/Inputs/TextInput";
+import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
+import Background from "../../assets/Images/background-grid.svg?react";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import Mail from "../../assets/icons/mail.svg?react";
+import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+import PropTypes from "prop-types";
+import { logger } from "../../Utils/Logger";
+import "./index.css";
+const DEMO = import.meta.env.VITE_APP_DEMO;
+
+/**
+ * Displays the initial landing page.
+ *
+ * @param {Object} props
+ * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
+ * @returns {JSX.Element}
+ */
+const LandingPage = ({ onContinue }) => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+ Log In
+ We are pleased to see you again!
+
+
+
+
+
+
+ By continuing, you agree to our{" "}
+ {
+ window.open(
+ "https://bluewavelabs.ca/terms-of-service-open-source",
+ "_blank",
+ "noreferrer"
+ );
+ }}
+ sx={{
+ "&:hover": {
+ color: theme.palette.text.tertiary,
+ },
+ }}
+ >
+ Terms of Service
+ {" "}
+ and{" "}
+ {
+ window.open(
+ "https://bluewavelabs.ca/privacy-policy-open-source",
+ "_blank",
+ "noreferrer"
+ );
+ }}
+ sx={{
+ "&:hover": {
+ color: theme.palette.text.tertiary,
+ },
+ }}
+ >
+ Privacy Policy.
+
+
+
+
+ >
+ );
+};
+
+LandingPage.propTypes = {
+ onContinue: PropTypes.func.isRequired,
+};
+
+/**
+ * Renders the first step of the login process.
+ *
+ * @param {Object} props
+ * @param {Object} props.form - Form state object.
+ * @param {Object} props.errors - Object containing form validation errors.
+ * @param {Function} props.onSubmit - Callback function to handle form submission.
+ * @param {Function} props.onChange - Callback function to handle form input changes.
+ * @param {Function} props.onBack - Callback function to handle "Back" button click.
+ * @returns {JSX.Element}
+ */
+const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
+ const theme = useTheme();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ Log In
+ Enter your email address
+
+
+ (e.target.value = e.target.value.toLowerCase())}
+ onChange={onChange}
+ error={errors.email ? true : false}
+ helperText={errors.email}
+ ref={inputRef}
+ />
+
+
+
+
+
+
+ >
+ );
+};
+
+StepOne.propTypes = {
+ form: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onBack: PropTypes.func.isRequired,
+};
+
+/**
+ * Renders the second step of the login process, including a password input field.
+ *
+ * @param {Object} props
+ * @param {Object} props.form - Form state object.
+ * @param {Object} props.errors - Object containing form validation errors.
+ * @param {Function} props.onSubmit - Callback function to handle form submission.
+ * @param {Function} props.onChange - Callback function to handle form input changes.
+ * @param {Function} props.onBack - Callback function to handle "Back" button click.
+ * @returns {JSX.Element}
+ */
+const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const inputRef = useRef(null);
+ const authState = useSelector((state) => state.auth);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ const handleNavigate = () => {
+ if (form.email !== "" && !errors.email) {
+ sessionStorage.setItem("email", form.email);
+ }
+ navigate("/forgot-password");
+ };
+
+ return (
+ <>
+
+
+ Log In
+ Enter your password
+
+
+ }
+ />
+
+
+
+ Continue
+
+
+
+
+
+ Forgot password?
+
+
+ Reset password
+
+
+
+ >
+ );
+};
+
+StepTwo.propTypes = {
+ form: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onBack: PropTypes.func.isRequired,
+};
+
+const Login = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const theme = useTheme();
+
+ const authState = useSelector((state) => state.auth);
+ const { authToken } = authState;
+
+ const idMap = {
+ "login-email-input": "email",
+ "login-password-input": "password",
+ };
+
+ const [form, setForm] = useState({
+ email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
+ password: DEMO !== undefined ? "Demouser1!" : "",
+ });
+ const [errors, setErrors] = useState({});
+ const [step, setStep] = useState(0);
+
+ useEffect(() => {
+ if (authToken) {
+ navigate("/uptime");
+ return;
+ }
+ networkService
+ .doesSuperAdminExist()
+ .then((response) => {
+ if (response.data.data === false) {
+ navigate("/register");
+ }
+ })
+ .catch((error) => {
+ logger.error(error);
+ });
+ }, [authToken, navigate]);
+
+ const handleChange = (event) => {
+ const { value, id } = event.target;
+ const name = idMap[id];
+ setForm((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ const { error } = credentials.validate({ [name]: value }, { abortEarly: false });
+
+ setErrors((prev) => {
+ const prevErrors = { ...prev };
+ if (error) prevErrors[name] = error.details[0].message;
+ else delete prevErrors[name];
+ return prevErrors;
+ });
+ };
+
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+
+ if (step === 1) {
+ const { error } = credentials.validate(
+ { email: form.email },
+ { abortEarly: false }
+ );
+ if (error) {
+ setErrors((prev) => ({ ...prev, email: error.details[0].message }));
+ createToast({ body: error.details[0].message });
+ } else {
+ setStep(2);
+ }
+ } else if (step === 2) {
+ const { error } = credentials.validate(form, { abortEarly: false });
+
+ if (error) {
+ // validation errors
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ setErrors(newErrors);
+ createToast({
+ body:
+ error.details && error.details.length > 0
+ ? error.details[0].message
+ : "Error validating data.",
+ });
+ } else {
+ const action = await dispatch(login(form));
+ if (action.payload.success) {
+ navigate("/uptime");
+ createToast({
+ body: "Welcome back! You're successfully logged in.",
+ });
+ } else {
+ if (action.payload) {
+ if (action.payload.msg === "Incorrect password")
+ setErrors({
+ password: "The password you provided does not match our records",
+ });
+ // dispatch errors
+ createToast({
+ body: action.payload.msg,
+ });
+ } else {
+ // unknown errors
+ createToast({
+ body: "Unknown error.",
+ });
+ }
+ }
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ BlueWave Uptime
+
+ .MuiStack-root": {
+ border: 1,
+ borderRadius: theme.spacing(5),
+ borderColor: theme.palette.border.light,
+ backgroundColor: theme.palette.background.main,
+ padding: {
+ xs: theme.spacing(12),
+ sm: theme.spacing(20),
+ },
+ },
+ }}
+ >
+ {step === 0 ? (
+ setStep(1)} />
+ ) : step === 1 ? (
+ setStep(0)}
+ />
+ ) : (
+ step === 2 && (
+ setStep(1)}
+ />
+ )
+ )}
+
+
+ );
+};
+
+export default Login;
diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx
index 2f4c26040..65c20ca40 100644
--- a/Client/src/Pages/Auth/Register/Register.jsx
+++ b/Client/src/Pages/Auth/Register/Register.jsx
@@ -237,7 +237,7 @@ const Register = ({ isSuperAdmin }) => {
if (action.payload.success) {
const authToken = action.payload.data;
localStorage.setItem("token", authToken);
- navigate("/monitors");
+ navigate("/uptime");
createToast({
body: "Welcome! Your account was created successfully.",
});
diff --git a/Client/src/Pages/Auth/Register/StepThree/index.jsx b/Client/src/Pages/Auth/Register/StepThree/index.jsx
index 760587b7b..4f565c528 100644
--- a/Client/src/Pages/Auth/Register/StepThree/index.jsx
+++ b/Client/src/Pages/Auth/Register/StepThree/index.jsx
@@ -31,7 +31,6 @@ function StepThree({ onSubmit, onBack }) {
}, []);
const { handleChange, feedbacks, form, errors } = useValidatePassword();
- console.log(errors);
return (
<>
*/
-
export const CustomThreshold = ({
checkboxId,
+ checkboxName,
checkboxLabel,
onCheckboxChange,
+ isChecked,
fieldId,
+ fieldName,
+ fieldValue,
onFieldChange,
onFieldBlur,
alertUnit,
- infrastructureMonitor,
errors,
}) => {
const theme = useTheme();
@@ -46,8 +72,9 @@ export const CustomThreshold = ({
@@ -61,11 +88,12 @@ export const CustomThreshold = ({
maxWidth="var(--env-var-width-4)"
type="number"
id={fieldId}
- value={infrastructureMonitor[fieldId]}
+ name={fieldName}
+ value={fieldValue}
onBlur={onFieldBlur}
onChange={onFieldChange}
error={errors[fieldId] ? true : false}
- disabled={!infrastructureMonitor[checkboxId]}
+ disabled={!isChecked}
/>
{
+ return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
+};
+
+const getAlertError = (errors) => {
+ return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))
+ ? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))]
+ : null;
+};
const CreateInfrastructureMonitor = () => {
+ const theme = useTheme();
+ const { user, authToken } = useSelector((state) => state.auth);
+ const monitorState = useSelector((state) => state.infrastructureMonitor);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ // State
+ const [errors, setErrors] = useState({});
+ const [https, setHttps] = useState(false);
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
@@ -38,118 +73,73 @@ const CreateInfrastructureMonitor = () => {
secret: "",
});
- const MS_PER_MINUTE = 60000;
- const THRESHOLD_FIELD_PREFIX = "usage_";
- const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk", "temperature"];
- const { user, authToken } = useSelector((state) => state.auth);
- const monitorState = useSelector((state) => state.infrastructureMonitor);
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const theme = useTheme();
-
- const idMap = {
- "notify-email-default": "notification-email",
- };
-
- const [errors, setErrors] = useState({});
-
- const alertErrKeyLen = Object.keys(errors).filter((k) =>
- k.startsWith(THRESHOLD_FIELD_PREFIX)
- ).length;
-
- const handleCustomAlertCheckChange = (event) => {
- const { value, id } = event.target;
- setInfrastructureMonitor((prev) => {
- const newState = {
- [id]: prev[id] == undefined && value == "on" ? true : !prev[id],
- };
- return {
- ...prev,
- ...newState,
- [THRESHOLD_FIELD_PREFIX + id]: newState[id]
- ? prev[THRESHOLD_FIELD_PREFIX + id]
- : "",
- };
- });
- // Remove the error if unchecked
- setErrors((prev) => {
- return buildErrors(prev, [THRESHOLD_FIELD_PREFIX + id]);
- });
- };
-
- const handleBlur = (event, appendID) => {
+ // Handlers
+ const handleCreateInfrastructureMonitor = async (event) => {
event.preventDefault();
- const { value, id } = event.target;
- let name = idMap[id] ?? id;
- if (name === "url" && infrastructureMonitor.name === "") {
- setInfrastructureMonitor((prev) => ({
- ...prev,
- name: parseDomainName(value),
- }));
+ // Build the form
+ let form = {
+ url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
+ name:
+ infrastructureMonitor.name === ""
+ ? infrastructureMonitor.url
+ : infrastructureMonitor.name,
+ interval: infrastructureMonitor.interval * MS_PER_MINUTE,
+ cpu: infrastructureMonitor.cpu,
+ ...(infrastructureMonitor.cpu
+ ? { usage_cpu: infrastructureMonitor.usage_cpu }
+ : {}),
+ memory: infrastructureMonitor.memory,
+ ...(infrastructureMonitor.memory
+ ? { usage_memory: infrastructureMonitor.usage_memory }
+ : {}),
+ disk: infrastructureMonitor.disk,
+ ...(infrastructureMonitor.disk
+ ? { usage_disk: infrastructureMonitor.usage_disk }
+ : {}),
+ temperature: infrastructureMonitor.temperature,
+ ...(infrastructureMonitor.temperature
+ ? { usage_temperature: infrastructureMonitor.usage_temperature }
+ : {}),
+ secret: infrastructureMonitor.secret,
+ };
+
+ const { error } = infrastructureMonitorValidation.validate(form, {
+ abortEarly: false,
+ });
+
+ if (error) {
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ setErrors(newErrors);
+ createToast({ body: "Please check the form for errors." });
+ return;
}
- if (id?.startsWith("notify-email-")) return;
- const { error } = infrastructureMonitorValidation.validate(
- { [id ?? appendID]: value },
- {
- abortEarly: false,
- }
- );
- setErrors((prev) => {
- return buildErrors(prev, id ?? appendID, error);
- });
- };
+ // Build the thresholds for the form
+ const {
+ cpu,
+ usage_cpu,
+ memory,
+ usage_memory,
+ disk,
+ usage_disk,
+ temperature,
+ usage_temperature,
+ ...rest
+ } = form;
- const handleChange = (event, appendedId) => {
- event.preventDefault();
- const { value, id } = event.target;
- let name = appendedId ?? idMap[id] ?? id;
- if (name.includes("notification-")) {
- name = name.replace("notification-", "");
- let hasNotif = infrastructureMonitor.notifications.some(
- (notification) => notification.type === name
- );
- setInfrastructureMonitor((prev) => {
- const notifs = [...prev.notifications];
- if (hasNotif) {
- return {
- ...prev,
- notifications: notifs.filter((notif) => notif.type !== name),
- };
- } else {
- return {
- ...prev,
- notifications: [
- ...notifs,
- name === "email"
- ? { type: name, address: value }
- : // TODO - phone number
- { type: name, phone: value },
- ],
- };
- }
- });
- } else {
- setInfrastructureMonitor((prev) => ({
- ...prev,
- [name]: value,
- }));
- }
- };
-
- const generatePayload = (form) => {
- let thresholds = {};
- Object.keys(form)
- .filter((k) => k.startsWith(THRESHOLD_FIELD_PREFIX))
- .map((k) => {
- if (form[k]) thresholds[k] = form[k] / 100;
- delete form[k];
- delete form[k.substring(THRESHOLD_FIELD_PREFIX.length)];
- });
+ const thresholds = {
+ ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
+ ...(memory ? { usage_memory: usage_memory / 100 } : {}),
+ ...(disk ? { usage_disk: usage_disk / 100 } : {}),
+ ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
+ };
form = {
- ...form,
+ ...rest,
description: form.name,
teamId: user.teamId,
userId: user._id,
@@ -157,54 +147,69 @@ const CreateInfrastructureMonitor = () => {
notifications: infrastructureMonitor.notifications,
thresholds,
};
- return form;
- };
- const handleCreateInfrastructureMonitor = async (event) => {
- event.preventDefault();
- let form = {
- ...infrastructureMonitor,
- name:
- infrastructureMonitor.name === ""
- ? infrastructureMonitor.url
- : infrastructureMonitor.name,
- interval: infrastructureMonitor.interval * MS_PER_MINUTE,
- };
- delete form.notifications;
- if (hasValidationErrors(form, infrastructureMonitorValidation, setErrors)) {
- return;
+ const action = await dispatch(
+ createInfrastructureMonitor({ authToken, monitor: form })
+ );
+ if (action.meta.requestStatus === "fulfilled") {
+ createToast({ body: "Infrastructure monitor created successfully!" });
+ navigate("/infrastructure");
} else {
- const checkEndpointAction = await dispatch(
- checkInfrastructureEndpointResolution({ authToken, monitorURL: form.url })
- );
- if (checkEndpointAction.meta.requestStatus === "rejected") {
- createToast({
- body: "The endpoint you entered doesn't resolve. Check the URL again.",
- });
- setErrors({ url: "The entered URL is not reachable." });
- return;
- }
- const action = await dispatch(
- createInfrastructureMonitor({ authToken, monitor: generatePayload(form) })
- );
- if (action.meta.requestStatus === "fulfilled") {
- createToast({ body: "Infrastructure monitor created successfully!" });
- navigate("/infrastructure");
- } else {
- createToast({ body: "Failed to create monitor." });
- }
+ createToast({ body: "Failed to create monitor." });
}
};
- //select values
- const frequencies = [
- { _id: 0.25, name: "15 seconds" },
- { _id: 0.5, name: "30 seconds" },
- { _id: 1, name: "1 minute" },
- { _id: 2, name: "2 minutes" },
- { _id: 5, name: "5 minutes" },
- { _id: 10, name: "10 minutes" },
- ];
+ const handleChange = (event) => {
+ const { value, name } = event.target;
+ setInfrastructureMonitor({
+ ...infrastructureMonitor,
+ [name]: value,
+ });
+
+ const { error } = infrastructureMonitorValidation.validate(
+ { [name]: value },
+ { abortEarly: false }
+ );
+ setErrors((prev) => ({
+ ...prev,
+ ...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
+ }));
+ };
+
+ const handleCheckboxChange = (event) => {
+ const { name } = event.target;
+ const { checked } = event.target;
+ setInfrastructureMonitor({
+ ...infrastructureMonitor,
+ [name]: checked,
+ });
+ };
+
+ const handleNotifications = (event, type) => {
+ const { value } = event.target;
+ let notifications = [...infrastructureMonitor.notifications];
+ const notificationExists = notifications.some((notification) => {
+ if (notification.type === type && notification.address === value) {
+ return true;
+ }
+ return false;
+ });
+ if (notificationExists) {
+ notifications = notifications.filter((notification) => {
+ if (notification.type === type && notification.address === value) {
+ return false;
+ }
+ return true;
+ });
+ } else {
+ notifications.push({ type, address: value });
+ }
+
+ setInfrastructureMonitor((prev) => ({
+ ...prev,
+ notifications,
+ }));
+ };
return (
@@ -239,56 +244,75 @@ const CreateInfrastructureMonitor = () => {
fontSize="inherit"
fontWeight="inherit"
>
- infrastructure monitor
+ monitor
-
-
- General settings
-
- Here you can select the URL of the host, together with the friendly name
- and authorization secret to connect to the server agent.
-
-
- The server you are monitoring must be running the{" "}
-
-
-
-
+
+ General settings
+
+ Here you can select the URL of the host, together with the friendly name and
+ authorization secret to connect to the server agent.
+
+
+ The server you are monitoring must be running the{" "}
+
+
+
}
+ placeholder={"localhost:59232/api/v1/metrics"}
label="Server URL"
- placeholder="https://"
+ https={https}
value={infrastructureMonitor.url}
- onBlur={handleBlur}
onChange={handleChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
+
+ Protocol
+
+
+
+
+
{
(notification) => notification.type === "email"
)}
value={user?.email}
- onChange={(e) => handleChange(e)}
- onBlur={handleBlur}
+ onChange={(event) => handleNotifications(event, "email")}
/>
-
Customize alerts
@@ -325,25 +347,31 @@ const CreateInfrastructureMonitor = () => {
- {HARDWARE_MONITOR_TYPES.map((type, idx) => (
-
- ))}
- {alertErrKeyLen > 0 && (
+ {METRICS.map((metric) => {
+ return (
+
+ );
+ })}
+ {/* Error text */}
+ {hasAlertError(errors) && (
{
opacity: 0.8,
}}
>
- {
- errors[
- THRESHOLD_FIELD_PREFIX +
- HARDWARE_MONITOR_TYPES.filter(
- (type) => errors[THRESHOLD_FIELD_PREFIX + type]
- )[0]
- ]
- }
+ {getAlertError(errors)}
)}
@@ -372,11 +393,11 @@ const CreateInfrastructureMonitor = () => {
diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx
index 06fb1296a..346fe9c5e 100644
--- a/Client/src/Pages/Infrastructure/Details/index.jsx
+++ b/Client/src/Pages/Infrastructure/Details/index.jsx
@@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart";
import { useSelector } from "react-redux";
import { networkService } from "../../../main";
import PulseDot from "../../../Components/Animated/PulseDot";
-import useUtils from "../../Monitors/utils";
+import useUtils from "../../Uptime/utils";
import { useNavigate } from "react-router-dom";
import Empty from "./empty";
import { logger } from "../../../Utils/Logger";
@@ -283,7 +283,7 @@ const InfrastructureDetails = () => {
{
type: "memory",
value: decimalToPercentage(memoryUsagePercent),
- heading: "Memory Usage",
+ heading: "Memory usage",
metricOne: "Used",
valueOne: formatBytes(memoryUsedBytes),
metricTwo: "Total",
@@ -292,7 +292,7 @@ const InfrastructureDetails = () => {
{
type: "cpu",
value: decimalToPercentage(cpuUsagePercent),
- heading: "CPU Usage",
+ heading: "CPU usage",
metricOne: "Cores",
valueOne: cpuPhysicalCores ?? 0,
metricTwo: "Frequency",
@@ -364,7 +364,7 @@ const InfrastructureDetails = () => {
heading: "Memory usage",
strokeColor: theme.palette.primary.main,
gradientStartColor: theme.palette.primary.main,
- yLabel: "Memory Usage",
+ yLabel: "Memory usage",
yDomain: [0, 1],
yTick: ,
xTick: ,
@@ -372,7 +372,7 @@ const InfrastructureDetails = () => {
),
},
@@ -383,7 +383,7 @@ const InfrastructureDetails = () => {
heading: "CPU usage",
strokeColor: theme.palette.success.main,
gradientStartColor: theme.palette.success.main,
- yLabel: "CPU Usage",
+ yLabel: "CPU usage",
yDomain: [0, 1],
yTick: ,
xTick: ,
@@ -391,7 +391,7 @@ const InfrastructureDetails = () => {
),
},
diff --git a/Client/src/Pages/Infrastructure/components/Menu/index.jsx b/Client/src/Pages/Infrastructure/components/Menu/index.jsx
index 9794a34d9..98b3f9878 100644
--- a/Client/src/Pages/Infrastructure/components/Menu/index.jsx
+++ b/Client/src/Pages/Infrastructure/components/Menu/index.jsx
@@ -109,88 +109,6 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
},
}}
>
- {/*
- Open site action. Not necessary for infrastructure?
-
- {actions.url !== null ? (
-
- ) : (
- ""
- )}
- */}
-
- {/*
- Incidents. Necessary?
- */}
- {/*
- Configure. Necessary?
- {isAdmin && (
-
- )} */}
- {/*
- Clone. Necessary?
- {isAdmin && (
-
- )} */}
- {/*
- Pause. Necessary?
- const handlePause = async () => {
- try {
- const action = await dispatch(
- pauseUptimeMonitor({ authToken, monitorId: monitor._id })
- );
- if (pauseUptimeMonitor.fulfilled.match(action)) {
- updateCallback();
- const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
- createToast({ body: `Monitor ${state} successfully.` });
- } else {
- throw new Error(action?.error?.message ?? "Failed to pause monitor.");
- }
- } catch (error) {
- logger.error("Error pausing monitor:", monitor._id, error);
- createToast({ body: "Failed to pause monitor." });
- }
- };
- {isAdmin && (
-
- )} */}
{isAdmin && }