diff --git a/.dockerignore b/.dockerignore index e6b9c067d..fdfa00585 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ Client/node_modules/ Server/node_modules/ -*.env \ No newline at end of file +Server/*.env \ No newline at end of file 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 f1de2ddb8..c3ac51a62 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -11,24 +11,24 @@ "@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.1", + "@mui/lab": "6.0.0-beta.20", + "@mui/material": "6.2.1", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.23.0", - "@mui/x-date-pickers": "7.23.0", - "@reduxjs/toolkit": "2.4.0", + "@mui/x-data-grid": "7.23.3", + "@mui/x-date-pickers": "7.23.3", + "@reduxjs/toolkit": "2.5.0", "axios": "^1.7.4", "dayjs": "1.11.13", "joi": "17.13.3", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-redux": "9.1.2", + "react-redux": "9.2.0", "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" }, @@ -352,9 +352,9 @@ } }, "node_modules/@emotion/cache": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", - "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", @@ -386,16 +386,16 @@ "license": "MIT" }, "node_modules/@emotion/react": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz", - "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.13.5", + "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" @@ -429,16 +429,16 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz", - "integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2" }, "peerDependencies": { @@ -458,9 +458,9 @@ "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", "license": "MIT", "peerDependencies": { "react": ">=16.8.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.67", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.67.tgz", + "integrity": "sha512-pwjibXqBrSw1Z1fZEIhWJnMJHSvh3BLz0pZcrX/x01B/BVf6QUbFlGVV/k8KCsZ9sPzln1l8bBJ+ynIIE9LtjA==", "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/types": "^7.2.20", + "@mui/utils": "^6.2.1", "@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.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.1.tgz", + "integrity": "sha512-U/8vS1+1XiHBnnRRESSG1gvr6JDHdPjrpnW6KEebkAQWBn6wrpbSF/XSZ8/vJIRXH5NyDmMHi4Ro5Q70//JKhA==", "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.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.1.tgz", + "integrity": "sha512-bP0XtW+t5KFL+wjfQp2UctN/8CuWqF1qaxbYuCAsJhL+AzproM8gGOh2n8sNBcrjbVckzDNqaXqxdpn+OmoWug==", "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.1", "@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.20", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.20.tgz", + "integrity": "sha512-Sg3aTrSSsfXcSHwKhnZbXmz7kUZzvXww3vO4p8U/DbMVSGJxll1GRpKNQkFLpZy2y7ZBTgKaZTbPYw4Q6UKrsA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/base": "5.0.0-beta.64", - "@mui/system": "^6.1.10", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.10", + "@mui/base": "5.0.0-beta.67", + "@mui/system": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "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.1", + "@mui/material-pigment-css": "^6.2.1", "@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.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.1.tgz", + "integrity": "sha512-7VlKGsRKsy1bOSOPaSNgpkzaL+0C7iWAVKd2KYyAvhR9fTLJtiAMpq+KuzgEh1so2mtvQERN0tZVIceWMiIesw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.1.10", - "@mui/system": "^6.1.10", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.10", + "@mui/core-downloads-tracker": "^6.2.1", + "@mui/system": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.11", + "@types/react-transition-group": "^4.4.12", "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.1", "@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.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.1.tgz", + "integrity": "sha512-u1y0gpcfrRRxCcIdVeU5eIvkinA82Q8ft178WUNYuoFQrsOrXdlBdZlRVi+eYuUFp1iXI55Cud7sMZZtETix5Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.1.10", + "@mui/utils": "^6.2.1", "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.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.2.1.tgz", + "integrity": "sha512-6R3OgYw6zgCZWFYYMfxDqpGfJA78mUTOIlUDmmJlr60ogVNCrM87X0pqx5TbZ2OwUyxlJxN9qFgRr+J9H6cOBg==", "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.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.1.tgz", + "integrity": "sha512-0lc8CbBP4WAAF+SmGMFJI9bpIyQvW3zvwIDzLsb26FIB/4Z0pO7qGe8mkAl0RM63Vb37899qxnThhHKgAAdy6w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.1.10", - "@mui/styled-engine": "^6.1.10", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.10", + "@mui/private-theming": "^6.2.1", + "@mui/styled-engine": "^6.2.1", + "@mui/types": "^7.2.20", + "@mui/utils": "^6.2.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1334,9 +1340,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.19", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", - "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", + "version": "7.2.20", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz", + "integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -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.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.1.tgz", + "integrity": "sha512-ubLqGIMhKUH2TF/Um+wRzYXgAooQw35th+DPemGrTpgrZHpOgcnUDIDbwsk1e8iQiuJ3mV/ErTtcQrecmlj5cg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.19", - "@types/prop-types": "^15.7.13", + "@mui/types": "^7.2.20", + "@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.0", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.23.0.tgz", - "integrity": "sha512-lXG+vRMFvisFaj93LlglZij7e5NEHptXQG0x9EshSI8uIm8HN9Xp77qIdE95xLyltImakuFCeJaEk9oaRMBTpA==", + "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.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.0.tgz", - "integrity": "sha512-nypSz/7j0HPvW7tRPcZAlQADOiRAE4jTIcxwwJUPLtU17EPJOiw1iB29SRYtUThw4f3aXETPAeT4fzgagpuiKg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.3.tgz", + "integrity": "sha512-EiM5kut6N/0o0iEYx8A7M3fJqknAa1kcPvGhlX3hH50ERLDeuJaqoKzvRYLBbYKWydHIc+0hHIFcK5oQTXLenw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1473,9 +1485,9 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.0.tgz", - "integrity": "sha512-Db9ElibVYHluXLVsRLfFwlYkL6/3NNE5AosSZiTx+Gw7uix/Z3pdjyHeA3ab65fU1tCk08XHY0PU6LQFifYB2g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz", + "integrity": "sha512-bjTYX/QzD5ZhVZNNnastMUS3j2Hy4p4IXmJgPJ0vKvQBvUdfEO+ZF42r3PJNNde0FVT1MmTzkmdTlz0JZ6ukdw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1679,9 +1691,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.4.0.tgz", - "integrity": "sha512-wJZEuSKj14tvNfxiIiJws0tQN77/rDqucBq528ApebMIRHyWpCanJVQRxQ8WWZC19iCDKxDsGlbAir3F1layxA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", + "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", "license": "MIT", "dependencies": { "immer": "^10.0.3", @@ -1690,7 +1702,7 @@ "reselect": "^5.1.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "peerDependenciesMeta": { @@ -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.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.13.tgz", - "integrity": "sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg==", + "version": "18.3.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2370,28 +2382,28 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "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", - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^18.0.0" } }, "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "license": "MIT", - "dependencies": { + "peerDependencies": { "@types/react": "*" } }, "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, "node_modules/@ungap/structured-clone": { @@ -3551,9 +3563,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", - "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", "dev": true, "license": "MIT", "engines": { @@ -4887,16 +4899,15 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5333,17 +5344,17 @@ "license": "MIT" }, "node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", "redux": "^5.0.0" }, "peerDependenciesMeta": { @@ -5366,9 +5377,9 @@ } }, "node_modules/react-router": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", - "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.1.tgz", + "integrity": "sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA==", "license": "MIT", "dependencies": { "@remix-run/router": "1.21.0" @@ -5381,13 +5392,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", - "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.1.tgz", + "integrity": "sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ==", "license": "MIT", "dependencies": { "@remix-run/router": "1.21.0", - "react-router": "6.28.0" + "react-router": "6.28.1" }, "engines": { "node": ">=14.0.0" @@ -5442,9 +5453,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 +5471,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": { @@ -6163,12 +6174,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/victory-vendor": { diff --git a/Client/package.json b/Client/package.json index d0d4b380d..1e22e76d3 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,24 +14,24 @@ "@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.1", + "@mui/lab": "6.0.0-beta.20", + "@mui/material": "6.2.1", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.23.0", - "@mui/x-date-pickers": "7.23.0", - "@reduxjs/toolkit": "2.4.0", + "@mui/x-data-grid": "7.23.3", + "@mui/x-date-pickers": "7.23.3", + "@reduxjs/toolkit": "2.5.0", "axios": "^1.7.4", "dayjs": "1.11.13", "joi": "17.13.3", "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-redux": "9.1.2", + "react-redux": "9.2.0", "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 82c8203a8..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"; -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/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx index 3760c04b8..39e8a3f42 100644 --- a/Client/src/Components/Avatar/index.jsx +++ b/Client/src/Components/Avatar/index.jsx @@ -2,28 +2,7 @@ import { Avatar as MuiAvatar } from "@mui/material"; import PropTypes from "prop-types"; import { useSelector } from "react-redux"; import { useEffect, useState } from "react"; - -/** - * Generates a color based on the input string. - * @param {string} string - The input string to generate the color from. - * @returns {string} - */ -const stringToColor = (string) => { - let hash = 0; - let i; - for (i = 0; i < string.length; i += 1) { - hash = string.charCodeAt(i) + ((hash << 5) - hash); - } - - let color = "#"; - for (i = 0; i < 3; i += 1) { - const value = (hash >> (i * 8)) & 0xff; - color += `00${value.toString(16)}`.slice(-2); - } - - return color; -}; - +import { useTheme } from "@emotion/react"; /** * @component * @param {Object} props @@ -38,6 +17,7 @@ const stringToColor = (string) => { const Avatar = ({ src, small, sx }) => { const { user } = useSelector((state) => state.auth); + const theme = useTheme(); const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; const border = small ? 1 : 3; @@ -57,7 +37,7 @@ const Avatar = ({ src, small, sx }) => { fontSize: small ? "16px" : "22px", color: "white", fontWeight: 400, - backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`), + backgroundColor: theme.palette.primary.main, // Same BG color as checkmate BG in sidebar display: "inline-flex", "&::before": { content: `""`, @@ -74,7 +54,7 @@ const Avatar = ({ src, small, sx }) => { }} > {user.firstName?.charAt(0)} - {user.lastName?.charAt(0)} + {user.lastName?.charAt(0) || ''} ); }; diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx index e519a079f..cfc3030b0 100644 --- a/Client/src/Components/BasicTable/index.jsx +++ b/Client/src/Components/BasicTable/index.jsx @@ -115,7 +115,7 @@ TablePaginationActions.propTypes = { * @param {boolean} [props.paginated=false] - Flag to enable pagination. * @param {boolean} [props.reversed=false] - Flag to enable reverse order. * @param {number} props.rowsPerPage- Number of rows per page (table). - * + * @param {string} props.emptyMessage - Message to display when there is no data. * @example * const data = { * cols: [ @@ -149,7 +149,7 @@ TablePaginationActions.propTypes = { * */ -const BasicTable = ({ data, paginated, reversed, table }) => { +const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => { const DEFAULT_ROWS_PER_PAGE = 5; const theme = useTheme(); const dispatch = useDispatch(); @@ -216,7 +216,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => { sx={{ cursor: row.handleClick ? "pointer" : "default", "&:hover": { - backgroundColor: theme.palette.background.accent, + filter: "brightness(.75)", + opacity: 0.75, + transition: "filter 0.3s ease, opacity 0.3s ease", }, }} key={row.id} @@ -228,6 +230,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => { ); })} + {displayData.length === 0 && ( + + + {emptyMessage} + + + )} @@ -317,6 +329,7 @@ BasicTable.propTypes = { reversed: PropTypes.bool, rowPerPage: PropTypes.number, table: PropTypes.string, + emptyMessage: PropTypes.string, }; export default BasicTable; diff --git a/Client/src/Pages/Monitors/styled.jsx b/Client/src/Components/ConfigBox/index.jsx similarity index 81% rename from Client/src/Pages/Monitors/styled.jsx rename to Client/src/Components/ConfigBox/index.jsx index f159d44f2..7650dd47f 100644 --- a/Client/src/Pages/Monitors/styled.jsx +++ b/Client/src/Components/ConfigBox/index.jsx @@ -1,12 +1,15 @@ import { Stack, styled } from "@mui/material"; -export const ConfigBox = styled(Stack)(({ theme }) => ({ +const ConfigBox = styled(Stack)(({ theme }) => ({ + display: "flex", flexDirection: "row", + justifyContent: "space-between", + gap: theme.spacing(20), + backgroundColor: theme.palette.background.main, border: 1, borderStyle: "solid", borderColor: theme.palette.border.light, borderRadius: theme.spacing(2), - backgroundColor: theme.palette.background.main, "& > *": { paddingTop: theme.spacing(12), paddingBottom: theme.spacing(18), @@ -24,15 +27,12 @@ export const ConfigBox = styled(Stack)(({ theme }) => ({ paddingRight: theme.spacing(20), paddingLeft: theme.spacing(18), }, - "& h2": { + "& h1, & h2": { color: theme.palette.text.secondary, - fontSize: 15, - fontWeight: 600, - }, - "& h3, & p": { - color: theme.palette.text.tertiary, }, "& p": { - fontSize: 13, + color: theme.palette.text.tertiary, }, })); + +export default ConfigBox; 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/IconBox/index.jsx b/Client/src/Components/IconBox/index.jsx new file mode 100644 index 000000000..8000fbd64 --- /dev/null +++ b/Client/src/Components/IconBox/index.jsx @@ -0,0 +1,77 @@ +import { Box, styled } from "@mui/material"; +import PropTypes from "prop-types"; + +/** + * IconBox - A styled box component for rendering icons with consistent sizing and styling + * + * @component + * @param {Object} [props] - Configuration options for the IconBox + * @param {number} [props.height=34] - Height of the icon box + * @param {number} [props.width=34] - Width of the icon box + * @param {number} [props.minWidth=34] - Minimum width of the icon box + * @param {number} [props.borderRadius=4] - Border radius of the icon box + * @param {number} [props.svgWidth=20] - Width of the SVG icon + * @param {number} [props.svgHeight=20] - Height of the SVG icon + * + * @example + * // Basic usage + * + * + * + * + * @example + * // Customized usage + * + * + * + * + * @returns {React.ReactElement} A styled box containing an icon + */ +const IconBox = styled(Box)( + ({ + theme, + height = 34, + width = 34, + minWidth = 34, + borderRadius = 4, + svgWidth = 20, + svgHeight = 20, + }) => ({ + height: height, + minWidth: minWidth, + width: width, + position: "relative", + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.dark, + borderRadius: borderRadius, + backgroundColor: theme.palette.background.accent, + "& svg": { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: svgWidth, + height: svgHeight, + "& path": { + stroke: theme.palette.text.tertiary, + }, + }, + }) +); + +IconBox.propTypes = { + height: PropTypes.number, + width: PropTypes.number, + minWidth: PropTypes.number, + borderRadius: PropTypes.number, + svgWidth: PropTypes.number, + svgHeight: PropTypes.number, +}; + +export default IconBox; 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/Inputs/Radio/index.jsx b/Client/src/Components/Inputs/Radio/index.jsx index 7a700e2b7..6291485f0 100644 --- a/Client/src/Components/Inputs/Radio/index.jsx +++ b/Client/src/Components/Inputs/Radio/index.jsx @@ -42,6 +42,9 @@ const Radio = (props) => { width: 16, height: 16, boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`, + "&:not(.Mui-checked)": { + boxShadow: `inset 0 0 0 1px ${theme.palette.text.primary}70`, // Use theme text color for the outline + }, mt: theme.spacing(0.5), }} /> diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx index 100b826b4..9a1995726 100644 --- a/Client/src/Components/Inputs/Search/index.jsx +++ b/Client/src/Components/Inputs/Search/index.jsx @@ -40,6 +40,7 @@ const SearchAdornment = () => { //TODO keep search state inside of component const Search = ({ + label, id, options, filteredBy, @@ -76,6 +77,14 @@ const Search = ({ getOptionLabel={(option) => option[filteredBy]} renderInput={(params) => ( + + {label} + {error && ( diff --git a/Client/src/Components/Inputs/Select/index.jsx b/Client/src/Components/Inputs/Select/index.jsx index 8517c3214..ea9509c87 100644 --- a/Client/src/Components/Inputs/Select/index.jsx +++ b/Client/src/Components/Inputs/Select/index.jsx @@ -49,6 +49,7 @@ const Select = ({ onBlur, sx, name = "", + labelControlSpacing = 2, }) => { const theme = useTheme(); const itemStyles = { @@ -57,10 +58,10 @@ const Select = ({ borderRadius: theme.shape.borderRadius, margin: theme.spacing(2), }; - + return ( {label && ( @@ -144,6 +145,7 @@ Select.propTypes = { onChange: PropTypes.func.isRequired, onBlur: PropTypes.func, sx: PropTypes.object, + labelControlSpacing: PropTypes.number, }; export default Select; diff --git a/Client/src/Components/Label/index.jsx b/Client/src/Components/Label/index.jsx index 1a372c8a1..19d7998d0 100644 --- a/Client/src/Components/Label/index.jsx +++ b/Client/src/Components/Label/index.jsx @@ -141,9 +141,9 @@ const StatusLabel = ({ status, text, customStyles }) => { borderColor: theme.palette.warning.light, }, pending: { - dotColor: theme.palette.warning.main, - bgColor: theme.palette.warning.dark, - borderColor: theme.palette.warning.light, + dotColor: theme.palette.text.secondary, + bgColor: theme.palette.background.main, + borderColor: theme.palette.border.dark, }, "cannot resolve": { dotColor: theme.palette.unresolved.main, diff --git a/Client/src/Components/ProtectedRoute/index.jsx b/Client/src/Components/ProtectedRoute/index.jsx index 5b1032ab8..313500338 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.element.isRequired, }; export default ProtectedRoute; diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 4c274409b..b071caa1e 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -21,13 +21,13 @@ import { useDispatch, useSelector } from "react-redux"; import { clearAuthState } from "../../Features/Auth/authSlice"; import { toggleSidebar } from "../../Features/UI/uiSlice"; import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import ThemeSwitch from "../ThemeSwitch"; import Avatar from "../Avatar"; import LockSvg from "../../assets/icons/lock.svg?react"; import UserSvg from "../../assets/icons/user.svg?react"; import TeamSvg from "../../assets/icons/user-two.svg?react"; import LogoutSvg from "../../assets/icons/logout.svg?react"; import Support from "../../assets/icons/support.svg?react"; -import Dashboard from "../../assets/icons/dashboard.svg?react"; import Account from "../../assets/icons/user-edit.svg?react"; import Maintenance from "../../assets/icons/maintenance.svg?react"; import Monitors from "../../assets/icons/monitors.svg?react"; @@ -43,11 +43,12 @@ import DotsVertical from "../../assets/icons/dots-vertical.svg?react"; import ChangeLog from "../../assets/icons/changeLog.svg?react"; import Docs from "../../assets/icons/docs.svg?react"; import Folder from "../../assets/icons/folder.svg?react"; +import ChatBubbleOutlineRoundedIcon from "@mui/icons-material/ChatBubbleOutlineRounded"; 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: }, @@ -63,20 +64,31 @@ const menu = [ { name: "Team", path: "account/team", icon: }, ], }, + { + name: "Settings", + icon: , + path: "settings", + }, { name: "Other", icon: , nested: [ - { name: "Settings", path: "settings", icon: }, { name: "Support", path: "support", icon: }, + { + name: "Discussions", + path: "discussions", + icon: , + }, { name: "Docs", path: "docs", icon: }, { name: "Changelog", path: "changelog", icon: }, ], }, ]; +/* TODO this could be a key in nested Path would be the link */ const URL_MAP = { - support: "https://github.com/bluewave-labs/bluewave-uptime/issues", + support: "https://discord.com/invite/NAb6H3UTjK", + discussions: "https://github.com/bluewave-labs/checkmate/discussions", docs: "https://bluewavelabs.gitbook.io/checkmate", changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases", }; @@ -177,13 +189,7 @@ function Sidebar() { direction="row" alignItems="center" gap={theme.spacing(4)} - onClick={() => - window.open( - "https://github.com/bluewave-labs/bluewave-uptime", - "_blank", - "noreferrer" - ) - } + onClick={() => navigate("/")} sx={{ cursor: "pointer" }} > } - sx={{ px: theme.spacing(6) }} + sx={{ + px: theme.spacing(6), + height: "100%", + overflow: "hidden", + }} > {menu.map((item) => item.path ? ( @@ -547,28 +557,35 @@ function Sidebar() { {authState.user?.role} - - openPopup(event, "logout")} + + - - - + openPopup(event, "logout")} + > + + + + )} + * ) + * + * @returns {React.ReactElement} A styled box containing the statistic + */ + +const StatBox = ({ heading, subHeading, sx }) => { + const theme = useTheme(); + return ( + + {heading} + {subHeading} + + ); +}; + +StatBox.propTypes = { + heading: PropTypes.string.isRequired, + subHeading: PropTypes.node.isRequired, + sx: PropTypes.object, +}; + +export default StatBox; diff --git a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx index 5e66667cb..a34c0000d 100644 --- a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx +++ b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx @@ -28,6 +28,9 @@ const PasswordPanel = () => { const theme = useTheme(); const dispatch = useDispatch(); + + const SPACING_GAP = theme.spacing(12); + //redux state const { authToken, isLoading } = useSelector((state) => state.auth); @@ -139,7 +142,7 @@ const PasswordPanel = () => { direction="row" justifyContent={"flex-start"} alignItems={"center"} - gap={theme.spacing(8)} + gap={SPACING_GAP} flexWrap={"wrap"} > { { { const theme = useTheme(); const dispatch = useDispatch(); + + const SPACING_GAP = theme.spacing(12); //redux state const { user, authToken, isLoading } = useSelector((state) => state.auth); @@ -223,9 +225,9 @@ const ProfilePanel = () => { className="edit-profile-form" noValidate spellCheck="false" - gap={theme.spacing(20)} + gap={SPACING_GAP} > - + First name @@ -240,7 +242,7 @@ const ProfilePanel = () => { flex={1} /> - + Last name @@ -255,7 +257,7 @@ const ProfilePanel = () => { flex={1} /> - + Email { flex={1} /> - + Your photo { 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/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index fd17987d1..bbc6c4305 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -30,6 +30,8 @@ const TeamPanel = () => { const theme = useTheme(); + const SPACING_GAP = theme.spacing(12); + const { authToken, user } = useSelector((state) => state.auth); //TODO // const [orgStates, setOrgStates] = useState({ @@ -62,7 +64,7 @@ const TeamPanel = () => { }; fetchTeam(); - }, [user]); + }, [authToken]); useEffect(() => { let team = members; @@ -79,8 +81,6 @@ const TeamPanel = () => { { id: 1, name: "NAME" }, { id: 2, name: "EMAIL" }, { id: 3, name: "ROLE" }, - // FEATURE STILL TO BE IMPLEMENTED - // { id: 4, name: "ACTION" }, ], rows: team?.map((member, idx) => { const roles = member.role.map((role) => roleMap[role]).join(","); @@ -102,45 +102,19 @@ const TeamPanel = () => { }, { id: idx + 1, data: member.email }, { - // TODO - Add select dropdown id: idx + 2, data: roles, }, - // FEATURE STILL TO BE IMPLEMENTED - // { - // // TODO - Add delete onClick - // id: idx + 3, - // data: ( - // - // - // - // ), - // }, ], }; }), }; setTableData(data); - }, [members, filter]); + }, [filter, members, roleMap, theme]); useEffect(() => { setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === ""); }, [errors, toInvite.email]); - - // RENAME ORGANIZATION - // const toggleEdit = () => { - // setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit })); - // }; - // const handleRename = () => {}; - - // INVITE MEMBER const [isOpen, setIsOpen] = useState(false); const handleChange = (event) => { @@ -224,58 +198,11 @@ const TeamPanel = () => { }, }} > - {/* FEATURE STILL TO BE IMPLEMENTED */} - {/* - - Organization name - - - - setOrgStates((prev) => ({ - ...prev, - name: event.target.value, - })) - } - disabled={!orgStates.isEdit} - sx={{ - color: theme.palette.otherColors.bluishGray, - "& .Mui-disabled": { - WebkitTextFillColor: "initial !important", - }, - "& .Mui-disabled fieldset": { - borderColor: "transparent !important", - }, - }} - inputProps={{ - sx: { textAlign: "end", padding: theme.spacing(4) }, - }} - /> - - - - - 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("/monitors"); - 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("/monitors"); - 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/Login/Components/EmailStep.jsx b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx new file mode 100644 index 000000000..ac796e627 --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx @@ -0,0 +1,96 @@ +import { useRef, useEffect } from "react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import PropTypes from "prop-types"; + +/** + * Renders the email step of the login process which includes an email 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 EmailStep = ({ form, errors, onSubmit, onChange }) => { + 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} + /> + + + + + + + ); +}; + +EmailStep.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default EmailStep; diff --git a/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx new file mode 100644 index 000000000..0a5fa8153 --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx @@ -0,0 +1,43 @@ +import { Box, Typography, useTheme } from "@mui/material"; +import PropTypes from "prop-types"; +import { useNavigate } from "react-router"; + +const ForgotPasswordLabel = ({ email, errorEmail }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + const handleNavigate = () => { + if (email !== "" && !errorEmail) { + sessionStorage.setItem("email", email); + } + navigate("/forgot-password"); + }; + + return ( + + + Forgot password? + + + Reset password + + + ); +}; + +ForgotPasswordLabel.proptype = { + email: PropTypes.string.isRequired, + emailError: PropTypes.string.isRequired, +}; + +export default ForgotPasswordLabel; diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx new file mode 100644 index 000000000..b7384ba98 --- /dev/null +++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx @@ -0,0 +1,124 @@ +import { useRef, useEffect } from "react"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import LoadingButton from "@mui/lab/LoadingButton"; +import { useSelector } from "react-redux"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import PropTypes from "prop-types"; + +/** + * Renders the password 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 PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => { + const theme = useTheme(); + const inputRef = useRef(null); + const authState = useSelector((state) => state.auth); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + <> + + + Log In + Enter your password + + + } + /> + + + + Continue + + + + + + ); +}; + +PasswordStep.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, +}; + +export default PasswordStep; diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx new file mode 100644 index 000000000..890c87eb7 --- /dev/null +++ b/Client/src/Pages/Auth/Login/Login.jsx @@ -0,0 +1,223 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { credentials } from "../../../Validation/validation"; +import { login } from "../../../Features/Auth/authSlice"; +import { useDispatch, useSelector } from "react-redux"; +import { createToast } from "../../../Utils/toastUtils"; +import { networkService } from "../../../main"; +import Background from "../../../assets/Images/background-grid.svg?react"; +import Logo from "../../../assets/icons/bwu-icon.svg?react"; +import { logger } from "../../../Utils/Logger"; +import "../index.css"; +import EmailStep from "./Components/EmailStep"; +import PasswordStep from "./Components/PasswordStep"; +import ThemeSwitch from "../../../Components/ThemeSwitch"; +import ForgotPasswordLabel from "./Components/ForgotPasswordLabel"; + +const DEMO = import.meta.env.VITE_APP_DEMO; + +/** + * Displays the login page. + */ + +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 === 0) { + 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(1); + } + } else if (step === 1) { + 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 ? ( + + ) : ( + step === 1 && ( + setStep(0)} + /> + ) + )} + + + + + + + ); +}; + +export default Login; diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx index c380425c9..2c4ef0106 100644 --- a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx +++ b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx @@ -4,10 +4,10 @@ import { useNavigate } from "react-router"; import { useDispatch } from "react-redux"; import { clearAuthState } from "../../Features/Auth/authSlice"; import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { IconBox } from "./styled"; import Background from "../../assets/Images/background-grid.svg?react"; import ConfirmIcon from "../../assets/icons/check-outlined.svg?react"; import Logo from "../../assets/icons/bwu-icon.svg?react"; +import IconBox from "../../Components/IconBox"; import "./index.css"; const NewPasswordConfirmed = () => { @@ -80,9 +80,22 @@ const NewPasswordConfirmed = () => { textAlign="center" > - - - + + + + + Password reset Your password has been successfully reset. Click below to log in magically. @@ -91,7 +104,7 @@ const NewPasswordConfirmed = () => { + + + { - When there is a new incident, { (notification) => notification.type === "email" )} value={user?.email} - onChange={(e) => handleChange(e)} - onBlur={handleBlur} + onChange={(event) => handleNotifications(event, "email")} /> - Customize alerts @@ -326,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)} )} @@ -373,11 +393,11 @@ const CreateInfrastructureMonitor = () => { { }} items={repeatConfig} /> - - Date + { /> - + - - - - Start time - - All dates and times are in GMT+0 time zone. - - - - { - handleTimeChange("startTime", newTime); - }} - slotProps={{ - nextIconButton: { sx: { ml: theme.spacing(2) } }, - field: { - sx: { - width: "fit-content", - "& > .MuiOutlinedInput-root": { - flexDirection: "row-reverse", - }, - "& input": { - height: 34, - p: 0, - pl: theme.spacing(5), - }, - "& fieldset": { - borderColor: theme.palette.border.dark, - borderRadius: theme.shape.borderRadius, - }, - "&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset": - { - borderColor: theme.palette.border.dark, - }, + + + Start time + + All dates and times are in GMT+0 time zone. + + + + { + handleTimeChange("startTime", newTime); + }} + slotProps={{ + nextIconButton: { sx: { ml: theme.spacing(2) } }, + field: { + sx: { + width: "fit-content", + "& > .MuiOutlinedInput-root": { + flexDirection: "row-reverse", }, + "& input": { + height: 34, + p: 0, + pl: theme.spacing(5), + }, + "& fieldset": { + borderColor: theme.palette.border.dark, + borderRadius: theme.shape.borderRadius, + }, + "&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset": + { + borderColor: theme.palette.border.dark, + }, }, - }} - error={errors["startTime"]} - /> - - - - - - - Duration - - + }, + }} + error={errors["startTime"]} + /> + + { handleFormChange("duration", event.target.value); @@ -491,66 +489,34 @@ const CreateMaintenance = () => { - - - Monitor related settings - - - - - - Friendly name - - - - { - handleFormChange("name", event.target.value); - }} - error={errors["name"] ? true : false} - helperText={errors["name"]} - /> - - - - - - Add monitors - - - - - - - - + + + + + Monitors to apply maintenance window to + + + + + + ({ - border: 1, - borderStyle: "solid", - borderColor: theme.palette.border.light, - borderRadius: theme.spacing(2), - backgroundColor: theme.palette.background.main, - "& > *": { padding: theme.spacing(14) }, - "& > :first-of-type, & > .MuiStack-root > div:first-of-type": { - flex: 0.6, - }, - "& > div:last-of-type, & > .MuiStack-root > div:last-of-type": { - flex: 1, - }, - "& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) }, - "& > .MuiStack-root > div:last-of-type": { - paddingLeft: theme.spacing(14), - }, - "& h2": { fontSize: 13.5, fontWeight: 500 }, - "& h3, & p": { - color: theme.palette.text.tertiary, - }, - "& h3": { fontWeight: 500 }, -})); diff --git a/Client/src/Pages/Maintenance/index.jsx b/Client/src/Pages/Maintenance/index.jsx index fd0d7edc3..5c2930294 100644 --- a/Client/src/Pages/Maintenance/index.jsx +++ b/Client/src/Pages/Maintenance/index.jsx @@ -8,13 +8,14 @@ import { useSelector } from "react-redux"; import { networkService } from "../../main"; import Breadcrumbs from "../../Components/Breadcrumbs"; import { useNavigate } from "react-router-dom"; +import { useIsAdmin } from "../../Hooks/useIsAdmin"; -const Maintenance = ({ isAdmin }) => { +const Maintenance = () => { const theme = useTheme(); const navigate = useNavigate(); const { authToken } = useSelector((state) => state.auth); const { rowsPerPage } = useSelector((state) => state.ui.maintenance); - + const isAdmin = useIsAdmin(); const [maintenanceWindows, setMaintenanceWindows] = useState([]); const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0); const [page, setPage] = useState(0); @@ -99,7 +100,7 @@ const Maintenance = ({ isAdmin }) => { "Stop sending alerts in maintenance windows", ]} link="/maintenance/create" - isAdmin={true} + isAdmin={isAdmin} /> )} diff --git a/Client/src/Pages/Monitors/Details/index.css b/Client/src/Pages/Monitors/Details/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Pages/Monitors/Details/styled.jsx b/Client/src/Pages/Monitors/Details/styled.jsx deleted file mode 100644 index 0361d0156..000000000 --- a/Client/src/Pages/Monitors/Details/styled.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Box, Stack, styled } from "@mui/material"; - -export const ChartBox = styled(Stack)(({ theme }) => ({ - flex: "1 30%", - gap: theme.spacing(8), - height: 300, - minWidth: 250, - padding: theme.spacing(8), - border: 1, - borderStyle: "solid", - borderColor: theme.palette.border.light, - borderRadius: 4, - backgroundColor: theme.palette.background.main, - "& h2": { - color: theme.palette.text.secondary, - fontSize: 15, - fontWeight: 500, - }, - "& .MuiBox-root:not(.area-tooltip) p": { - color: theme.palette.text.tertiary, - fontSize: 13, - }, - "& .MuiBox-root > span": { - color: theme.palette.text.primary, - fontSize: 20, - "& span": { - opacity: 0.8, - marginLeft: 2, - fontSize: 15, - }, - }, - "& .MuiStack-root": { - flexDirection: "row", - gap: theme.spacing(6), - }, - "& .MuiStack-root:first-of-type": { - alignItems: "center", - }, - "& tspan, & text": { - fill: theme.palette.text.tertiary, - }, - "& path": { - transition: "fill 300ms ease, stroke-width 400ms ease", - }, -})); - -export const IconBox = styled(Box)(({ theme }) => ({ - height: 34, - minWidth: 34, - width: 34, - position: "relative", - border: 1, - borderStyle: "solid", - borderColor: theme.palette.border.dark, - borderRadius: 4, - backgroundColor: theme.palette.background.accent, - "& svg": { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: 20, - height: 20, - "& path": { - stroke: theme.palette.text.tertiary, - }, - }, -})); - -export const StatBox = styled(Box)(({ theme }) => ({ - padding: `${theme.spacing(4)} ${theme.spacing(8)}`, - minWidth: 200, - width: 225, - border: 1, - borderStyle: "solid", - borderColor: theme.palette.border.light, - borderRadius: 4, - backgroundColor: theme.palette.background.main, - background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`, - "& h2": { - fontSize: 13, - fontWeight: 500, - color: theme.palette.text.secondary, - textTransform: "uppercase", - }, - "& p": { - fontSize: 18, - color: theme.palette.text.primary, - marginTop: theme.spacing(2), - "& span": { - color: theme.palette.text.tertiary, - marginLeft: theme.spacing(2), - fontSize: 15, - }, - }, -})); diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index 757804c97..3cb40f8d2 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -13,7 +13,7 @@ import { import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; -import { ConfigBox } from "../../Monitors/styled"; +import ConfigBox from "../../../Components/ConfigBox"; import TextInput from "../../../Components/Inputs/TextInput"; import Select from "../../../Components/Inputs/Select"; import Checkbox from "../../../Components/Inputs/Checkbox"; @@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import LoadingButton from "@mui/lab/LoadingButton"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import SkeletonLayout from "./skeleton"; -import useUtils from "../../Monitors/utils"; +import useUtils from "../../Uptime/utils"; import "./index.css"; import Dialog from "../../../Components/Dialog"; @@ -135,6 +135,8 @@ const PageSpeedConfigure = () => { if (pausePageSpeed.fulfilled.match(action)) { const monitor = action.payload.data; setMonitor(monitor); + const state = action?.payload?.data.isActive === false ? "paused" : "resumed"; + createToast({ body: `Monitor ${state} successfully.` }); } else if (pausePageSpeed.rejected.match(action)) { throw new Error(action.error.message); } diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index ba3a7bb4a..e5d4a6d79 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -1,42 +1,50 @@ -import { useState } from "react"; -import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; -import LoadingButton from "@mui/lab/LoadingButton"; -import { useSelector, useDispatch } from "react-redux"; -import { monitorValidation } from "../../../Validation/validation"; +// React, Redux, Router import { useNavigate } from "react-router-dom"; -import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; + +// Utility and Network +import { monitorValidation } from "../../../Validation/validation"; import { createPageSpeed, checkEndpointResolution, } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; -import { createToast } from "../../../Utils/toastUtils"; -import { logger } from "../../../Utils/Logger"; -import { ConfigBox } from "../../Monitors/styled"; -import Radio from "../../../Components/Inputs/Radio"; +import { parseDomainName } from "../../../Utils/monitorUtils"; + +// MUI +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; +import LoadingButton from "@mui/lab/LoadingButton"; + +//Components +import Breadcrumbs from "../../../Components/Breadcrumbs"; import TextInput from "../../../Components/Inputs/TextInput"; import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import Select from "../../../Components/Inputs/Select"; +import ConfigBox from "../../../Components/ConfigBox"; +import { createToast } from "../../../Utils/toastUtils"; +import Radio from "../../../Components/Inputs/Radio"; import Checkbox from "../../../Components/Inputs/Checkbox"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import { parseDomainName } from "../../../Utils/monitorUtils"; -import "./index.css"; +import Select from "../../../Components/Inputs/Select"; + +const MS_PER_MINUTE = 60000; + +const CRUMBS = [ + { name: "pagespeed", path: "/pagespeed" }, + { name: "create", path: `/pagespeed/create` }, +]; + +const SELECT_VALUES = [ + { _id: 3, name: "3 minutes" }, + { _id: 5, name: "5 minutes" }, + { _id: 10, name: "10 minutes" }, + { _id: 20, name: "20 minutes" }, + { _id: 60, name: "1 hour" }, + { _id: 1440, name: "1 day" }, + { _id: 10080, name: "1 week" }, +]; const CreatePageSpeed = () => { - const MS_PER_MINUTE = 60000; - const { user, authToken } = useSelector((state) => state.auth); - const { isLoading } = useSelector((state) => state.pageSpeedMonitors); - const dispatch = useDispatch(); - const navigate = useNavigate(); - const theme = useTheme(); - - const idMap = { - "monitor-url": "url", - "monitor-name": "name", - "monitor-checks-http": "type", - "monitor-checks-ping": "type", - "notify-email-default": "notification-email", - }; - + // State const [monitor, setMonitor] = useState({ url: "", name: "", @@ -44,71 +52,20 @@ const CreatePageSpeed = () => { notifications: [], interval: 3, }); + const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); + const { user, authToken } = useSelector((state) => state.auth); + const { isLoading } = useSelector((state) => state.pageSpeedMonitors); - const handleChange = (event, name) => { - const { value, id } = event.target; - if (!name) name = idMap[id]; - - if (name.includes("notification-")) { - name = name.replace("notification-", ""); - let hasNotif = monitor.notifications.some( - (notification) => notification.type === name - ); - setMonitor((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 { - setMonitor((prev) => ({ - ...prev, - [name]: value, - })); - - const { error } = monitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => { - const updatedErrors = { ...prev }; - if (error) updatedErrors[name] = error.details[0].message; - else delete updatedErrors[name]; - return updatedErrors; - }); - } - }; - - const onUrlBlur = (event) => { - const { value } = event.target; - if (monitor.name === "") { - setMonitor((prev) => ({ - ...prev, - name: parseDomainName(value), - })); - } - }; + // Setup + const dispatch = useDispatch(); + const navigate = useNavigate(); + const theme = useTheme(); + // Handlers const handleCreateMonitor = async (event) => { event.preventDefault(); - //obj to submit let form = { url: `http${https ? "s" : ""}://` + monitor.url, name: monitor.name === "" ? monitor.url : monitor.name, @@ -126,46 +83,97 @@ const CreatePageSpeed = () => { newErrors[err.path[0]] = err.message; }); setErrors(newErrors); - createToast({ body: "Error validation data." }); - } else { - const checkEndpointAction = await dispatch( - checkEndpointResolution({ 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; - } + createToast({ body: "Please check the form for errors." }); + return; + } - form = { - ...form, - description: form.name, - teamId: user.teamId, - userId: user._id, - notifications: monitor.notifications, - }; - const action = await dispatch(createPageSpeed({ authToken, monitor: form })); - if (action.meta.requestStatus === "fulfilled") { - createToast({ body: "Monitor created successfully!" }); - navigate("/pagespeed"); - } else { - createToast({ body: "Failed to create monitor." }); - } + const checkEndpointAction = await dispatch( + checkEndpointResolution({ 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; + } + + form = { + ...form, + description: form.name, + teamId: user.teamId, + userId: user._id, + notifications: monitor.notifications, + }; + + const action = await dispatch(createPageSpeed({ authToken, monitor: form })); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor created successfully!" }); + navigate("/pagespeed"); + } else { + createToast({ body: "Failed to create monitor." }); + } + }; + + const handleChange = (event) => { + const { value, name } = event.target; + setMonitor({ + ...monitor, + [name]: value, + }); + + const { error } = monitorValidation.validate( + { [name]: value }, + { abortEarly: false } + ); + + setErrors((prev) => ({ + ...prev, + ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), + })); + }; + + const handleNotifications = (event, type) => { + const { value } = event.target; + let notifications = [...monitor.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 }); + } + + setMonitor((prev) => ({ + ...prev, + notifications, + })); + }; + + const handleBlur = (event) => { + const { name } = event.target; + if (name === "url") { + const { value } = event.target; + if (monitor.name !== "") { + return; + } + setMonitor((prev) => ({ + ...prev, + name: parseDomainName(value), + })); } }; - //select values - const frequencies = [ - { _id: 3, name: "3 minutes" }, - { _id: 5, name: "5 minutes" }, - { _id: 10, name: "10 minutes" }, - { _id: 20, name: "20 minutes" }, - { _id: 60, name: "1 hour" }, - { _id: 1440, name: "1 day" }, - { _id: 10080, name: "1 week" }, - ]; return ( { }, }} > - + { fontWeight="inherit" color={theme.palette.text.secondary} > - pagespeed monitor + PageSpeed monitor @@ -219,19 +222,21 @@ const CreatePageSpeed = () => { } placeholder="google.com" value={monitor.url} onChange={handleChange} - onBlur={onUrlBlur} + onBlur={handleBlur} error={errors["url"] ? true : false} helperText={errors["url"]} /> { handleChange(event)} />