Merge branch 'develop' into feat/dark-mode-switch
This commit is contained in:
commit
4e85e60735
46 files changed
+1101
-1581
No files matched your search
+1
-1
@@ -1,3 +1,3 @@
|
||||
Client/node_modules/
|
||||
Server/node_modules/
|
||||
*.env
|
||||
Server/*.env
|
||||
Generated
+69
-69
@@ -11,12 +11,12 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "6.2.0",
|
||||
"@mui/lab": "6.0.0-beta.19",
|
||||
"@mui/material": "6.2.0",
|
||||
"@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.2",
|
||||
"@mui/x-date-pickers": "7.23.2",
|
||||
"@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",
|
||||
@@ -1071,15 +1071,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/base": {
|
||||
"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==",
|
||||
"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.2.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/utils": "^6.2.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -1103,9 +1103,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"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==",
|
||||
"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.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.2.0.tgz",
|
||||
"integrity": "sha512-WR1EEhGOSvxAsoTSzWZBlrWFjul8wziDrII4rC3PvMBHhBYBqEc2n/0aamfFbwkH5EiYb96aqc6kYY6tB310Sw==",
|
||||
"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.2.0",
|
||||
"@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.19",
|
||||
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.19.tgz",
|
||||
"integrity": "sha512-t7iub8kjpLdA5uDGwGnNRjtGc1vYEUnDwSROjKrnYqjOlCQhBajFa8uoQtaST6jE/VEk6cxpDMnN5MalC6YpCg==",
|
||||
"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.66",
|
||||
"@mui/system": "^6.2.0",
|
||||
"@mui/types": "^7.2.19",
|
||||
"@mui/utils": "^6.2.0",
|
||||
"@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.2.0",
|
||||
"@mui/material-pigment-css": "^6.2.0",
|
||||
"@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,18 +1184,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.2.0.tgz",
|
||||
"integrity": "sha512-7FXXUPIyYzP02a7GvqwJ7ocmdP+FkvLvmy/uxG1TDmTlsr8nEClQp75uxiVznJqAY/jJy4d+Rj/fNWNxwidrYQ==",
|
||||
"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.2.0",
|
||||
"@mui/system": "^6.2.0",
|
||||
"@mui/types": "^7.2.19",
|
||||
"@mui/utils": "^6.2.0",
|
||||
"@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",
|
||||
@@ -1212,7 +1212,7 @@
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^6.2.0",
|
||||
"@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"
|
||||
@@ -1239,13 +1239,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.2.0.tgz",
|
||||
"integrity": "sha512-lYd2MrVddhentF1d/cMXKnwlDjr/shbO3A2eGq22PCYUoZaqtAGZMc0U86KnJ/Sh5YzNYePqTOaaowAN8Qea8A==",
|
||||
"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.2.0",
|
||||
"@mui/utils": "^6.2.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1266,9 +1266,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"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==",
|
||||
"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",
|
||||
@@ -1300,16 +1300,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.2.0.tgz",
|
||||
"integrity": "sha512-DCeqev9Cd4f4pm3O1lqSGW/DIHHBG6ZpqMX9iIAvN4asYv+pPWv2/lKov9kWk5XThhxFnGSv93SRNE1kNRRg5Q==",
|
||||
"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.2.0",
|
||||
"@mui/styled-engine": "^6.2.0",
|
||||
"@mui/types": "^7.2.19",
|
||||
"@mui/utils": "^6.2.0",
|
||||
"@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"
|
||||
@@ -1340,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"
|
||||
@@ -1354,13 +1354,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.2.0.tgz",
|
||||
"integrity": "sha512-77CaFJi+OIi2SjbPwCis8z5DXvE0dfx9hBz5FguZHt1VYFlWEPCWTHcMsQCahSErnfik5ebLsYK8+D+nsjGVfw==",
|
||||
"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",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -1448,9 +1448,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"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==",
|
||||
"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",
|
||||
@@ -1485,9 +1485,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.2.tgz",
|
||||
"integrity": "sha512-Kt9VsEnShaBKiaastTYku66UIWptgc9UMA16d0G/0TkfIsvZrAD3iacQR6HHAXWspaFshdfsRmW2JAoFhzKZsg==",
|
||||
"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",
|
||||
@@ -2372,9 +2372,9 @@
|
||||
"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": "*",
|
||||
@@ -2392,11 +2392,11 @@
|
||||
}
|
||||
},
|
||||
"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": "*"
|
||||
}
|
||||
},
|
||||
|
||||
+5
-5
@@ -14,12 +14,12 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "6.2.0",
|
||||
"@mui/lab": "6.0.0-beta.19",
|
||||
"@mui/material": "6.2.0",
|
||||
"@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.2",
|
||||
"@mui/x-date-pickers": "7.23.2",
|
||||
"@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",
|
||||
|
||||
@@ -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) || ''}
|
||||
</MuiAvatar>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
* <BasicTable data={data} rows={rows} paginated={true} />
|
||||
*/
|
||||
|
||||
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();
|
||||
@@ -228,6 +228,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{displayData.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ textAlign: "center" }}
|
||||
colSpan={data.cols.length}
|
||||
>
|
||||
{emptyMessage}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -317,6 +327,7 @@ BasicTable.propTypes = {
|
||||
reversed: PropTypes.bool,
|
||||
rowPerPage: PropTypes.number,
|
||||
table: PropTypes.string,
|
||||
emptyMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BasicTable;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
* <IconBox>
|
||||
* <SomeIcon />
|
||||
* </IconBox>
|
||||
*
|
||||
* @example
|
||||
* // Customized usage
|
||||
* <IconBox
|
||||
* height={40}
|
||||
* width={40}
|
||||
* svgWidth={24}
|
||||
* svgHeight={24}
|
||||
* >
|
||||
* <CustomIcon />
|
||||
* </IconBox>
|
||||
*
|
||||
* @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;
|
||||
@@ -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),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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) => (
|
||||
<Stack>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
|
||||
@@ -26,7 +26,7 @@ const ProtectedRoute = ({ children }) => {
|
||||
};
|
||||
|
||||
ProtectedRoute.propTypes = {
|
||||
children: PropTypes.elementType.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@@ -28,7 +28,6 @@ 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";
|
||||
@@ -178,13 +177,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" }}
|
||||
>
|
||||
<Stack
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* StatBox Component
|
||||
*
|
||||
* A reusable component that displays a statistic with a heading and subheading
|
||||
* in a styled box with a gradient background.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - The component props
|
||||
* @param {string} props.heading - The primary heading/title of the statistic
|
||||
* @param {string|React.ReactNode} props.subHeading - The value or description of the statistic
|
||||
* @param {Object} [props.sx] - Additional custom styling to be applied to the box
|
||||
*
|
||||
* @example
|
||||
* return (
|
||||
* <StatBox
|
||||
* heading="Total Users"
|
||||
* subHeading="1,234"
|
||||
* sx={{ width: 300 }}
|
||||
* />
|
||||
* )
|
||||
*
|
||||
* @returns {React.ReactElement} A styled box containing the statistic
|
||||
*/
|
||||
|
||||
const StatBox = ({ heading, subHeading, sx }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
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,
|
||||
},
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Typography>{subHeading}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
heading: PropTypes.string.isRequired,
|
||||
subHeading: PropTypes.node.isRequired,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default StatBox;
|
||||
@@ -64,7 +64,7 @@ const TeamPanel = () => {
|
||||
};
|
||||
|
||||
fetchTeam();
|
||||
}, [user]);
|
||||
}, [authToken]);
|
||||
|
||||
useEffect(() => {
|
||||
let team = members;
|
||||
@@ -81,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(",");
|
||||
@@ -104,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: (
|
||||
// <IconButton
|
||||
// aria-label="remove member"
|
||||
// sx={{
|
||||
// "&:focus": {
|
||||
// outline: "none",
|
||||
// },
|
||||
// }}
|
||||
// >
|
||||
// <Remove />
|
||||
// </IconButton>
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
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) => {
|
||||
@@ -226,58 +198,11 @@ const TeamPanel = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* FEATURE STILL TO BE IMPLEMENTED */}
|
||||
{/* <Stack component="form">
|
||||
<Box sx={{ alignSelf: "flex-start" }}>
|
||||
<Typography component="h1">Organization name</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
sx={{ height: "34px" }}
|
||||
>
|
||||
<TextField
|
||||
value={orgStates.name}
|
||||
onChange={(event) =>
|
||||
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) },
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level={orgStates.isEdit ? "secondary" : "tertiary"}
|
||||
label={orgStates.isEdit ? "Save" : ""}
|
||||
img={!orgStates.isEdit ? <EditSvg /> : ""}
|
||||
onClick={() => toggleEdit()}
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
paddingX: theme.spacing(4),
|
||||
ml: orgStates.isEdit ? theme.spacing(4) : 0,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} /> */}
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={SPACING_GAP}
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Typography component="h1">Team members</Typography>
|
||||
<Stack
|
||||
@@ -328,6 +253,7 @@ const TeamPanel = () => {
|
||||
paginated={false}
|
||||
reversed={true}
|
||||
table={"team"}
|
||||
emptyMessage={"There are no team members with this role"}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -341,7 +267,7 @@ const TeamPanel = () => {
|
||||
theme={theme}
|
||||
>
|
||||
<TextInput
|
||||
marginBottom={SPACING_GAP}
|
||||
marginBottom={SPACING_GAP}
|
||||
type="email"
|
||||
id="input-team-member"
|
||||
placeholder="Email"
|
||||
|
||||
@@ -92,6 +92,7 @@ const Account = ({ open = "profile" }) => {
|
||||
paddingY: theme.spacing(4),
|
||||
fontWeight: 400,
|
||||
marginRight: theme.spacing(8),
|
||||
borderBottom: "2px solid transparent",
|
||||
"&:focus": {
|
||||
borderBottom: `2px solid ${theme.palette.border.light}`,
|
||||
},
|
||||
|
||||
Whitespace-only changes.
@@ -1,361 +0,0 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Link from "../../Components/Link";
|
||||
import "./index.css";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { ConfigBox } from "../Settings/styled";
|
||||
import { useNavigate } from "react-router";
|
||||
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import { advancedSettingsValidation } from "../../Validation/validation";
|
||||
import { buildErrors, hasValidationErrors } from "../../Validation/error";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
|
||||
const AdvancedSettings = () => {
|
||||
const navigate = useNavigate();
|
||||
const isAdmin = useIsAdmin();
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
const [errors, setErrors] = useState({});
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTLNum: 99,
|
||||
jwtTTLUnits: "days",
|
||||
jwtTTL: "99d",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
|
||||
const parseJWTTTL = (data) => {
|
||||
if (data.jwtTTL) {
|
||||
const len = data.jwtTTL.length;
|
||||
data.jwtTTLNum = data.jwtTTL.substring(0, len - 1);
|
||||
data.jwtTTLUnits = unitItems.filter(
|
||||
(itm) => itm._id == data.jwtTTL.substring(len - 1)
|
||||
)[0].name;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload.success) {
|
||||
parseJWTTTL(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
|
||||
const logItems = [
|
||||
{ _id: 1, name: "none" },
|
||||
{ _id: 2, name: "debug" },
|
||||
{ _id: 3, name: "error" },
|
||||
{ _id: 4, name: "warn" },
|
||||
];
|
||||
|
||||
const logItemLookup = {
|
||||
none: 1,
|
||||
debug: 2,
|
||||
error: 3,
|
||||
warn: 4,
|
||||
};
|
||||
|
||||
const unitItemLookup = {
|
||||
days: "d",
|
||||
hours: "h",
|
||||
};
|
||||
const unitItems = Object.keys(unitItemLookup).map((key) => ({
|
||||
_id: unitItemLookup[key],
|
||||
name: key,
|
||||
}));
|
||||
|
||||
const handleLogLevel = (e) => {
|
||||
const id = e.target.value;
|
||||
const newLogLevel = logItems.find((item) => item._id === id).name;
|
||||
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
|
||||
};
|
||||
|
||||
const handleJWTTTLUnits = (e) => {
|
||||
const id = e.target.value;
|
||||
const newUnits = unitItems.find((item) => item._id === id).name;
|
||||
setLocalSettings({ ...localSettings, jwtTTLUnits: newUnits });
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const { error } = advancedSettingsValidation.validate(
|
||||
{ [id]: value },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, id, error);
|
||||
});
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
localSettings.jwtTTL =
|
||||
localSettings.jwtTTLNum + unitItemLookup[localSettings.jwtTTLUnits];
|
||||
if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) {
|
||||
return;
|
||||
}
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
parseJWTTTL(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<TextInput
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.apiBaseUrl ? true : false}
|
||||
helperText={errors.apiBaseUrl}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
onBlur={handleBlur}
|
||||
error={errors.logLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for sending
|
||||
system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="System email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailHost ? true : false}
|
||||
helperText={errors.systemEmailHost}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email port"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort?.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailPort ? true : false}
|
||||
helperText={errors.systemEmailPort}
|
||||
/>
|
||||
<TextInput
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailAddress ? true : false}
|
||||
helperText={errors.systemEmailAddress}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailPassword ? true : false}
|
||||
helperText={errors.systemEmailPassword}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<TextInput
|
||||
type="number"
|
||||
id="jwtTTLNum"
|
||||
label="JWT time to live"
|
||||
name="jwtTTLNum"
|
||||
value={localSettings.jwtTTLNum.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.jwtTTLNum ? true : false}
|
||||
helperText={errors.jwtTTLNum}
|
||||
/>
|
||||
<Select
|
||||
id="jwtTTLUnits"
|
||||
label="JWT TTL Units"
|
||||
name="jwtTTLUnits"
|
||||
placeholder="Select time"
|
||||
isHidden={true}
|
||||
items={unitItems}
|
||||
value={unitItemLookup[localSettings.jwtTTLUnits]}
|
||||
onChange={handleJWTTTLUnits}
|
||||
onBlur={handleBlur}
|
||||
error={errors.jwtTTLUnits}
|
||||
labelControlSpacing={0}
|
||||
/>
|
||||
</Stack>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.dbType ? true : false}
|
||||
helperText={errors.dbType}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.redisHost ? true : false}
|
||||
helperText={errors.redisHost}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort?.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.redisPort ? true : false}
|
||||
helperText={errors.redisPort}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.pagespeedApiKey ? true : false}
|
||||
helperText={errors.pagespeedApiKey}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedSettings.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default AdvancedSettings;
|
||||
@@ -5,10 +5,10 @@ import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { forgotPassword } from "../../Features/Auth/authSlice";
|
||||
import { IconBox } from "./styled";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import EmailIcon from "../../assets/icons/email.svg?react";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import IconBox from "../../Components/IconBox";
|
||||
import "./index.css";
|
||||
|
||||
const CheckEmail = () => {
|
||||
@@ -144,9 +144,22 @@ const CheckEmail = () => {
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<EmailIcon alt="email icon" />
|
||||
</IconBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<IconBox
|
||||
height={48}
|
||||
width={48}
|
||||
minWidth={48}
|
||||
borderRadius={12}
|
||||
svgWidth={24}
|
||||
svgHeight={24}
|
||||
mb={theme.spacing(4)}
|
||||
>
|
||||
<EmailIcon alt="email icon" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">Check your email</Typography>
|
||||
<Typography>
|
||||
We sent a password reset link to{" "}
|
||||
|
||||
@@ -6,12 +6,12 @@ import { forgotPassword } from "../../Features/Auth/authSlice";
|
||||
import { useEffect, useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { IconBox } from "./styled";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import Key from "../../assets/icons/key.svg?react";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import IconBox from "../../Components/IconBox";
|
||||
import "./index.css";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
@@ -146,9 +146,22 @@ const ForgotPassword = () => {
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<Key alt="password key icon" />
|
||||
</IconBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<IconBox
|
||||
height={48}
|
||||
width={48}
|
||||
minWidth={48}
|
||||
borderRadius={12}
|
||||
svgWidth={24}
|
||||
svgHeight={24}
|
||||
mb={theme.spacing(4)}
|
||||
>
|
||||
<Key alt="password key icon" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">Forgot password?</Typography>
|
||||
<Typography>No worries, we'll send you reset instructions.</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -1,559 +0,0 @@
|
||||
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";
|
||||
import ThemeSwitch from "../../Components/ThemeSwitch";
|
||||
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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>We are pleased to see you again!</Typography>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onContinue}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& svg": {
|
||||
mr: theme.spacing(4),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Mail />
|
||||
Continue with Email
|
||||
</Button>
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
By continuing, you agree to our{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/terms-of-service-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Terms of Service
|
||||
</Typography>{" "}
|
||||
and{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/privacy-policy-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={errors.email && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
textAlign="left"
|
||||
mb={theme.spacing(5)}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gap: { xs: theme.spacing(12), sm: theme.spacing(16) },
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password}
|
||||
ref={inputRef}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
loading={authState.isLoading}
|
||||
disabled={errors.password && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box textAlign="center">
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
Reset password
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Stack
|
||||
className="login-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 28,
|
||||
},
|
||||
"& p": { fontSize: 14, color: theme.palette.text.accent },
|
||||
"& span": { fontSize: "inherit" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
gap={theme.spacing(8)}
|
||||
sx={{
|
||||
"& > .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 ? (
|
||||
<LandingPage onContinue={() => setStep(1)} />
|
||||
) : step === 1 ? (
|
||||
<StepOne
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(0)}
|
||||
/>
|
||||
) : (
|
||||
step === 2 && (
|
||||
<StepTwo
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(1)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Box marginX={"auto"}>
|
||||
<ThemeSwitch />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,131 @@
|
||||
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";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (form.email !== "" && !errors.email) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
}
|
||||
navigate("/forgot-password");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
textAlign="center"
|
||||
position="relative"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="left"
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
display="grid"
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
id="login-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={errors.email && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="center"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
transform: `translate(-50%, 150%)`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
Reset password
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EmailStep.propTypes = {
|
||||
form: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EmailStep;
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
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 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 (
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your password</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck={false}
|
||||
onSubmit={onSubmit}
|
||||
textAlign="left"
|
||||
mb={theme.spacing(5)}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gap: { xs: theme.spacing(12), sm: theme.spacing(16) },
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="login-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password}
|
||||
ref={inputRef}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
"&:focus-visible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
loading={authState.isLoading}
|
||||
disabled={errors.password && true}
|
||||
sx={{
|
||||
width: "30%",
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
textAlign="center"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
transform: `translate(-50%, 150%)`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
Reset password
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
@@ -0,0 +1,213 @@
|
||||
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";
|
||||
|
||||
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 (
|
||||
<Stack
|
||||
className="login-page auth"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 28,
|
||||
},
|
||||
"& p": { fontSize: 14, color: theme.palette.text.accent },
|
||||
"& span": { fontSize: "inherit" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .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 ? (
|
||||
<EmailStep
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
step === 1 && (
|
||||
<PasswordStep
|
||||
form={form}
|
||||
errors={errors}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={handleChange}
|
||||
onBack={() => setStep(0)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -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"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<ConfirmIcon alt="password confirm icon" />
|
||||
</IconBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<IconBox
|
||||
height={48}
|
||||
width={48}
|
||||
minWidth={48}
|
||||
borderRadius={12}
|
||||
svgWidth={24}
|
||||
svgHeight={24}
|
||||
mb={theme.spacing(4)}
|
||||
>
|
||||
<ConfirmIcon alt="password confirm icon" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">Password reset</Typography>
|
||||
<Typography mb={theme.spacing(2)}>
|
||||
Your password has been successfully reset. Click below to log in magically.
|
||||
@@ -91,7 +104,7 @@ const NewPasswordConfirmed = () => {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/monitors")}
|
||||
onClick={() => navigate("/uptime")}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
|
||||
@@ -11,8 +11,7 @@ import { credentials } from "../../Validation/validation";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
|
||||
|
||||
import { IconBox } from "./styled";
|
||||
import IconBox from "../../Components/IconBox";
|
||||
import LockIcon from "../../assets/icons/lock.svg?react";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
@@ -126,9 +125,22 @@ const SetNewPassword = () => {
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<IconBox>
|
||||
<LockIcon alt="lock icon" />
|
||||
</IconBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<IconBox
|
||||
height={48}
|
||||
width={48}
|
||||
minWidth={48}
|
||||
borderRadius={12}
|
||||
svgWidth={24}
|
||||
svgHeight={24}
|
||||
mb={theme.spacing(4)}
|
||||
>
|
||||
<LockIcon alt="lock icon" />
|
||||
</IconBox>
|
||||
</Stack>
|
||||
<Typography component="h1">Set new password</Typography>
|
||||
<Typography>
|
||||
Your new password must be different to previously used passwords.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Box, styled } from "@mui/material";
|
||||
|
||||
export const IconBox = styled(Box)(({ theme }) => ({
|
||||
height: 48,
|
||||
minWidth: 48,
|
||||
width: 48,
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 12,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
margin: "auto",
|
||||
marginBottom: 8,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 24,
|
||||
height: 24,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -16,7 +16,7 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
//Components
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import Link from "../../../Components/Link";
|
||||
import { ConfigBox } from "../../Uptime/styled";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
|
||||
@@ -21,8 +21,7 @@ const Empty = ({ styles }) => {
|
||||
textAlign="center"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Hang tight! When we receive data, we'll show it here. Please check back in a few
|
||||
minutes.
|
||||
Hang tight! Data is loading
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
TemperatureTooltip,
|
||||
} from "../../../Components/Charts/Utils/chartUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import StatBox from "../../../Components/StatBox";
|
||||
|
||||
const BASE_BOX_PADDING_VERTICAL = 4;
|
||||
const BASE_BOX_PADDING_HORIZONTAL = 8;
|
||||
@@ -29,18 +30,50 @@ const TYPOGRAPHY_PADDING = 8;
|
||||
* @param {number} bytes - Number of bytes to convert
|
||||
* @returns {number} Converted value in gigabytes
|
||||
*/
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === undefined || bytes === null) return "0 GB";
|
||||
if (typeof bytes !== "number") return "0 GB";
|
||||
if (bytes === 0) return "0 GB";
|
||||
const formatBytes = (bytes, space = false) => {
|
||||
if (bytes === undefined || bytes === null)
|
||||
return (
|
||||
<>
|
||||
{0}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">GB</Typography>
|
||||
</>
|
||||
);
|
||||
if (typeof bytes !== "number")
|
||||
return (
|
||||
<>
|
||||
{0}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">GB</Typography>
|
||||
</>
|
||||
);
|
||||
if (bytes === 0)
|
||||
return (
|
||||
<>
|
||||
{0}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">GB</Typography>
|
||||
</>
|
||||
);
|
||||
|
||||
const GB = bytes / (1024 * 1024 * 1024);
|
||||
const MB = bytes / (1024 * 1024);
|
||||
|
||||
if (GB >= 1) {
|
||||
return `${Number(GB.toFixed(0))} GB`;
|
||||
return (
|
||||
<>
|
||||
{Number(GB.toFixed(0))}
|
||||
{space ? " " : ""}
|
||||
<Typography component="span">GB</Typography>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return `${Number(MB.toFixed(0))} MB`;
|
||||
return (
|
||||
<>
|
||||
{Number(MB.toFixed(0))}
|
||||
<Typography component="span">MB</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,27 +126,6 @@ BaseBox.propTypes = {
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a statistic box with a heading and subheading
|
||||
* @param {Object} props - Component properties
|
||||
* @param {string} props.heading - Primary heading text
|
||||
* @param {string} props.subHeading - Secondary heading text
|
||||
* @returns {React.ReactElement} Stat box component
|
||||
*/
|
||||
const StatBox = ({ heading, subHeading }) => {
|
||||
return (
|
||||
<BaseBox>
|
||||
<Typography component="h2">{heading}</Typography>
|
||||
<Typography>{subHeading}</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
heading: PropTypes.string.isRequired,
|
||||
subHeading: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a gauge box with usage visualization
|
||||
* @param {Object} props - Component properties
|
||||
@@ -193,7 +205,7 @@ const InfrastructureDetails = () => {
|
||||
const [monitor, setMonitor] = useState(null);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [dateRange, setDateRange] = useState("all");
|
||||
const { statusColor, determineState } = useUtils();
|
||||
const { statusColor, statusStyles, determineState } = useUtils();
|
||||
// These calculations are needed because ResponsiveContainer
|
||||
// doesn't take padding of parent/siblings into account
|
||||
// when calculating height.
|
||||
@@ -205,7 +217,7 @@ const InfrastructureDetails = () => {
|
||||
(chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95;
|
||||
// end height calculations
|
||||
|
||||
const buildStatBoxes = (checks) => {
|
||||
const buildStatBoxes = (checks, uptime) => {
|
||||
let latestCheck = checks[0] ?? null;
|
||||
if (latestCheck === null) return [];
|
||||
|
||||
@@ -224,25 +236,51 @@ const InfrastructureDetails = () => {
|
||||
const platform = latestCheck?.host?.platform ?? null;
|
||||
const osPlatform = os === null && platform === null ? null : `${os} ${platform}`;
|
||||
return [
|
||||
{
|
||||
id: 7,
|
||||
sx: statusStyles[determineState(monitor)],
|
||||
heading: "Status",
|
||||
subHeading: monitor?.status === true ? "Active" : "Inactive",
|
||||
},
|
||||
{
|
||||
id: 0,
|
||||
heading: "CPU (Physical)",
|
||||
subHeading: `${physicalCores} cores`,
|
||||
subHeading: (
|
||||
<>
|
||||
{physicalCores}
|
||||
<Typography component="span">cores</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
heading: "CPU (Logical)",
|
||||
subHeading: `${logicalCores} cores`,
|
||||
subHeading: (
|
||||
<>
|
||||
{logicalCores}
|
||||
<Typography component="span">cores</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
heading: "CPU Frequency",
|
||||
subHeading: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
|
||||
subHeading: (
|
||||
<>
|
||||
{(cpuFrequency / 1000).toFixed(2)}
|
||||
<Typography component="span">Ghz</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
heading: "Average CPU Temperature",
|
||||
subHeading: `${cpuTemperature.toFixed(2)} C`,
|
||||
subHeading: (
|
||||
<>
|
||||
{cpuTemperature.toFixed(2)}
|
||||
<Typography component="span">C</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -254,12 +292,17 @@ const InfrastructureDetails = () => {
|
||||
heading: "Disk",
|
||||
subHeading: formatBytes(diskTotalBytes),
|
||||
},
|
||||
{ id: 6, heading: "Uptime", subHeading: "100%" },
|
||||
{
|
||||
id: 7,
|
||||
heading: "Status",
|
||||
subHeading: monitor?.status === true ? "Active" : "Inactive",
|
||||
id: 6,
|
||||
heading: "Uptime",
|
||||
subHeading: (
|
||||
<>
|
||||
{(uptime * 100).toFixed(2)}
|
||||
<Typography component="span">%</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: 8,
|
||||
heading: "OS",
|
||||
@@ -285,9 +328,9 @@ const InfrastructureDetails = () => {
|
||||
value: decimalToPercentage(memoryUsagePercent),
|
||||
heading: "Memory usage",
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(memoryUsedBytes),
|
||||
valueOne: formatBytes(memoryUsedBytes, true),
|
||||
metricTwo: "Total",
|
||||
valueTwo: formatBytes(memoryTotalBytes),
|
||||
valueTwo: formatBytes(memoryTotalBytes, true),
|
||||
},
|
||||
{
|
||||
type: "cpu",
|
||||
@@ -304,9 +347,9 @@ const InfrastructureDetails = () => {
|
||||
value: decimalToPercentage(disk.usage_percent),
|
||||
heading: `Disk${idx} usage`,
|
||||
metricOne: "Used",
|
||||
valueOne: formatBytes(disk.total_bytes - disk.free_bytes),
|
||||
valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true),
|
||||
metricTwo: "Total",
|
||||
valueTwo: formatBytes(disk.total_bytes),
|
||||
valueTwo: formatBytes(disk.total_bytes, true),
|
||||
})),
|
||||
];
|
||||
};
|
||||
@@ -466,7 +509,10 @@ const InfrastructureDetails = () => {
|
||||
fetchData();
|
||||
}, [authToken, monitorId, dateRange, navigate]);
|
||||
|
||||
const statBoxConfigs = buildStatBoxes(monitor?.checks ?? []);
|
||||
const statBoxConfigs = buildStatBoxes(
|
||||
monitor?.checks ?? [],
|
||||
monitor?.uptimePercentage ?? "Unknown"
|
||||
);
|
||||
const gaugeBoxConfigs = buildGaugeBoxConfigs(monitor?.checks ?? []);
|
||||
const areaChartConfigs = buildAreaChartConfigs(monitor?.checks ?? []);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ConfigBox } from "./styled";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
@@ -321,18 +321,27 @@ const CreateMaintenance = () => {
|
||||
Your pings won't be sent during this time frame
|
||||
</Typography>
|
||||
</Box>
|
||||
<ConfigBox direction="row">
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
General Settings
|
||||
</Typography>
|
||||
<Box
|
||||
px={theme.spacing(14)}
|
||||
borderLeft={1}
|
||||
borderLeftColor={theme.palette.border.light}
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
General Settings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<TextInput
|
||||
id="name"
|
||||
label="Friendly name"
|
||||
placeholder="Maintenance at __ : __ for ___ minutes"
|
||||
value={form.name}
|
||||
onChange={(event) => {
|
||||
handleFormChange("name", event.target.value);
|
||||
}}
|
||||
error={errors["name"] ? true : false}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
<Select
|
||||
id="repeat"
|
||||
name="maintenance-repeat"
|
||||
@@ -346,15 +355,12 @@ const CreateMaintenance = () => {
|
||||
}}
|
||||
items={repeatConfig}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
mt={theme.spacing(16)}
|
||||
>
|
||||
<Typography component="h3">Date</Typography>
|
||||
<Stack>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DatePicker
|
||||
id="startDate"
|
||||
disablePast
|
||||
label="Date"
|
||||
disableHighlightToday
|
||||
value={form.startDate}
|
||||
slots={{ openPickerIcon: CalendarIcon }}
|
||||
@@ -403,72 +409,64 @@ const CreateMaintenance = () => {
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
Start time
|
||||
</Typography>
|
||||
<Typography>All dates and times are in GMT+0 time zone.</Typography>
|
||||
</Box>
|
||||
<Stack direction="row">
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<MobileTimePicker
|
||||
id="startTime"
|
||||
value={form.startTime}
|
||||
onChange={(newTime) => {
|
||||
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,
|
||||
},
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
Start time
|
||||
</Typography>
|
||||
<Typography>All dates and times are in GMT+0 time zone.</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<MobileTimePicker
|
||||
id="startTime"
|
||||
label="Start time"
|
||||
value={form.startTime}
|
||||
onChange={(newTime) => {
|
||||
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"]}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
Duration
|
||||
</Typography>
|
||||
</Box>
|
||||
},
|
||||
}}
|
||||
error={errors["startTime"]}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="end"
|
||||
spacing={theme.spacing(8)}
|
||||
>
|
||||
<TextInput
|
||||
type="number"
|
||||
id="duration"
|
||||
label="Duration"
|
||||
value={form.duration}
|
||||
onChange={(event) => {
|
||||
handleFormChange("duration", event.target.value);
|
||||
@@ -491,66 +489,34 @@ const CreateMaintenance = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontSize={16}
|
||||
my={theme.spacing(6)}
|
||||
>
|
||||
Monitor related settings
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
Friendly name
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<TextInput
|
||||
id="name"
|
||||
placeholder="Maintenance at __ : __ for ___ minutes"
|
||||
value={form.name}
|
||||
onChange={(event) => {
|
||||
handleFormChange("name", event.target.value);
|
||||
}}
|
||||
error={errors["name"] ? true : false}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
Add monitors
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Search
|
||||
id={"monitors"}
|
||||
multiple={true}
|
||||
isAdorned={false}
|
||||
options={monitors ? monitors : []}
|
||||
filteredBy="name"
|
||||
secondaryLabel={"type"}
|
||||
inputValue={search}
|
||||
value={form.monitors}
|
||||
handleInputChange={handleSearch}
|
||||
handleChange={handleSelectMonitors}
|
||||
error={errors["monitors"]}
|
||||
disabled={maintenanceWindowId !== undefined}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
</Box>
|
||||
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
Monitors to apply maintenance window to
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Search
|
||||
id={"monitors"}
|
||||
label="Add monitors"
|
||||
multiple={true}
|
||||
isAdorned={false}
|
||||
options={monitors ? monitors : []}
|
||||
filteredBy="name"
|
||||
secondaryLabel={"type"}
|
||||
inputValue={search}
|
||||
value={form.monitors}
|
||||
handleInputChange={handleSearch}
|
||||
handleChange={handleSelectMonitors}
|
||||
error={errors["monitors"]}
|
||||
disabled={maintenanceWindowId !== undefined}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Box
|
||||
ml="auto"
|
||||
display="inline-block"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
export const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
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 },
|
||||
}));
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../../Uptime/styled";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import { ConfigBox } from "../../Uptime/styled";
|
||||
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
|
||||
import { ChartBox, IconBox, StatBox } from "./styled";
|
||||
import { ChartBox } from "./styled";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { networkService } from "../../../main";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
@@ -22,6 +22,8 @@ import PieChart from "./Charts/PieChart";
|
||||
import useUtils from "../../Uptime/utils";
|
||||
import "./index.css";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import StatBox from "../../../Components/StatBox";
|
||||
import IconBox from "../../../Components/IconBox";
|
||||
|
||||
const PageSpeedDetails = () => {
|
||||
const theme = useTheme();
|
||||
@@ -187,20 +189,24 @@ const PageSpeedDetails = () => {
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<StatBox>
|
||||
<Typography component="h2">checks since</Typography>
|
||||
<Typography>
|
||||
{splitDuration(monitor?.uptimeDuration)}
|
||||
<Typography component="span">ago</Typography>
|
||||
</Typography>
|
||||
</StatBox>
|
||||
<StatBox>
|
||||
<Typography component="h2">last check</Typography>
|
||||
<Typography>
|
||||
{splitDuration(monitor?.lastChecked)}
|
||||
<Typography component="span">ago</Typography>
|
||||
</Typography>
|
||||
</StatBox>
|
||||
<StatBox
|
||||
heading="checks since"
|
||||
subHeading={
|
||||
<>
|
||||
{splitDuration(monitor?.uptimeDuration)}
|
||||
<Typography component="span">ago</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last check"
|
||||
subHeading={
|
||||
<>
|
||||
{splitDuration(monitor?.lastChecked)}
|
||||
<Typography component="span">ago</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Typography
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Stack, styled } from "@mui/material";
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
export const ChartBox = styled(Stack)(({ theme }) => ({
|
||||
display: "grid",
|
||||
@@ -40,54 +40,3 @@ export const ChartBox = styled(Stack)(({ theme }) => ({
|
||||
transition: "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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -4,13 +4,12 @@ import { StatusLabel } from "../../Components/Label";
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { IconBox } from "./Details/styled";
|
||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils";
|
||||
import useUtils from "../Uptime/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
import IconBox from "../../Components/IconBox";
|
||||
/**
|
||||
* CustomToolTip displays a tooltip with formatted date and score information.
|
||||
* @param {Object} props
|
||||
|
||||
@@ -18,13 +18,12 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { setTimezone, setMode } from "../../Features/UI/uiSlice";
|
||||
import timezones from "../../Utils/timezones.json";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ConfigBox } from "./styled";
|
||||
import { networkService } from "../../main";
|
||||
import { settingsValidation } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
|
||||
const Settings = () => {
|
||||
@@ -324,28 +323,7 @@ const Settings = () => {
|
||||
/>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Advanced settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Click here to modify advanced settings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
navigate("/advanced-settings");
|
||||
}}
|
||||
>
|
||||
Advanced Settings
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
)}
|
||||
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
export const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing(20),
|
||||
paddingTop: theme.spacing(12),
|
||||
paddingInline: theme.spacing(15),
|
||||
paddingBottom: theme.spacing(25),
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.spacing(2),
|
||||
"& > div:first-of-type": {
|
||||
flex: 0.7,
|
||||
},
|
||||
"& > div:last-of-type": {
|
||||
flex: 1,
|
||||
},
|
||||
"& h1, & h2": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
"& p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}));
|
||||
@@ -6,7 +6,7 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../styled";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import {
|
||||
updateUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
@@ -141,6 +141,8 @@ const Configure = () => {
|
||||
if (pauseUptimeMonitor.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 (pauseUptimeMonitor.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
|
||||
@@ -17,14 +17,13 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
//Components
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { ConfigBox } from "../styled";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
const CreateMonitor = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const SELECT_VALUES = [
|
||||
|
||||
@@ -26,14 +26,15 @@ import HistoryIcon from "../../../assets/icons/history-icon.svg?react";
|
||||
import PaginationTable from "./PaginationTable";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import { StatBox, ChartBox, IconBox } from "./styled";
|
||||
import { ChartBox } from "./styled";
|
||||
import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
import useUtils from "../utils";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
|
||||
import IconBox from "../../../Components/IconBox";
|
||||
import StatBox from "../../../Components/StatBox";
|
||||
/**
|
||||
* Details page component displaying monitor details and related information.
|
||||
* @component
|
||||
@@ -283,24 +284,24 @@ const DetailsPage = () => {
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<StatBox sx={statusStyles[determineState(monitor)]}>
|
||||
<Typography component="h2">active for</Typography>
|
||||
<Typography>{splitDuration(monitor?.uptimeDuration)}</Typography>
|
||||
</StatBox>
|
||||
<StatBox>
|
||||
<Typography component="h2">last check</Typography>
|
||||
<Typography>
|
||||
{splitDuration(monitor?.lastChecked)}
|
||||
<Typography component="span">ago</Typography>
|
||||
</Typography>
|
||||
</StatBox>
|
||||
<StatBox>
|
||||
<Typography component="h2">last response time</Typography>
|
||||
<Typography>
|
||||
{monitor?.latestResponseTime}
|
||||
<Typography component="span">ms</Typography>
|
||||
</Typography>
|
||||
</StatBox>
|
||||
<StatBox
|
||||
sx={statusStyles[determineState(monitor)]}
|
||||
heading={"active for"}
|
||||
subHeading={splitDuration(monitor?.uptimeDuration)}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last check"
|
||||
subHeading={splitDuration(monitor?.lastChecked)}
|
||||
/>
|
||||
<StatBox
|
||||
heading="last response time"
|
||||
subHeading={
|
||||
<>
|
||||
{monitor?.latestResponseTime}
|
||||
<Typography component="span">{"ms"}</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Stack, styled } from "@mui/material";
|
||||
import { Stack, styled } from "@mui/material";
|
||||
|
||||
export const ChartBox = styled(Stack)(({ theme }) => ({
|
||||
flex: "1 30%",
|
||||
@@ -43,54 +43,3 @@ export const ChartBox = styled(Stack)(({ theme }) => ({
|
||||
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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -3,7 +3,7 @@ import HomeLayout from "../Components/Layouts/HomeLayout";
|
||||
import { Infrastructure } from "../Pages/Infrastructure";
|
||||
import InfrastructureDetails from "../Pages/Infrastructure/Details";
|
||||
import NotFound from "../Pages/NotFound";
|
||||
import Login from "../Pages/Auth/Login";
|
||||
import Login from "../Pages/Auth/Login/Login";
|
||||
import Register from "../Pages/Auth/Register/Register";
|
||||
import Account from "../Pages/Account";
|
||||
import Monitors from "../Pages/Uptime/Home";
|
||||
@@ -19,7 +19,6 @@ import SetNewPassword from "../Pages/Auth/SetNewPassword";
|
||||
import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed";
|
||||
import ProtectedRoute from "../Components/ProtectedRoute";
|
||||
import Details from "../Pages/Uptime/Details";
|
||||
import AdvancedSettings from "../Pages/AdvancedSettings";
|
||||
import Maintenance from "../Pages/Maintenance";
|
||||
import Configure from "../Pages/Uptime/Configure";
|
||||
import PageSpeed from "../Pages/PageSpeed";
|
||||
@@ -114,10 +113,6 @@ const Routes = () => {
|
||||
path="settings"
|
||||
element={<Settings />}
|
||||
/>
|
||||
<Route
|
||||
path="advanced-settings"
|
||||
element={<AdvancedSettings />}
|
||||
/>
|
||||
<Route
|
||||
path="account/profile"
|
||||
element={<Account open={"profile"} />}
|
||||
|
||||
@@ -281,10 +281,11 @@ const editUser = async (req, res, next) => {
|
||||
const user = await req.db.getUserByEmail(email);
|
||||
// Compare passwords
|
||||
const match = await user.comparePassword(req.body.password);
|
||||
// If not a match, throw a 401
|
||||
// If not a match, throw a 403
|
||||
// 403 instead of 401 to avoid triggering axios interceptor
|
||||
if (!match) {
|
||||
const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
|
||||
error.status = 401;
|
||||
error.status = 403;
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
Generated
+7
-7
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.34.2",
|
||||
"bullmq": "5.34.3",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -1223,9 +1223,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.34.2",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.2.tgz",
|
||||
"integrity": "sha512-eUzeCswrKbQDE1WY8h4ZTBtynOzCU5qx9felFdYOmIJrsy0warDahHKUiCZ6dUCs6ZxYMGtcaciIMcAf1L54yw==",
|
||||
"version": "5.34.3",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.3.tgz",
|
||||
"integrity": "sha512-S8/V11w7p6jYAGvv+00skLza/4inTOupWPe0uCD8mZSUiYKzvmW4/YEB+KVjZI2CC2oD3KJ3t7/KkUd31MxMig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.6.0",
|
||||
@@ -4489,9 +4489,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.0.tgz",
|
||||
"integrity": "sha512-b58zY3PLNBcoz6ZXFckr0leJcVVBMAOBvD+7Bj2ZjghAwntXmNnqwlDixTKQU3UYoQIGTv+AQx/0ThsvaeVrCA==",
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.2.tgz",
|
||||
"integrity": "sha512-mLWynmZS1v8HTeMxyLhskQncS1SkrjW1eLNuFDYGQMQ/5QrFrxTLNwWXeCRZeKT2lXyaxW8bnJC9AKPT9jYMkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.10.1",
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.34.2",
|
||||
"bullmq": "5.34.3",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -9,6 +9,6 @@ import {
|
||||
const router = Router();
|
||||
|
||||
router.post("/", isAllowed(["admin", "superadmin"]), verifyJWT, issueInvitation);
|
||||
router.post("/verify", issueInvitation);
|
||||
router.post("/verify", inviteVerifyController);
|
||||
|
||||
export default router;
|
||||
Reference in new issue
Block a user