diff --git a/.dockerignore b/.dockerignore
index e6b9c067d..fdfa00585 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,3 @@
Client/node_modules/
Server/node_modules/
-*.env
\ No newline at end of file
+Server/*.env
\ No newline at end of file
diff --git a/Client/package-lock.json b/Client/package-lock.json
index dfc221fc4..8e84d7a57 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -11,12 +11,12 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
- "@mui/icons-material": "6.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": "*"
}
},
diff --git a/Client/package.json b/Client/package.json
index b1a8409cf..1e22e76d3 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -14,12 +14,12 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
- "@mui/icons-material": "6.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",
diff --git a/Client/src/Components/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx
index 3760c04b8..39e8a3f42 100644
--- a/Client/src/Components/Avatar/index.jsx
+++ b/Client/src/Components/Avatar/index.jsx
@@ -2,28 +2,7 @@ import { Avatar as MuiAvatar } from "@mui/material";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
-
-/**
- * Generates a color based on the input string.
- * @param {string} string - The input string to generate the color from.
- * @returns {string}
- */
-const stringToColor = (string) => {
- let hash = 0;
- let i;
- for (i = 0; i < string.length; i += 1) {
- hash = string.charCodeAt(i) + ((hash << 5) - hash);
- }
-
- let color = "#";
- for (i = 0; i < 3; i += 1) {
- const value = (hash >> (i * 8)) & 0xff;
- color += `00${value.toString(16)}`.slice(-2);
- }
-
- return color;
-};
-
+import { useTheme } from "@emotion/react";
/**
* @component
* @param {Object} props
@@ -38,6 +17,7 @@ const stringToColor = (string) => {
const Avatar = ({ src, small, sx }) => {
const { user } = useSelector((state) => state.auth);
+ const theme = useTheme();
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
const border = small ? 1 : 3;
@@ -57,7 +37,7 @@ const Avatar = ({ src, small, sx }) => {
fontSize: small ? "16px" : "22px",
color: "white",
fontWeight: 400,
- backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`),
+ backgroundColor: theme.palette.primary.main, // Same BG color as checkmate BG in sidebar
display: "inline-flex",
"&::before": {
content: `""`,
@@ -74,7 +54,7 @@ const Avatar = ({ src, small, sx }) => {
}}
>
{user.firstName?.charAt(0)}
- {user.lastName?.charAt(0)}
+ {user.lastName?.charAt(0) || ''}
);
};
diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx
index e519a079f..9f9a5b9da 100644
--- a/Client/src/Components/BasicTable/index.jsx
+++ b/Client/src/Components/BasicTable/index.jsx
@@ -115,7 +115,7 @@ TablePaginationActions.propTypes = {
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
* @param {number} props.rowsPerPage- Number of rows per page (table).
- *
+ * @param {string} props.emptyMessage - Message to display when there is no data.
* @example
* const data = {
* cols: [
@@ -149,7 +149,7 @@ TablePaginationActions.propTypes = {
*
*/
-const BasicTable = ({ data, paginated, reversed, table }) => {
+const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
const DEFAULT_ROWS_PER_PAGE = 5;
const theme = useTheme();
const dispatch = useDispatch();
@@ -228,6 +228,16 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
);
})}
+ {displayData.length === 0 && (
+
+
+ {emptyMessage}
+
+
+ )}
@@ -317,6 +327,7 @@ BasicTable.propTypes = {
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
table: PropTypes.string,
+ emptyMessage: PropTypes.string,
};
export default BasicTable;
diff --git a/Client/src/Pages/Uptime/styled.jsx b/Client/src/Components/ConfigBox/index.jsx
similarity index 81%
rename from Client/src/Pages/Uptime/styled.jsx
rename to Client/src/Components/ConfigBox/index.jsx
index f159d44f2..7650dd47f 100644
--- a/Client/src/Pages/Uptime/styled.jsx
+++ b/Client/src/Components/ConfigBox/index.jsx
@@ -1,12 +1,15 @@
import { Stack, styled } from "@mui/material";
-export const ConfigBox = styled(Stack)(({ theme }) => ({
+const ConfigBox = styled(Stack)(({ theme }) => ({
+ display: "flex",
flexDirection: "row",
+ justifyContent: "space-between",
+ gap: theme.spacing(20),
+ backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
- backgroundColor: theme.palette.background.main,
"& > *": {
paddingTop: theme.spacing(12),
paddingBottom: theme.spacing(18),
@@ -24,15 +27,12 @@ export const ConfigBox = styled(Stack)(({ theme }) => ({
paddingRight: theme.spacing(20),
paddingLeft: theme.spacing(18),
},
- "& h2": {
+ "& h1, & h2": {
color: theme.palette.text.secondary,
- fontSize: 15,
- fontWeight: 600,
- },
- "& h3, & p": {
- color: theme.palette.text.tertiary,
},
"& p": {
- fontSize: 13,
+ color: theme.palette.text.tertiary,
},
}));
+
+export default ConfigBox;
diff --git a/Client/src/Components/IconBox/index.jsx b/Client/src/Components/IconBox/index.jsx
new file mode 100644
index 000000000..8000fbd64
--- /dev/null
+++ b/Client/src/Components/IconBox/index.jsx
@@ -0,0 +1,77 @@
+import { Box, styled } from "@mui/material";
+import PropTypes from "prop-types";
+
+/**
+ * IconBox - A styled box component for rendering icons with consistent sizing and styling
+ *
+ * @component
+ * @param {Object} [props] - Configuration options for the IconBox
+ * @param {number} [props.height=34] - Height of the icon box
+ * @param {number} [props.width=34] - Width of the icon box
+ * @param {number} [props.minWidth=34] - Minimum width of the icon box
+ * @param {number} [props.borderRadius=4] - Border radius of the icon box
+ * @param {number} [props.svgWidth=20] - Width of the SVG icon
+ * @param {number} [props.svgHeight=20] - Height of the SVG icon
+ *
+ * @example
+ * // Basic usage
+ *
+ *
+ *
+ *
+ * @example
+ * // Customized usage
+ *
+ *
+ *
+ *
+ * @returns {React.ReactElement} A styled box containing an icon
+ */
+const IconBox = styled(Box)(
+ ({
+ theme,
+ height = 34,
+ width = 34,
+ minWidth = 34,
+ borderRadius = 4,
+ svgWidth = 20,
+ svgHeight = 20,
+ }) => ({
+ height: height,
+ minWidth: minWidth,
+ width: width,
+ position: "relative",
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.border.dark,
+ borderRadius: borderRadius,
+ backgroundColor: theme.palette.background.accent,
+ "& svg": {
+ position: "absolute",
+ top: "50%",
+ left: "50%",
+ transform: "translate(-50%, -50%)",
+ width: svgWidth,
+ height: svgHeight,
+ "& path": {
+ stroke: theme.palette.text.tertiary,
+ },
+ },
+ })
+);
+
+IconBox.propTypes = {
+ height: PropTypes.number,
+ width: PropTypes.number,
+ minWidth: PropTypes.number,
+ borderRadius: PropTypes.number,
+ svgWidth: PropTypes.number,
+ svgHeight: PropTypes.number,
+};
+
+export default IconBox;
diff --git a/Client/src/Components/Inputs/Radio/index.jsx b/Client/src/Components/Inputs/Radio/index.jsx
index 7a700e2b7..6291485f0 100644
--- a/Client/src/Components/Inputs/Radio/index.jsx
+++ b/Client/src/Components/Inputs/Radio/index.jsx
@@ -42,6 +42,9 @@ const Radio = (props) => {
width: 16,
height: 16,
boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
+ "&:not(.Mui-checked)": {
+ boxShadow: `inset 0 0 0 1px ${theme.palette.text.primary}70`, // Use theme text color for the outline
+ },
mt: theme.spacing(0.5),
}}
/>
diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx
index c944955b9..9a1995726 100644
--- a/Client/src/Components/Inputs/Search/index.jsx
+++ b/Client/src/Components/Inputs/Search/index.jsx
@@ -40,6 +40,7 @@ const SearchAdornment = () => {
//TODO keep search state inside of component
const Search = ({
+ label,
id,
options,
filteredBy,
@@ -76,6 +77,14 @@ const Search = ({
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
+
+ {label}
+
{
};
ProtectedRoute.propTypes = {
- children: PropTypes.elementType.isRequired,
+ children: PropTypes.element.isRequired,
};
export default ProtectedRoute;
diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx
index feed25f24..d6d25fbc2 100644
--- a/Client/src/Components/Sidebar/index.jsx
+++ b/Client/src/Components/Sidebar/index.jsx
@@ -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" }}
>
+ * )
+ *
+ * @returns {React.ReactElement} A styled box containing the statistic
+ */
+
+const StatBox = ({ heading, subHeading, sx }) => {
+ const theme = useTheme();
+ return (
+
+ {heading}
+ {subHeading}
+
+ );
+};
+
+StatBox.propTypes = {
+ heading: PropTypes.string.isRequired,
+ subHeading: PropTypes.node.isRequired,
+ sx: PropTypes.object,
+};
+
+export default StatBox;
diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
index 209dbeaed..bbc6c4305 100644
--- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
@@ -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: (
- //
- //
- //
- // ),
- // },
],
};
}),
};
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 */}
- {/*
-
- Organization name
-
-
-
- setOrgStates((prev) => ({
- ...prev,
- name: event.target.value,
- }))
- }
- disabled={!orgStates.isEdit}
- sx={{
- color: theme.palette.otherColors.bluishGray,
- "& .Mui-disabled": {
- WebkitTextFillColor: "initial !important",
- },
- "& .Mui-disabled fieldset": {
- borderColor: "transparent !important",
- },
- }}
- inputProps={{
- sx: { textAlign: "end", padding: theme.spacing(4) },
- }}
- />
- : ""}
- onClick={() => toggleEdit()}
- sx={{
- minWidth: 0,
- paddingX: theme.spacing(4),
- ml: orgStates.isEdit ? theme.spacing(4) : 0,
- }}
- />
-
-
- */}
Team members
{
paginated={false}
reversed={true}
table={"team"}
+ emptyMessage={"There are no team members with this role"}
/>
@@ -341,7 +267,7 @@ const TeamPanel = () => {
theme={theme}
>
{
paddingY: theme.spacing(4),
fontWeight: 400,
marginRight: theme.spacing(8),
+ borderBottom: "2px solid transparent",
"&:focus": {
borderBottom: `2px solid ${theme.palette.border.light}`,
},
diff --git a/Client/src/Pages/AdvancedSettings/index.css b/Client/src/Pages/AdvancedSettings/index.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/Client/src/Pages/AdvancedSettings/index.jsx b/Client/src/Pages/AdvancedSettings/index.jsx
deleted file mode 100644
index 433829f28..000000000
--- a/Client/src/Pages/AdvancedSettings/index.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
- Client settings
-
- Modify client settings here.
-
-
-
-
-
-
-
-
-
- Email settings
-
- Set your host email settings here. These settings are used for sending
- system emails.
-
-
-
-
-
-
-
-
-
-
-
- Server settings
-
- Modify server settings here.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- About
-
-
- BlueWave Uptime v1.0.0
-
- Developed by Bluewave Labs.
-
-
-
-
-
-
- Save
-
-
-
-
- );
-};
-
-AdvancedSettings.propTypes = {
- isAdmin: PropTypes.bool,
-};
-export default AdvancedSettings;
diff --git a/Client/src/Pages/Auth/CheckEmail.jsx b/Client/src/Pages/Auth/CheckEmail.jsx
index b36cca7b0..70acbe72d 100644
--- a/Client/src/Pages/Auth/CheckEmail.jsx
+++ b/Client/src/Pages/Auth/CheckEmail.jsx
@@ -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"
>
-
-
-
+
+
+
+
+
Check your email
We sent a password reset link to{" "}
diff --git a/Client/src/Pages/Auth/ForgotPassword.jsx b/Client/src/Pages/Auth/ForgotPassword.jsx
index 1fd5f1b2f..7fcc19531 100644
--- a/Client/src/Pages/Auth/ForgotPassword.jsx
+++ b/Client/src/Pages/Auth/ForgotPassword.jsx
@@ -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"
>
-
-
-
+
+
+
+
+
Forgot password?
No worries, we'll send you reset instructions.
diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx
deleted file mode 100644
index f007600f7..000000000
--- a/Client/src/Pages/Auth/Login.jsx
+++ /dev/null
@@ -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 (
- <>
-
-
- Log In
- We are pleased to see you again!
-
-
-
-
-
-
- By continuing, you agree to our{" "}
- {
- window.open(
- "https://bluewavelabs.ca/terms-of-service-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- sx={{
- "&:hover": {
- color: theme.palette.text.tertiary,
- },
- }}
- >
- Terms of Service
- {" "}
- and{" "}
- {
- window.open(
- "https://bluewavelabs.ca/privacy-policy-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- sx={{
- "&:hover": {
- color: theme.palette.text.tertiary,
- },
- }}
- >
- Privacy Policy.
-
-
-
-
- >
- );
-};
-
-LandingPage.propTypes = {
- onContinue: PropTypes.func.isRequired,
-};
-
-/**
- * Renders the first step of the login process.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- Log In
- Enter your email address
-
-
- (e.target.value = e.target.value.toLowerCase())}
- onChange={onChange}
- error={errors.email ? true : false}
- helperText={errors.email}
- ref={inputRef}
- />
-
-
-
-
-
-
- >
- );
-};
-
-StepOne.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onBack: PropTypes.func.isRequired,
-};
-
-/**
- * Renders the second step of the login process, including a password input field.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const navigate = useNavigate();
- const inputRef = useRef(null);
- const authState = useSelector((state) => state.auth);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- const handleNavigate = () => {
- if (form.email !== "" && !errors.email) {
- sessionStorage.setItem("email", form.email);
- }
- navigate("/forgot-password");
- };
-
- return (
- <>
-
-
- Log In
- Enter your password
-
-
- }
- />
-
-
-
- Continue
-
-
-
-
-
-
- Forgot password?
-
-
- Reset password
-
-
- >
- );
-};
-
-StepTwo.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onBack: PropTypes.func.isRequired,
-};
-
-const Login = () => {
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const theme = useTheme();
-
- const authState = useSelector((state) => state.auth);
- const { authToken } = authState;
-
- const idMap = {
- "login-email-input": "email",
- "login-password-input": "password",
- };
-
- const [form, setForm] = useState({
- email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
- password: DEMO !== undefined ? "Demouser1!" : "",
- });
- const [errors, setErrors] = useState({});
- const [step, setStep] = useState(0);
-
- useEffect(() => {
- if (authToken) {
- navigate("/uptime");
- return;
- }
- networkService
- .doesSuperAdminExist()
- .then((response) => {
- if (response.data.data === false) {
- navigate("/register");
- }
- })
- .catch((error) => {
- logger.error(error);
- });
- }, [authToken, navigate]);
-
- const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- setForm((prev) => ({
- ...prev,
- [name]: value,
- }));
-
- const { error } = credentials.validate({ [name]: value }, { abortEarly: false });
-
- setErrors((prev) => {
- const prevErrors = { ...prev };
- if (error) prevErrors[name] = error.details[0].message;
- else delete prevErrors[name];
- return prevErrors;
- });
- };
-
- const handleSubmit = async (event) => {
- event.preventDefault();
-
- if (step === 1) {
- const { error } = credentials.validate(
- { email: form.email },
- { abortEarly: false }
- );
- if (error) {
- setErrors((prev) => ({ ...prev, email: error.details[0].message }));
- createToast({ body: error.details[0].message });
- } else {
- setStep(2);
- }
- } else if (step === 2) {
- const { error } = credentials.validate(form, { abortEarly: false });
-
- if (error) {
- // validation errors
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
- createToast({
- body:
- error.details && error.details.length > 0
- ? error.details[0].message
- : "Error validating data.",
- });
- } else {
- const action = await dispatch(login(form));
- if (action.payload.success) {
- navigate("/uptime");
- createToast({
- body: "Welcome back! You're successfully logged in.",
- });
- } else {
- if (action.payload) {
- if (action.payload.msg === "Incorrect password")
- setErrors({
- password: "The password you provided does not match our records",
- });
- // dispatch errors
- createToast({
- body: action.payload.msg,
- });
- } else {
- // unknown errors
- createToast({
- body: "Unknown error.",
- });
- }
- }
- }
- }
- };
-
- return (
-
-
-
-
-
-
- BlueWave Uptime
-
- .MuiStack-root": {
- border: 1,
- borderRadius: theme.spacing(5),
- borderColor: theme.palette.border.light,
- backgroundColor: theme.palette.background.main,
- padding: {
- xs: theme.spacing(12),
- sm: theme.spacing(20),
- },
- },
- }}
- >
- {step === 0 ? (
- setStep(1)} />
- ) : step === 1 ? (
- setStep(0)}
- />
- ) : (
- step === 2 && (
- setStep(1)}
- />
- )
- )}
-
-
-
-
-
- );
-};
-
-export default Login;
diff --git a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
new file mode 100644
index 000000000..08ae51c4f
--- /dev/null
+++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx
@@ -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 (
+ <>
+
+
+ Log In
+ Enter your email address
+
+
+ (e.target.value = e.target.value.toLowerCase())}
+ onChange={onChange}
+ error={errors.email ? true : false}
+ helperText={errors.email}
+ ref={inputRef}
+ />
+
+
+
+
+
+
+ Forgot password?
+
+
+ Reset password
+
+
+
+ >
+ );
+};
+
+EmailStep.propTypes = {
+ form: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
+
+export default EmailStep;
diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
new file mode 100644
index 000000000..34df4959d
--- /dev/null
+++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx
@@ -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 (
+ <>
+
+
+ Log In
+ Enter your password
+
+
+ }
+ />
+
+
+
+ Continue
+
+
+
+
+
+ Forgot password?
+
+
+ Reset password
+
+
+
+ >
+ );
+};
+
+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
\ No newline at end of file
diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx
new file mode 100644
index 000000000..c1687a0ec
--- /dev/null
+++ b/Client/src/Pages/Auth/Login/Login.jsx
@@ -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 (
+
+
+
+
+
+
+ BlueWave Uptime
+
+ .MuiStack-root": {
+ border: 1,
+ borderRadius: theme.spacing(5),
+ borderColor: theme.palette.border.light,
+ backgroundColor: theme.palette.background.main,
+ padding: {
+ xs: theme.spacing(12),
+ sm: theme.spacing(20),
+ },
+ },
+ }}
+ >
+ {step === 0 ? (
+
+ ) : (
+ step === 1 && (
+ setStep(0)}
+ />
+ )
+ )}
+
+
+ );
+};
+
+export default Login;
diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
index c380425c9..2c4ef0106 100644
--- a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
@@ -4,10 +4,10 @@ import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { IconBox } from "./styled";
import Background from "../../assets/Images/background-grid.svg?react";
import ConfirmIcon from "../../assets/icons/check-outlined.svg?react";
import Logo from "../../assets/icons/bwu-icon.svg?react";
+import IconBox from "../../Components/IconBox";
import "./index.css";
const NewPasswordConfirmed = () => {
@@ -80,9 +80,22 @@ const NewPasswordConfirmed = () => {
textAlign="center"
>
-
-
-
+
+
+
+
+
Password reset
Your password has been successfully reset. Click below to log in magically.
@@ -91,7 +104,7 @@ const NewPasswordConfirmed = () => {
diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx
index 346fe9c5e..66cebdea7 100644
--- a/Client/src/Pages/Infrastructure/Details/index.jsx
+++ b/Client/src/Pages/Infrastructure/Details/index.jsx
@@ -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 ? " " : ""}
+ GB
+ >
+ );
+ if (typeof bytes !== "number")
+ return (
+ <>
+ {0}
+ {space ? " " : ""}
+ GB
+ >
+ );
+ if (bytes === 0)
+ return (
+ <>
+ {0}
+ {space ? " " : ""}
+ GB
+ >
+ );
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 ? " " : ""}
+ GB
+ >
+ );
} else {
- return `${Number(MB.toFixed(0))} MB`;
+ return (
+ <>
+ {Number(MB.toFixed(0))}
+ MB
+ >
+ );
}
};
@@ -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 (
-
- {heading}
- {subHeading}
-
- );
-};
-
-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}
+ cores
+ >
+ ),
},
{
id: 1,
heading: "CPU (Logical)",
- subHeading: `${logicalCores} cores`,
+ subHeading: (
+ <>
+ {logicalCores}
+ cores
+ >
+ ),
},
{
id: 2,
heading: "CPU Frequency",
- subHeading: `${(cpuFrequency / 1000).toFixed(2)} Ghz`,
+ subHeading: (
+ <>
+ {(cpuFrequency / 1000).toFixed(2)}
+ Ghz
+ >
+ ),
},
{
id: 3,
heading: "Average CPU Temperature",
- subHeading: `${cpuTemperature.toFixed(2)} C`,
+ subHeading: (
+ <>
+ {cpuTemperature.toFixed(2)}
+ C
+ >
+ ),
},
{
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)}
+ %
+ >
+ ),
},
+
{
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 ?? []);
diff --git a/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx
index f41603330..c832d463a 100644
--- a/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx
+++ b/Client/src/Pages/Maintenance/CreateMaintenance/index.jsx
@@ -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
-
-
- General Settings
-
-
+
+
+
+ General Settings
+
+
+
+ {
+ handleFormChange("name", event.target.value);
+ }}
+ error={errors["name"] ? true : false}
+ helperText={errors["name"]}
+ />
+
-
-
-
- Start time
-
- All dates and times are in GMT+0 time zone.
-
-
-
- {
- handleTimeChange("startTime", newTime);
- }}
- slotProps={{
- nextIconButton: { sx: { ml: theme.spacing(2) } },
- field: {
- sx: {
- width: "fit-content",
- "& > .MuiOutlinedInput-root": {
- flexDirection: "row-reverse",
- },
- "& input": {
- height: 34,
- p: 0,
- pl: theme.spacing(5),
- },
- "& fieldset": {
- borderColor: theme.palette.border.dark,
- borderRadius: theme.shape.borderRadius,
- },
- "&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
- {
- borderColor: theme.palette.border.dark,
- },
+
+
+ Start time
+
+ All dates and times are in GMT+0 time zone.
+
+
+
+ {
+ handleTimeChange("startTime", newTime);
+ }}
+ slotProps={{
+ nextIconButton: { sx: { ml: theme.spacing(2) } },
+ field: {
+ sx: {
+ width: "fit-content",
+ "& > .MuiOutlinedInput-root": {
+ flexDirection: "row-reverse",
},
+ "& input": {
+ height: 34,
+ p: 0,
+ pl: theme.spacing(5),
+ },
+ "& fieldset": {
+ borderColor: theme.palette.border.dark,
+ borderRadius: theme.shape.borderRadius,
+ },
+ "&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
+ {
+ borderColor: theme.palette.border.dark,
+ },
},
- }}
- error={errors["startTime"]}
- />
-
-
-
-
-
-
- Duration
-
-
+ },
+ }}
+ error={errors["startTime"]}
+ />
+
+
{
handleFormChange("duration", event.target.value);
@@ -491,66 +489,34 @@ const CreateMaintenance = () => {
-
-
- Monitor related settings
-
-
-
-
-
- Friendly name
-
-
-
- {
- handleFormChange("name", event.target.value);
- }}
- error={errors["name"] ? true : false}
- helperText={errors["name"]}
- />
-
-
-
-
-
- Add monitors
-
-
-
-
-
-
-
-
+
+
+
+
+ Monitors to apply maintenance window to
+
+
+
+
+
+
({
- border: 1,
- borderStyle: "solid",
- borderColor: theme.palette.border.light,
- borderRadius: theme.spacing(2),
- backgroundColor: theme.palette.background.main,
- "& > *": { padding: theme.spacing(14) },
- "& > :first-of-type, & > .MuiStack-root > div:first-of-type": {
- flex: 0.6,
- },
- "& > div:last-of-type, & > .MuiStack-root > div:last-of-type": {
- flex: 1,
- },
- "& > .MuiStack-root > div:first-of-type": { paddingRight: theme.spacing(14) },
- "& > .MuiStack-root > div:last-of-type": {
- paddingLeft: theme.spacing(14),
- },
- "& h2": { fontSize: 13.5, fontWeight: 500 },
- "& h3, & p": {
- color: theme.palette.text.tertiary,
- },
- "& h3": { fontWeight: 500 },
-}));
diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx
index 47d847394..3cb40f8d2 100644
--- a/Client/src/Pages/PageSpeed/Configure/index.jsx
+++ b/Client/src/Pages/PageSpeed/Configure/index.jsx
@@ -13,7 +13,7 @@ import {
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
-import { ConfigBox } from "../../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);
}
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
index 30592e2eb..e5d4a6d79 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
@@ -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";
diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx
index b0db13432..bf06797cd 100644
--- a/Client/src/Pages/PageSpeed/Details/index.jsx
+++ b/Client/src/Pages/PageSpeed/Details/index.jsx
@@ -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)}
>
-
- checks since
-
- {splitDuration(monitor?.uptimeDuration)}
- ago
-
-
-
- last check
-
- {splitDuration(monitor?.lastChecked)}
- ago
-
-
+
+ {splitDuration(monitor?.uptimeDuration)}
+ ago
+ >
+ }
+ />
+
+ {splitDuration(monitor?.lastChecked)}
+ ago
+ >
+ }
+ />
({
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,
- },
- },
-}));
diff --git a/Client/src/Pages/PageSpeed/card.jsx b/Client/src/Pages/PageSpeed/card.jsx
index 56ba62346..a08a4fcd1 100644
--- a/Client/src/Pages/PageSpeed/card.jsx
+++ b/Client/src/Pages/PageSpeed/card.jsx
@@ -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
diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx
index aab9fa537..5c53f3d01 100644
--- a/Client/src/Pages/Settings/index.jsx
+++ b/Client/src/Pages/Settings/index.jsx
@@ -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 = () => {
/>
)}
- {isAdmin && (
-
-
- Advanced settings
-
- Click here to modify advanced settings
-
-
-
-
-
-
-
-
- )}
+
About
diff --git a/Client/src/Pages/Settings/styled.jsx b/Client/src/Pages/Settings/styled.jsx
deleted file mode 100644
index 269706a27..000000000
--- a/Client/src/Pages/Settings/styled.jsx
+++ /dev/null
@@ -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,
- },
-}));
diff --git a/Client/src/Pages/Uptime/Configure/index.jsx b/Client/src/Pages/Uptime/Configure/index.jsx
index 95af3ad76..32532d462 100644
--- a/Client/src/Pages/Uptime/Configure/index.jsx
+++ b/Client/src/Pages/Uptime/Configure/index.jsx
@@ -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);
}
diff --git a/Client/src/Pages/Uptime/CreateUptime/index.jsx b/Client/src/Pages/Uptime/CreateUptime/index.jsx
index 6d5682415..d72a4fa25 100644
--- a/Client/src/Pages/Uptime/CreateUptime/index.jsx
+++ b/Client/src/Pages/Uptime/CreateUptime/index.jsx
@@ -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 = [
diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx
index 43dd86ca4..28b198b69 100644
--- a/Client/src/Pages/Uptime/Details/index.jsx
+++ b/Client/src/Pages/Uptime/Details/index.jsx
@@ -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)}
>
-
- active for
- {splitDuration(monitor?.uptimeDuration)}
-
-
- last check
-
- {splitDuration(monitor?.lastChecked)}
- ago
-
-
-
- last response time
-
- {monitor?.latestResponseTime}
- ms
-
-
+
+
+
+ {monitor?.latestResponseTime}
+ {"ms"}
+ >
+ }
+ />
({
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,
- },
- },
-}));
diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx
index 5fc764b6a..7d444f2ab 100644
--- a/Client/src/Routes/index.jsx
+++ b/Client/src/Routes/index.jsx
@@ -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={}
/>
- }
- />
}
diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js
index 1fba9c92f..b7e4a7808 100644
--- a/Server/controllers/authController.js
+++ b/Server/controllers/authController.js
@@ -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;
}
diff --git a/Server/package-lock.json b/Server/package-lock.json
index cb4ee1836..5b6d808f9 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -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",
diff --git a/Server/package.json b/Server/package.json
index 93d4d6be8..c480aea4c 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -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",
diff --git a/Server/routes/inviteRoute.js b/Server/routes/inviteRoute.js
index f1f8a20e1..36d30de5a 100644
--- a/Server/routes/inviteRoute.js
+++ b/Server/routes/inviteRoute.js
@@ -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;