Merge branch 'develop' of https://github.com/bluewave-labs/bluewave-uptime into develop
This commit is contained in:
commit
0154c2c71f
56 files changed
+1611
-1085
No files matched your search
Generated
+9
-15
@@ -18,7 +18,7 @@
|
||||
"@mui/x-data-grid": "7.3.2",
|
||||
"@mui/x-date-pickers": "7.3.2",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"axios": "1.7.2",
|
||||
"axios": "1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"dayjs": "1.11.11",
|
||||
"joi": "17.13.1",
|
||||
@@ -29,7 +29,7 @@
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recharts": "2.12.7",
|
||||
"recharts": "2.13.0-alpha.4",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
@@ -2655,9 +2655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -5509,15 +5509,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.12.7",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz",
|
||||
"integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==",
|
||||
"version": "2.13.0-alpha.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0-alpha.4.tgz",
|
||||
"integrity": "sha512-K9naL6F7pEcDYJE6yFQASSCQecSLPP0JagnvQ9hPtA/aHgsxsnIOjouLP5yrFZehxzfCkV5TEORr7/uNtSr7Qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-is": "^16.10.2",
|
||||
"react-is": "^18.3.1",
|
||||
"react-smooth": "^4.0.0",
|
||||
"recharts-scale": "^0.4.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
@@ -5540,12 +5540,6 @@
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@
|
||||
"@mui/x-data-grid": "7.3.2",
|
||||
"@mui/x-date-pickers": "7.3.2",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"axios": "1.7.2",
|
||||
"axios": "1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"dayjs": "1.11.11",
|
||||
"joi": "17.13.1",
|
||||
@@ -31,7 +31,7 @@
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recharts": "2.12.7",
|
||||
"recharts": "2.13.0-alpha.4",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,27 @@ 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @param {Object} props
|
||||
@@ -18,7 +39,7 @@ import { useEffect, useState } from "react";
|
||||
const Avatar = ({ src, small, sx }) => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
const style = small ? { width: 25, height: 25 } : { width: 64, height: 64 };
|
||||
const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
|
||||
const border = small ? 1 : 3;
|
||||
|
||||
const [image, setImage] = useState();
|
||||
@@ -35,8 +56,9 @@ const Avatar = ({ src, small, sx }) => {
|
||||
src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"
|
||||
}
|
||||
sx={{
|
||||
fontSize: small ? "13px" : "22px",
|
||||
fontSize: small ? "16px" : "22px",
|
||||
fontWeight: 400,
|
||||
backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`),
|
||||
display: "inline-flex",
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
@@ -52,7 +74,8 @@ const Avatar = ({ src, small, sx }) => {
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{user.firstName?.charAt(0)}{user.lastName?.charAt(0)}
|
||||
{user.firstName?.charAt(0)}
|
||||
{user.lastName?.charAt(0)}
|
||||
</MuiAvatar>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
}
|
||||
|
||||
.MuiPaper-root + .MuiPagination-root {
|
||||
margin-top: 35px;
|
||||
margin-top: 24px;
|
||||
border: solid 1px var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { BarChart, Bar, Cell, ReferenceLine, Label } from "recharts";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorDetails60MinChart = ({ data }) => {
|
||||
const labelStyle = {
|
||||
fontSize: "10px",
|
||||
fill: "#475467",
|
||||
};
|
||||
|
||||
const color = {
|
||||
true: "var(--env-var-color-23)",
|
||||
false: "var(--env-var-color-24)",
|
||||
undefined: "var(--env-var-color-33)",
|
||||
};
|
||||
return (
|
||||
<BarChart
|
||||
width={data.length * 10 + 30}
|
||||
height={35}
|
||||
data={data}
|
||||
margin={{ top: 14, left: 15, right: 15, bottom: 2 }}
|
||||
style={{ alignSelf: "baseline" }}
|
||||
>
|
||||
<Bar dataKey="value" barSize={10}>
|
||||
{data.map((check, index) => (
|
||||
<Cell key={`cell-${index}`} fill={color[check.status]} />
|
||||
))}
|
||||
</Bar>
|
||||
<ReferenceLine x={0} stroke="black" strokeDasharray="3 3">
|
||||
<Label value="60 mins" position="top" style={labelStyle} />
|
||||
</ReferenceLine>
|
||||
<ReferenceLine
|
||||
x={Math.floor(data.length * (2 / 3))}
|
||||
stroke="black"
|
||||
strokeDasharray="3 3"
|
||||
>
|
||||
<Label value="20 mins" position="top" style={labelStyle} />
|
||||
</ReferenceLine>
|
||||
<ReferenceLine x={data.length - 1} stroke="black" strokeDasharray="3 3">
|
||||
<Label value="now" position="top" style={labelStyle} />
|
||||
</ReferenceLine>
|
||||
</BarChart>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorDetails60MinChart.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorDetails60MinChart;
|
||||
@@ -28,15 +28,6 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
};
|
||||
|
||||
const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
// SQUASH ERROR, NOT PERMANENT SOLUTION
|
||||
const error = console.error;
|
||||
console.error = (...args) => {
|
||||
if (/defaultProps/.test(args[0])) return;
|
||||
error(...args);
|
||||
};
|
||||
|
||||
// END SQUASH ERROR, NOT PERMANENT SOLUTION
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
|
||||
@@ -46,7 +46,7 @@ const Check = ({ text, variant = "info", outlined = false }) => {
|
||||
};
|
||||
|
||||
Check.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||
variant: PropTypes.oneOf(["info", "error", "success"]),
|
||||
outlined: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.check span.MuiTypography-root {
|
||||
.check > span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
color: var(--env-var-color-2);
|
||||
opacity: 0.8;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/* NavBar Component Styles*/
|
||||
.MuiToolbar-root {
|
||||
min-height: var(--env-var-nav-bar-height) !important;
|
||||
}
|
||||
|
||||
.icon-button-toggle-title {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
|
||||
.icon-button-toggle-pic {
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#icon-button {
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#menu-appbar svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--env-var-color-25);
|
||||
}
|
||||
|
||||
#bw-uptime-logo-dashboard {
|
||||
width: fit-content;
|
||||
height: 16px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import "./index.css";
|
||||
import { cloneElement, useState } from "react";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import Avatar from "../Avatar";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import LockSvg from "../../assets/icons/lock.svg?react";
|
||||
import UserSvg from "../../assets/icons/user.svg?react";
|
||||
import TeamSvg from "../../assets/icons/user-two.svg?react";
|
||||
import LogoutSvg from "../../assets/icons/logout.svg?react";
|
||||
import { Stack, useScrollTrigger } from "@mui/material";
|
||||
import axiosIntance from "../../Utils/axiosConfig";
|
||||
import axios from "axios";
|
||||
|
||||
const icons = {
|
||||
Profile: <UserSvg />,
|
||||
Team: <TeamSvg />,
|
||||
Password: <LockSvg />,
|
||||
Logout: <LogoutSvg />,
|
||||
};
|
||||
|
||||
function AddBorderOnScroll(props) {
|
||||
const { children, window } = props;
|
||||
const trigger = useScrollTrigger({
|
||||
target: window ? window() : undefined,
|
||||
disableHysteresis: true,
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
className={trigger ? "scrolled" : ""}
|
||||
position="sticky"
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
transition: "all 0.3s ease",
|
||||
borderBottom: "1px solid transparent",
|
||||
"&.scrolled": {
|
||||
borderBottom: "1px solid #eaecf0",
|
||||
backgroundColor: "white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* NavBar component
|
||||
*
|
||||
* A responsive navigation bar component with a user menu.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* return (
|
||||
* <NavBar />
|
||||
* )
|
||||
*/
|
||||
function NavBar() {
|
||||
const theme = useTheme();
|
||||
const [anchorElUser, setAnchorElUser] = useState(null);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
// Initialize settings and update based on user role
|
||||
let settings = ["Profile", "Password", "Team", "Logout"];
|
||||
if (authState.user?.role && !authState.user.role.includes("admin")) {
|
||||
settings = ["Profile", "Password", "Logout"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles opening the user menu.
|
||||
*
|
||||
* @param {React.MouseEvent<HTMLElement>} event - The event triggered by clicking the user menu button.
|
||||
*/
|
||||
const handleOpenUserMenu = (event) => {
|
||||
setAnchorElUser(event.currentTarget);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles logging out the user
|
||||
*
|
||||
*/
|
||||
const logout = async () => {
|
||||
// Clear auth state
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
// Make request to BE to remove JWT from user
|
||||
await axiosIntance.post(
|
||||
"/auth/logout",
|
||||
{ email: authState.user.email },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles closing the user menu.
|
||||
*/
|
||||
const handleCloseUserMenu = (setting) => {
|
||||
setAnchorElUser(null);
|
||||
switch (setting) {
|
||||
case "Profile":
|
||||
navigate("/account/profile");
|
||||
break;
|
||||
case "Team":
|
||||
navigate("/account/team");
|
||||
break;
|
||||
case "Password":
|
||||
navigate("/account/password");
|
||||
break;
|
||||
case "Logout":
|
||||
logout();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AddBorderOnScroll>
|
||||
<Toolbar disableGutters sx={{ alignSelf: "flex-end", paddingX: "25px" }}>
|
||||
<Tooltip title="Open settings">
|
||||
<IconButton
|
||||
id="icon-button"
|
||||
onClick={handleOpenUserMenu}
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap="8px">
|
||||
<Avatar small={true} />
|
||||
<Box
|
||||
className="icon-button-toggle-title"
|
||||
sx={{ mr: "3px", lineHeight: 2 }}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Box>
|
||||
<KeyboardArrowDownIcon sx={{ mt: "2px" }} />
|
||||
</Stack>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
sx={{
|
||||
mt: theme.spacing(5.5),
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu}
|
||||
>
|
||||
{settings.map((setting) => (
|
||||
<MenuItem
|
||||
id="menu-item"
|
||||
key={setting}
|
||||
onClick={() => handleCloseUserMenu(setting)}
|
||||
sx={{ width: "150px" }}
|
||||
>
|
||||
{icons[setting]}
|
||||
<Typography
|
||||
fontSize="var(--env-var-font-size-medium)"
|
||||
textAlign="center"
|
||||
marginLeft="8px"
|
||||
sx={{ fontWeight: 400, color: "#344054" }}
|
||||
>
|
||||
{setting}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AddBorderOnScroll>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
@@ -18,11 +18,14 @@ aside span.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
line-height: 1;
|
||||
}
|
||||
aside .MuiStack-root:nth-last-child(2) {
|
||||
aside .MuiStack-root:nth-last-child(3) {
|
||||
margin-top: auto;
|
||||
}
|
||||
aside .MuiStack-root:last-child,
|
||||
aside .MuiStack-root:nth-last-child(3) {
|
||||
position: relative;
|
||||
}
|
||||
aside .MuiStack-root:nth-last-child(2):before {
|
||||
aside .MuiStack-root:last-child:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
@@ -31,5 +34,35 @@ aside .MuiStack-root:nth-last-child(2):before {
|
||||
border-top: solid 1px var(--env-var-color-6);
|
||||
}
|
||||
aside .MuiStack-root:last-child {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
margin-top: -20px;
|
||||
}
|
||||
.sidebar-menu .MuiPaper-root {
|
||||
box-shadow: var(--env-var-shadow-1);
|
||||
border: solid 1px var(--env-var-color-6);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
gap: 1px;
|
||||
}
|
||||
.sidebar-menu .MuiList-root {
|
||||
min-width: 100px;
|
||||
width: 150px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.sidebar-menu li.MuiButtonBase-root:last-child {
|
||||
border-top: solid 1px var(--env-var-color-6);
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.sidebar-menu li.MuiButtonBase-root {
|
||||
min-height: fit-content;
|
||||
}
|
||||
.sidebar-menu .MuiList-root svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.sidebar-menu span {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { Menu, MenuItem, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import Avatar from "../Avatar";
|
||||
import LockSvg from "../../assets/icons/lock.svg?react";
|
||||
import UserSvg from "../../assets/icons/user.svg?react";
|
||||
import TeamSvg from "../../assets/icons/user-two.svg?react";
|
||||
import LogoutSvg from "../../assets/icons/logout.svg?react";
|
||||
import BWULogo from "../../assets/Images/bwl-logo.svg?react";
|
||||
import Support from "../../assets/icons/support.svg?react";
|
||||
import StatusPages from "../../assets/icons/status-pages.svg?react";
|
||||
@@ -11,16 +21,10 @@ import Incidents from "../../assets/icons/incidents.svg?react";
|
||||
import Integrations from "../../assets/icons/integrations.svg?react";
|
||||
import PageSpeed from "../../assets/icons/page-speed.svg?react";
|
||||
import Settings from "../../assets/icons/settings.svg?react";
|
||||
import Arrow from "../../assets/icons/down-arrow.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* @component
|
||||
* Sidebar component serves as a sidebar containing a menu.
|
||||
*
|
||||
* @returns {JSX.Element} The JSX element representing the Sidebar component.
|
||||
*/
|
||||
|
||||
const menu = [
|
||||
{ name: "Monitors", path: "monitors", icon: <Monitors /> },
|
||||
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
|
||||
@@ -32,10 +36,94 @@ const menu = [
|
||||
{ name: "Settings", path: "settings", icon: <Settings /> },
|
||||
];
|
||||
|
||||
const icons = {
|
||||
Profile: <UserSvg />,
|
||||
Team: <TeamSvg />,
|
||||
Password: <LockSvg />,
|
||||
Logout: <LogoutSvg />,
|
||||
};
|
||||
|
||||
/**
|
||||
* @component
|
||||
* Sidebar component serves as a sidebar containing a menu.
|
||||
*
|
||||
* @returns {JSX.Element} The JSX element representing the Sidebar component.
|
||||
*/
|
||||
|
||||
function Sidebar() {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const [anchorElUser, setAnchorElUser] = useState(null);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
// Initialize settings and update based on user role
|
||||
let settings = ["Profile", "Password", "Team", "Logout"];
|
||||
if (authState.user?.role && !authState.user.role.includes("admin")) {
|
||||
settings = ["Profile", "Password", "Logout"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles opening the user menu.
|
||||
*
|
||||
* @param {React.MouseEvent<HTMLElement>} event - The event triggered by clicking the user menu button.
|
||||
*/
|
||||
const handleOpenUserMenu = (event) => {
|
||||
setAnchorElUser(event.currentTarget);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles logging out the user
|
||||
*
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
// Make request to BE to remove JWT from user
|
||||
await axiosInstance.post(
|
||||
"/auth/logout",
|
||||
{ email: authState.user.email },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Clear auth state
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
navigate("/login");
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles closing the user menu.
|
||||
*/
|
||||
const handleCloseUserMenu = (setting) => {
|
||||
setAnchorElUser(null);
|
||||
switch (setting) {
|
||||
case "Profile":
|
||||
navigate("/account/profile");
|
||||
break;
|
||||
case "Team":
|
||||
navigate("/account/team");
|
||||
break;
|
||||
case "Password":
|
||||
navigate("/account/password");
|
||||
break;
|
||||
case "Logout":
|
||||
logout();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack component="aside" gap={theme.gap.xs}>
|
||||
@@ -48,6 +136,8 @@ function Sidebar() {
|
||||
key={item.path}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
py={theme.gap.small}
|
||||
px={theme.gap.medium}
|
||||
gap={theme.gap.small}
|
||||
borderRadius={`${theme.shape.borderRadius}px`}
|
||||
onClick={() =>
|
||||
@@ -59,12 +149,66 @@ function Sidebar() {
|
||||
)
|
||||
: navigate(`/${item.path}`)
|
||||
}
|
||||
sx={{ p: `${theme.gap.small} ${theme.gap.medium}` }}
|
||||
>
|
||||
{item.icon}
|
||||
<Typography component="span">{item.name}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
<Tooltip
|
||||
title="Open settings"
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -14],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
py={theme.gap.small}
|
||||
px={theme.gap.medium}
|
||||
gap={theme.gap.xs}
|
||||
borderRadius={`${theme.shape.borderRadius}px`}
|
||||
onClick={handleOpenUserMenu}
|
||||
>
|
||||
<Avatar small={true} />
|
||||
<Typography component="span" ml={theme.gap.xs}>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Arrow style={{ marginTop: "2px", marginLeft: "auto" }} />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
className="sidebar-menu"
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
keepMounted
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu}
|
||||
autoFocus={false}
|
||||
sx={{
|
||||
ml: theme.gap.xxl,
|
||||
}}
|
||||
>
|
||||
{settings.map((setting) => (
|
||||
<MenuItem key={setting} onClick={() => handleCloseUserMenu(setting)}>
|
||||
{icons[setting]}
|
||||
<Typography component="span" ml={theme.gap.small}>
|
||||
{setting}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ export const register = createAsyncThunk(
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -33,7 +37,11 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,7 +80,11 @@ export const update = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -91,7 +103,11 @@ export const deleteUser = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -106,7 +122,11 @@ export const forgotPassword = createAsyncThunk(
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -128,7 +148,11 @@ export const setNewPassword = createAsyncThunk(
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -25,7 +25,11 @@ export const createPageSpeed = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -40,7 +44,11 @@ export const getPageSpeedMonitors = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -63,7 +71,11 @@ export const getPageSpeedByUserId = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -94,7 +106,11 @@ export const updatePageSpeed = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -115,7 +131,11 @@ export const deletePageSpeed = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -25,7 +25,11 @@ export const createUptimeMonitor = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -40,7 +44,11 @@ export const getUptimeMonitors = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -63,7 +71,11 @@ export const getUptimeMonitorsByUserId = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -94,7 +106,11 @@ export const updateUptimeMonitor = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -115,7 +131,11 @@ export const deleteUptimeMonitor = createAsyncThunk(
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,55 +1,34 @@
|
||||
.home-layout {
|
||||
display: grid;
|
||||
position: relative;
|
||||
grid-template-areas:
|
||||
"aside header"
|
||||
"aside main";
|
||||
grid-template-columns: var(--env-var-side-bar-width) auto;
|
||||
grid-template-rows: var(--env-var-nav-bar-height) auto;
|
||||
#root:has(.home-layout) {
|
||||
background-color: var(--env-var-color-30);
|
||||
}
|
||||
|
||||
.home-layout {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: var(--env-var-spacing-2);
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.home-layout aside {
|
||||
grid-area: aside;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: var(--env-var-side-bar-width);
|
||||
position: sticky;
|
||||
top: var(--env-var-spacing-2);
|
||||
left: 0;
|
||||
|
||||
height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
max-width: var(--env-var-side-bar-width);
|
||||
flex: 1;
|
||||
|
||||
border: 1px solid var(--color-border-0);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
background-color: var(--env-var-color-8);
|
||||
border-right: 1px solid var(--color-border-0);
|
||||
|
||||
padding: var(--env-var-spacing-1) var(--env-var-spacing-1-plus);
|
||||
}
|
||||
.home-layout header {
|
||||
grid-area: header;
|
||||
background-color: var(--env-var-color-30);
|
||||
}
|
||||
|
||||
.home-layout > div {
|
||||
height: 100%;
|
||||
grid-area: main;
|
||||
padding: var(--env-var-spacing-2) calc(var(--env-var-spacing-4) * 2);
|
||||
padding-bottom: var(--env-var-spacing-4);
|
||||
max-width: 100%;
|
||||
}
|
||||
.home-layout > div:not(:has([class*="fallback"])) {
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.home-layout {
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"main main";
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 64px auto;
|
||||
}
|
||||
|
||||
aside {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.home-layout > div {
|
||||
padding: var(--env-var-spacing-1);
|
||||
}
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import NavBar from "../../Components/NavBar";
|
||||
import Sidebar from "../../Components/Sidebar";
|
||||
import { Outlet } from "react-router";
|
||||
import { Box } from "@mui/material";
|
||||
@@ -9,7 +8,6 @@ const HomeLayout = () => {
|
||||
return (
|
||||
<Box className="home-layout">
|
||||
<Sidebar />
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ const Account = ({ open = "profile" }) => {
|
||||
if (!user.role.includes("admin")) tabList = ["Profile", "Password"];
|
||||
|
||||
return (
|
||||
<Box className="account">
|
||||
<Box className="account" pt={theme.gap.xl}>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import "./index.css";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import EmailIcon from "../../assets/icons/email.svg?react";
|
||||
import Button from "../../Components/Button";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { forgotPassword } from "../../Features/Auth/authSlice";
|
||||
import Button from "../../Components/Button";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import EmailIcon from "../../assets/icons/email.svg?react";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
const CheckEmail = () => {
|
||||
const theme = useTheme();
|
||||
@@ -79,28 +79,71 @@ const CheckEmail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="check-email-page">
|
||||
<img
|
||||
<Stack className="check-email-page auth" overflow="hidden">
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
sx={{ backgroundImage: `url(${background})` }}
|
||||
/>
|
||||
<form className="check-email-form">
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<EmailIcon alt="EmailIcon" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Check your email
|
||||
</Typography>
|
||||
<Typography sx={{ width: "max-content" }}>
|
||||
We sent a password reset link to{" "}
|
||||
<Typography component="span">
|
||||
{email || "username@email.com"}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.gap.large}
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<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.gap.large, lg: theme.gap.xl }}
|
||||
pb={theme.gap.xl}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.otherColors.graishWhite,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
padding: {
|
||||
xs: theme.gap.large,
|
||||
sm: theme.gap.xl,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<EmailIcon alt="email icon" />
|
||||
<Typography component="h1">Check your email</Typography>
|
||||
<Typography mt={theme.gap.xs}>
|
||||
We sent a password reset link to{" "}
|
||||
<Typography className="email-sent-to" component="span">
|
||||
{email || "username@email.com"}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.ml} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Button level="primary" label="Open email app" onClick={openMail} />
|
||||
</Box>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Open email app"
|
||||
onClick={openMail}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
/>
|
||||
<Typography sx={{ alignSelf: "center", mb: theme.gap.medium }}>
|
||||
Didn't receive the email?{" "}
|
||||
<Typography
|
||||
@@ -108,7 +151,6 @@ const CheckEmail = () => {
|
||||
onClick={resendToken}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
letterSpacing: "-0.1px",
|
||||
userSelect: "none",
|
||||
pointerEvents: disabled ? "none" : "auto",
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
@@ -118,17 +160,20 @@ const CheckEmail = () => {
|
||||
Click to resend
|
||||
</Typography>
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={() => navigate("/login")}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.gap.large}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import "./index.css";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import Logomark from "../../assets/icons/key.svg?react";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { forgotPassword } from "../../Features/Auth/authSlice";
|
||||
import { useEffect, useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import ButtonSpinner from "../../Components/ButtonSpinner";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Button from "../../Components/Button";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import Key from "../../assets/icons/key.svg?react";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import "./index.css";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -25,9 +24,10 @@ const ForgotPassword = () => {
|
||||
email: "",
|
||||
});
|
||||
|
||||
const idMap = {
|
||||
"forgot-password-email-input": "email",
|
||||
};
|
||||
useEffect(() => {
|
||||
const email = sessionStorage.getItem("email");
|
||||
email && setForm({ email: sessionStorage.getItem("email") });
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -40,11 +40,9 @@ const ForgotPassword = () => {
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.";
|
||||
setErrors(err);
|
||||
setErrors({ email: err });
|
||||
createToast({
|
||||
variant: "info",
|
||||
body: err,
|
||||
hasIcon: false,
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(forgotPassword(form));
|
||||
@@ -71,72 +69,111 @@ const ForgotPassword = () => {
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
const { value } = event.target;
|
||||
setForm({ email: value });
|
||||
|
||||
const { error } = credentials.validate(
|
||||
{ [name]: value },
|
||||
{ email: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
if (error) setErrors({ email: error.details[0].message });
|
||||
else delete errors.email;
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="forgot-password-page">
|
||||
<img
|
||||
<Stack className="forgot-password-page auth" overflow="hidden">
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
sx={{ backgroundImage: `url(${background})` }}
|
||||
/>
|
||||
<form className="forgot-password-form" onSubmit={handleSubmit}>
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<Logomark alt="Logomark" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography>
|
||||
No worries, we'll send you reset instructions.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.gap.large}
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<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.gap.large, lg: theme.gap.xl }}
|
||||
pb={theme.gap.xl}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.otherColors.graishWhite,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
padding: {
|
||||
xs: theme.gap.large,
|
||||
sm: theme.gap.xl,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Key alt="password key icon" />
|
||||
<Typography component="h1">Forgot password?</Typography>
|
||||
<Typography>
|
||||
No worries, we'll send you reset instructions.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box width="100%" textAlign="left">
|
||||
<form noValidate spellCheck={false} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
type="email"
|
||||
id="forgot-password-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="Enter your email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
disabled={errors.email !== undefined}
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
level="primary"
|
||||
label="Send instructions"
|
||||
sx={{
|
||||
width: "100%",
|
||||
fontWeight: 400,
|
||||
mt: theme.gap.mlplus,
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.ml} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Field
|
||||
type="email"
|
||||
id="forgot-password-email-input"
|
||||
label="Email"
|
||||
isRequired={true}
|
||||
placeholder="Enter your email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
disabled={errors.email !== undefined}
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
level="primary"
|
||||
label="Reset password"
|
||||
sx={{ mb: theme.gap.medium }}
|
||||
/>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={() => navigate("/login")}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.gap.large}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,11 @@ const LandingPage = ({ onContinue }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} alignItems="center" textAlign="center">
|
||||
<Stack
|
||||
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>We are pleased to see you again!</Typography>
|
||||
@@ -82,7 +86,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
@@ -154,9 +158,16 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (form.email !== "" && !errors.email) {
|
||||
sessionStorage.setItem("email", form.email);
|
||||
}
|
||||
navigate("/forgot-password");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Log In</Typography>
|
||||
<Typography>Enter your password</Typography>
|
||||
@@ -209,7 +220,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={() => navigate("/forgot-password")}
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
Reset password
|
||||
</Typography>
|
||||
@@ -329,10 +340,9 @@ const Login = () => {
|
||||
|
||||
return (
|
||||
<Stack className="login-page auth" overflow="hidden">
|
||||
<img
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
sx={{ backgroundImage: `url(${background})` }}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -348,8 +358,8 @@ const Login = () => {
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
px={{ xs: theme.gap.large, lg: theme.gap.xl }}
|
||||
pb={theme.gap.xl}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import "./index.css";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react";
|
||||
import Button from "../../Components/Button";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Button from "../../Components/Button";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
const NewPasswordConfirmed = () => {
|
||||
const theme = useTheme();
|
||||
@@ -22,39 +22,77 @@ const NewPasswordConfirmed = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="password-confirmed-page">
|
||||
<img
|
||||
<Stack className="password-confirmed-page auth" overflow="hidden">
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
sx={{ backgroundImage: `url(${background})` }}
|
||||
/>
|
||||
<form className="password-confirmed-form">
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<ConfirmIcon alt="confirm icon" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Password reset
|
||||
</Typography>
|
||||
<Typography sx={{ textAlign: "center" }}>
|
||||
Your password has been successfully reset. Click below to log in
|
||||
magically.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.gap.large}
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<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.gap.large, lg: theme.gap.xl }}
|
||||
pb={theme.gap.xl}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.otherColors.graishWhite,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
padding: {
|
||||
xs: theme.gap.large,
|
||||
sm: theme.gap.xl,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<ConfirmIcon alt="password confirm icon" />
|
||||
<Typography component="h1">Password reset</Typography>
|
||||
<Typography mt={theme.gap.xs}>
|
||||
Your password has been successfully reset. Click below to log in
|
||||
magically.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
onClick={() => navigate("/monitors")}
|
||||
/>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={handleNavigate}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.gap.large}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,11 @@ const LandingPage = ({ isAdmin, onSignup }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} alignItems="center" textAlign="center">
|
||||
<Stack
|
||||
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>
|
||||
@@ -87,7 +91,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your personal details</Typography>
|
||||
@@ -107,18 +111,16 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
/>
|
||||
</form>
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Box my={theme.gap.ml}>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
</Box>
|
||||
<Field
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
@@ -129,7 +131,6 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
@@ -173,7 +174,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Enter your email address</Typography>
|
||||
@@ -202,7 +203,6 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
@@ -246,7 +246,7 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={theme.gap.large} textAlign="center">
|
||||
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
|
||||
<Box>
|
||||
<Typography component="h1">Sign Up</Typography>
|
||||
<Typography>Create your password</Typography>
|
||||
@@ -267,23 +267,29 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
/>
|
||||
</form>
|
||||
<form noValidate spellCheck={false} onSubmit={onSubmit}>
|
||||
<Box mt={theme.gap.ml}>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="Confirm your password"
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</form>
|
||||
<Stack gap={theme.gap.small} my={theme.gap.large}>
|
||||
<Stack
|
||||
gap={theme.gap.small}
|
||||
mb={{ xs: theme.gap.ml, sm: theme.gap.large }}
|
||||
>
|
||||
<Check
|
||||
text="Must be at least 8 characters long"
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must be at least</Typography> 8
|
||||
characters long
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
@@ -295,7 +301,12 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain one special character and a number"
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain</Typography> one
|
||||
special character and a number
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
@@ -309,7 +320,14 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain at least one upper and lower character"
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">
|
||||
Must contain at least
|
||||
</Typography>{" "}
|
||||
one upper and lower character
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
@@ -330,7 +348,6 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.xs,
|
||||
@@ -513,10 +530,9 @@ const Register = ({ isAdmin }) => {
|
||||
|
||||
return (
|
||||
<Stack className="register-page auth" overflow="hidden">
|
||||
<img
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
sx={{ backgroundImage: `url(${background})` }}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -532,8 +548,8 @@ const Register = ({ isAdmin }) => {
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
px={{ xs: theme.gap.large, lg: theme.gap.xl }}
|
||||
pb={theme.gap.xl}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import "./index.css";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import LockIcon from "../../assets/icons/lock-button-icon.svg?react";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import ButtonSpinner from "../../Components/ButtonSpinner";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setNewPassword } from "../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import ButtonSpinner from "../../Components/ButtonSpinner";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setNewPassword } from "../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Button from "../../Components/Button";
|
||||
import LockIcon from "../../assets/icons/lock-button-icon.svg?react";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import "./index.css";
|
||||
|
||||
const SetNewPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -105,100 +104,158 @@ const SetNewPassword = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="set-new-password-page">
|
||||
<img
|
||||
<Stack className="set-new-password-page auth" overflow="hidden">
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
sx={{ backgroundImage: `url(${background})` }}
|
||||
/>
|
||||
<form className="set-new-password-form" onSubmit={handleSubmit}>
|
||||
<Stack direction="column" alignItems="center" gap={theme.gap.small}>
|
||||
<LockIcon alt="lock icon" style={{ fill: "white" }} />
|
||||
<Typography component="h1" sx={{ mt: theme.gap.ml }}>
|
||||
Set new password
|
||||
</Typography>
|
||||
<Typography sx={{ textAlign: "center" }}>
|
||||
Your new password must be different to previously used passwords.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} sx={{ mt: `calc(${theme.gap.ml}*2)` }}>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
<Field
|
||||
type="password"
|
||||
id="confirm-password-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
<Stack gap={theme.gap.small}>
|
||||
<Check
|
||||
text="Must be at least 8 characters long"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: form.password.length < 8
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain one special character and a number"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(
|
||||
form.password
|
||||
)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text="Must contain at least one upper and lower character"
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
px={theme.gap.large}
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<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.gap.large, lg: theme.gap.xl }}
|
||||
pb={theme.gap.large}
|
||||
mx="auto"
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.otherColors.graishWhite,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
padding: {
|
||||
xs: theme.gap.large,
|
||||
sm: theme.gap.xl,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
<LockIcon alt="lock icon" />
|
||||
<Typography component="h1">Set new password</Typography>
|
||||
<Typography>
|
||||
Your new password must be different to previously used passwords.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box width="100%" textAlign="left">
|
||||
<form noValidate spellCheck={false} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
label="Password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
/>
|
||||
</form>
|
||||
<form noValidate spellCheck={false} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
type="password"
|
||||
id="confirm-password-input"
|
||||
label="Confirm password"
|
||||
isRequired={true}
|
||||
placeholder="••••••••"
|
||||
value={form.confirm}
|
||||
onChange={handleChange}
|
||||
error={errors.confirm}
|
||||
/>
|
||||
</form>
|
||||
<Stack gap={theme.gap.small} mb={theme.gap.large}>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must be at least</Typography> 8
|
||||
characters long
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: form.password.length < 8
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">Must contain</Typography> one
|
||||
special character and a number
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[!@#$%^&*(),.?":{}|])(?=.*\d).+$/.test(
|
||||
form.password
|
||||
)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<Check
|
||||
text={
|
||||
<>
|
||||
<Typography component="span">
|
||||
Must contain at least
|
||||
</Typography>{" "}
|
||||
one upper and lower character
|
||||
</>
|
||||
}
|
||||
variant={
|
||||
errors?.password === "Password is required"
|
||||
? "error"
|
||||
: form.password === ""
|
||||
? "info"
|
||||
: !/^(?=.*[A-Z])(?=.*[a-z]).+$/.test(form.password)
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<ButtonSpinner
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit}
|
||||
level="primary"
|
||||
label="Reset password"
|
||||
/>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to log in"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
sx={{ alignSelf: "center", width: "fit-content" }}
|
||||
onClick={() => navigate("/login")}
|
||||
sx={{ width: "100%", maxWidth: 400 }}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</div>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.gap.large}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
onClick={() => navigate("/login")}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,80 +1,3 @@
|
||||
/* ////// */
|
||||
/* SHARED */
|
||||
/* ////// */
|
||||
.set-new-password-page,
|
||||
.password-confirmed-page,
|
||||
.forgot-password-page,
|
||||
.check-email-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: var(--env-var-height-1);
|
||||
}
|
||||
.set-new-password-page h1.MuiTypography-root,
|
||||
.password-confirmed-page h1.MuiTypography-root,
|
||||
.forgot-password-page h1.MuiTypography-root,
|
||||
.check-email-page h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.set-new-password-page p.MuiTypography-root,
|
||||
.set-new-password-page button,
|
||||
.password-confirmed-page p.MuiTypography-root,
|
||||
.password-confirmed-page button,
|
||||
.forgot-password-page p.MuiTypography-root,
|
||||
.forgot-password-page button,
|
||||
.check-email-page p.MuiTypography-root,
|
||||
.check-email-page button,
|
||||
.check-email-page span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.set-new-password-page button:not(.MuiIconButton-root),
|
||||
.password-confirmed-page button:not(.MuiIconButton-root),
|
||||
.forgot-password-page button:not(.MuiIconButton-root),
|
||||
.check-email-page button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
border-radius: var(--env-var-radius-2);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.set-new-password-form,
|
||||
.password-confirmed-form,
|
||||
.forgot-password-form,
|
||||
.check-email-form {
|
||||
margin-top: 65px;
|
||||
width: var(--env-var-width-2);
|
||||
}
|
||||
.set-new-password-page p.MuiTypography-root,
|
||||
.password-confirmed-page p.MuiTypography-root,
|
||||
.forgot-password-page p.MuiTypography-root,
|
||||
.check-email-page p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
|
||||
.set-new-password-page button:not(.MuiIconButton-root) svg,
|
||||
.password-confirmed-page button:not(.MuiIconButton-root) svg,
|
||||
.forgot-password-page button svg,
|
||||
.check-email-page button svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.set-new-password-page
|
||||
.MuiFormControl-root:has(#register-password-input)
|
||||
+ span.MuiTypography-root,
|
||||
.password-confirmed-page
|
||||
.MuiFormControl-root:has(#register-password-input)
|
||||
+ span.MuiTypography-root {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* /////////// */
|
||||
/* Check Email */
|
||||
/* /////////// */
|
||||
.check-email-page span.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* AUTH */
|
||||
.auth {
|
||||
height: 100vh;
|
||||
@@ -85,10 +8,8 @@
|
||||
color: var(--env-var-color-3);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field h3.MuiTypography-root,
|
||||
.auth .field input,
|
||||
.auth .field .input-error,
|
||||
.auth p,
|
||||
.auth .field input,
|
||||
.auth span {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
@@ -106,13 +27,13 @@
|
||||
cursor: pointer;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
.auth p > span {
|
||||
.auth p > span:not(.email-sent-to) {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
}
|
||||
.auth p > span:hover {
|
||||
.auth p > span:not(.email-sent-to):hover {
|
||||
color: var(--env-var-color-2);
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
@@ -132,15 +53,80 @@
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.auth .field h3.MuiTypography-root,
|
||||
.auth .field .input-error,
|
||||
.auth .check span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.auth form + form {
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
}
|
||||
.auth .email-sent-to {
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth > .MuiStack-root:nth-of-type(1) {
|
||||
.auth > .MuiStack-root:nth-of-type(2) {
|
||||
height: var(--env-var-nav-bar-height);
|
||||
}
|
||||
.auth .background-pattern-svg {
|
||||
top: 14%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -30%);
|
||||
z-index: -1;
|
||||
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.forgot-password-page h1,
|
||||
.check-email-page h1,
|
||||
.password-confirmed-page h1,
|
||||
.set-new-password-page h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.auth h1 {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field input,
|
||||
.auth p,
|
||||
.auth span {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.auth button:not(.MuiIconButton-root),
|
||||
.auth .field .MuiInputBase-root:has(input) {
|
||||
height: 36px;
|
||||
}
|
||||
.auth .check span.MuiTypography-root,
|
||||
.auth .field .input-error,
|
||||
.auth .field h3.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.auth .check span > span {
|
||||
display: none;
|
||||
}
|
||||
.auth form + form {
|
||||
margin: var(--env-var-spacing-1-plus) 0;
|
||||
}
|
||||
.auth .background-pattern-svg {
|
||||
max-width: 750px;
|
||||
max-height: 750px;
|
||||
}
|
||||
|
||||
.forgot-password-page h1,
|
||||
.check-email-page h1,
|
||||
.password-confirmed-page h1,
|
||||
.set-new-password-page h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
@@ -105,41 +107,55 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Monitor Name</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{check.statusCode}</TableCell>
|
||||
{checks?.length === 0 && selectedMonitor === "0" ? (
|
||||
<Box>
|
||||
<Typography textAlign="center">No incidents recorded yet.</Typography>
|
||||
</Box>
|
||||
) : checks?.length === 0 ? (
|
||||
<Box>
|
||||
<Typography textAlign="center">
|
||||
The monitor you have selected has no recorded incidents yet.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Monitor Name</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{check.statusCode}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
.incidents .MuiPagination-root {
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.incidents > .MuiBox-root {
|
||||
background-color: var(--env-var-color-8);
|
||||
border: solid 1px var(--env-var-color-6);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
/* remove later */
|
||||
.incidents .MuiSelect-select {
|
||||
|
||||
@@ -78,7 +78,7 @@ const Incidents = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="incidents" gap={theme.gap.large}>
|
||||
<Stack className="incidents" pt={theme.gap.xl} gap={theme.gap.large}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
|
||||
@@ -108,7 +108,7 @@ const Integrations = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack className="integrations" gap={theme.gap.xs}>
|
||||
<Stack className="integrations" pt={theme.gap.xl} gap={theme.gap.xs}>
|
||||
<Typography component="h1">Integrations</Typography>
|
||||
<Typography mb={theme.gap.large}>
|
||||
Connect BlueWave Uptime to your favorite service.
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
.configure-monitor-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--env-var-spacing-4);
|
||||
gap: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root,
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
.create-monitor-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--env-var-spacing-4);
|
||||
gap: var(--env-var-spacing-2);
|
||||
}
|
||||
|
||||
.create-monitor .MuiStack-root .MuiButtonGroup-root button {
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
color: var(--env-var-color-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.monitor-details h2.MuiTypography-root,
|
||||
.monitor-details h6.MuiTypography-root {
|
||||
.monitor-details h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.monitor-details h2.MuiTypography-root,
|
||||
.monitor-details h4.MuiTypography-root {
|
||||
.monitor-details h2.MuiTypography-root{
|
||||
font-weight: 600;
|
||||
}
|
||||
.monitor-details h2.MuiTypography-root,
|
||||
@@ -35,15 +33,21 @@
|
||||
}
|
||||
|
||||
.monitor-details .stat-box {
|
||||
flex: 1;
|
||||
flex: 1 20%;
|
||||
min-width: 100px;
|
||||
border: solid 1px;
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-spacing-1) 18px;
|
||||
padding: var(--env-var-spacing-1) 16px;
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.monitor-details .stat-box h6.MuiTypography-root {
|
||||
margin-bottom: var(--env-var-spacing-1);
|
||||
.monitor-details h6.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
.monitor-details h6.MuiTypography-root span.MuiTypography-root {
|
||||
font-style: italic;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.monitor-details button.MuiButtonBase-root,
|
||||
.monitor-details .stat-box {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import Button from "../../../Components/Button";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
@@ -18,20 +17,23 @@ import {
|
||||
formatDurationRounded,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import "./index.css";
|
||||
import MonitorDetails60MinChart from "../../../Components/Charts/MonitorDetails60MinChart";
|
||||
|
||||
const StatBox = ({ title, value }) => {
|
||||
return (
|
||||
<Box className="stat-box">
|
||||
<Typography variant="h6" sx={{ fontWeight: 400 }}>
|
||||
<Typography variant="h6" mb={1} fontWeight={500}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h4">{value}</Typography>
|
||||
<Typography variant="h4" fontWeight={500}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
@@ -114,12 +116,13 @@ const DetailsPage = () => {
|
||||
const { monitorId } = useParams();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
const [certificateExpiry, setCertificateExpiry] = useState("N/A");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`,
|
||||
`/monitors/stats/${monitorId}?dateRange=${dateRange}&numToDisplay=50&normalize=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -137,71 +140,23 @@ const DetailsPage = () => {
|
||||
fetchMonitor();
|
||||
}, [fetchMonitor]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCertificate = async () => {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/certificate/${monitorId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
setCertificateExpiry(res.data.data.certificateDate);
|
||||
};
|
||||
fetchCertificate();
|
||||
}, [authToken, monitorId]);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
/**
|
||||
* Function to calculate uptime duration based on the most recent check.
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Uptime duration in ms.
|
||||
*/
|
||||
|
||||
// TODO: This can be done more efficiently by iteratting backwards
|
||||
// and breaking when the first down check is found, calculate current time - downtime
|
||||
const calculateUptimeDuration = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const latestCheck = new Date(checks[checks.length - 1].createdAt);
|
||||
let latestDownCheck = 0;
|
||||
|
||||
// Checks are ordered oldest -> newest
|
||||
// So we iterate backwards and find the first down check
|
||||
for (let i = checks.length - 1; i >= 0; i--) {
|
||||
if (checks[i].status === false) {
|
||||
latestDownCheck = new Date(checks[i].createdAt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no down check is found, uptime is from the first check to now
|
||||
if (latestDownCheck === 0) {
|
||||
return Date.now() - new Date(checks[0].createdAt);
|
||||
}
|
||||
|
||||
// Otherwise the uptime is from the last check to the last down check
|
||||
return latestCheck - latestDownCheck;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get duration since last check
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getLastChecked = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
// Data is sorted oldest -> newest, so last check is the most recent
|
||||
return new Date() - new Date(checks[checks.length - 1].createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to count incidents (checks with status === false).
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Number of incidents.
|
||||
*/
|
||||
const countIncidents = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
return checks.reduce((acc, check) => {
|
||||
return check.status === false ? (acc += 1) : acc;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
return (
|
||||
<Box className="monitor-details">
|
||||
{loading ? (
|
||||
@@ -210,12 +165,12 @@ const DetailsPage = () => {
|
||||
<>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to Monitors"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
@@ -224,12 +179,23 @@ const DetailsPage = () => {
|
||||
}}
|
||||
/>
|
||||
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
mt={theme.gap.small}
|
||||
alignItems="baseline"
|
||||
>
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
<Typography component="h1" sx={{ lineHeight: 1 }}>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Stack direction="row">
|
||||
<Typography
|
||||
component="h1"
|
||||
sx={{ lineHeight: 1, alignSelf: "flex-end" }}
|
||||
>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<MonitorDetails60MinChart data={monitor.statusBar} />
|
||||
</Stack>
|
||||
<Typography mt={theme.gap.small}>
|
||||
<Typography
|
||||
component="span"
|
||||
@@ -242,8 +208,6 @@ const DetailsPage = () => {
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
Last time checked{" "}
|
||||
{formatDurationRounded(getLastChecked(monitor?.checks))} ago.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -274,18 +238,52 @@ const DetailsPage = () => {
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.large}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatBox
|
||||
title="Currently up for"
|
||||
value={formatDuration(calculateUptimeDuration(monitor.checks))}
|
||||
value={formatDuration(monitor?.uptimeDuration)}
|
||||
/>
|
||||
<StatBox
|
||||
title="Last checked"
|
||||
value={`${formatDuration(getLastChecked(monitor.checks))} ago`}
|
||||
title="Last check"
|
||||
value={`${formatDurationRounded(monitor?.lastChecked)} ago`}
|
||||
/>
|
||||
<StatBox title="Incidents" value={monitor?.incidents} />
|
||||
<StatBox title="Certificate Expiry" value={certificateExpiry} />
|
||||
<StatBox
|
||||
title="Latest response time"
|
||||
value={monitor?.latestResponseTime}
|
||||
/>
|
||||
<StatBox
|
||||
title="Incidents"
|
||||
value={countIncidents(monitor.checks)}
|
||||
title={
|
||||
<>
|
||||
Avg. Response Time{" "}
|
||||
<Typography component="span">(24-hr)</Typography>
|
||||
</>
|
||||
}
|
||||
value={parseFloat(monitor?.avgResponseTime24hours)
|
||||
.toFixed(2)
|
||||
.replace(/\.?0+$/, "")}
|
||||
/>
|
||||
<StatBox
|
||||
title={
|
||||
<>
|
||||
Uptime <Typography component="span">(24-hr)</Typography>
|
||||
</>
|
||||
}
|
||||
value={`${parseFloat(monitor?.uptime24Hours)
|
||||
.toFixed(2)
|
||||
.replace(/\.?0+$/, "")}%`}
|
||||
/>
|
||||
<StatBox
|
||||
title={
|
||||
<>
|
||||
Uptime <Typography component="span">(30-day)</Typography>
|
||||
</>
|
||||
}
|
||||
value={`${parseFloat(monitor?.uptime30Days)
|
||||
.toFixed(2)
|
||||
.replace(/\.?0+$/, "")}%`}
|
||||
/>
|
||||
</Stack>
|
||||
<Box>
|
||||
@@ -294,7 +292,7 @@ const DetailsPage = () => {
|
||||
justifyContent="space-between"
|
||||
mb={theme.gap.ml}
|
||||
>
|
||||
<Typography component="h2" mb={theme.gap.small}>
|
||||
<Typography component="h2" alignSelf="flex-end">
|
||||
Response Times
|
||||
</Typography>
|
||||
<ButtonGroup>
|
||||
@@ -330,8 +328,18 @@ const DetailsPage = () => {
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<MonitorDetailsAreaChart checks={monitor.checks} />
|
||||
<Box
|
||||
p={theme.gap.mlplus}
|
||||
pb={theme.gap.xs}
|
||||
backgroundColor={theme.palette.otherColors.white}
|
||||
border={1}
|
||||
borderColor={theme.palette.otherColors.graishWhite}
|
||||
borderRadius={`${theme.shape.borderRadius}px`}
|
||||
sx={{ height: "250px" }}
|
||||
>
|
||||
<MonitorDetailsAreaChart
|
||||
checks={[...monitor.checks].reverse()}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.ml}>
|
||||
|
||||
@@ -120,12 +120,11 @@ const Monitors = () => {
|
||||
};
|
||||
const closeMenu = () => {
|
||||
setAnchorEl(null);
|
||||
setActions({});
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const openRemove = () => {
|
||||
setAnchorEl(null);
|
||||
closeMenu();
|
||||
setIsOpen(true);
|
||||
};
|
||||
const handleRemove = async (event) => {
|
||||
@@ -220,7 +219,11 @@ const Monitors = () => {
|
||||
aria-label="monitor actions"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openMenu(event, monitor._id, monitor.url);
|
||||
openMenu(
|
||||
event,
|
||||
monitor._id,
|
||||
monitor.type === "ping" ? null : monitor.url
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
@@ -240,7 +243,7 @@ const Monitors = () => {
|
||||
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="monitors" gap={theme.gap.large}>
|
||||
<Stack className="monitors" pt={theme.gap.xl} gap={theme.gap.large}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
@@ -250,7 +253,10 @@ const Monitors = () => {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography component="h1">
|
||||
<Typography
|
||||
component="h1"
|
||||
sx={{ lineHeight: 1, alignSelf: "flex-end" }}
|
||||
>
|
||||
Hello, {authState.user.firstName}
|
||||
</Typography>
|
||||
{monitorState.monitors?.length !== 0 && (
|
||||
@@ -299,7 +305,8 @@ const Monitors = () => {
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
p={theme.gap.xl}
|
||||
p={theme.gap.lgplus}
|
||||
flex={1}
|
||||
border={1}
|
||||
borderColor={theme.palette.otherColors.graishWhite}
|
||||
backgroundColor={theme.palette.otherColors.white}
|
||||
@@ -326,13 +333,17 @@ const Monitors = () => {
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={closeMenu}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
window.open(actions.url, "_blank", "noreferrer");
|
||||
}}
|
||||
>
|
||||
Open site
|
||||
</MenuItem>
|
||||
{actions.url !== null ? (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
window.open(actions.url, "_blank", "noreferrer");
|
||||
}}
|
||||
>
|
||||
Open site
|
||||
</MenuItem>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<MenuItem onClick={() => navigate(`/monitors/${actions.id}`)}>
|
||||
Details
|
||||
</MenuItem>
|
||||
|
||||
@@ -106,7 +106,7 @@ const CreatePageSpeed = () => {
|
||||
onClick={() => navigate("/pagespeed")}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
mb: theme.gap.large,
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
@@ -122,8 +122,6 @@ const CreatePageSpeed = () => {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.gap.large,
|
||||
// TODO
|
||||
maxWidth: "1000px",
|
||||
}}
|
||||
>
|
||||
<Typography component="h1">Create a page speed monitor</Typography>
|
||||
|
||||
@@ -5,8 +5,10 @@ import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDate, formatDurationRounded } from "../../../Utils/timeUtils";
|
||||
import { getLastChecked } from "../../../Utils/monitorUtils";
|
||||
import {
|
||||
formatDuration,
|
||||
formatDurationRounded,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import Button from "../../../Components/Button";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
@@ -185,7 +187,7 @@ const PageSpeedDetails = () => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=desc`,
|
||||
`/monitors/stats/${monitorId}?sortOrder=desc&limit=50`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -297,7 +299,7 @@ const PageSpeedDetails = () => {
|
||||
const [expand, setExpand] = useState(false);
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
|
||||
return (
|
||||
<Stack className="page-speed-details" gap={theme.gap.large}>
|
||||
{loading ? (
|
||||
@@ -361,7 +363,7 @@ const PageSpeedDetails = () => {
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xl}
|
||||
gap={theme.gap.large}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatBox
|
||||
@@ -369,14 +371,13 @@ const PageSpeedDetails = () => {
|
||||
title="Last checked"
|
||||
value={
|
||||
<>
|
||||
{formatDate(getLastChecked(monitor?.checks, false))}{" "}
|
||||
{formatDuration(monitor?.lastChecked)}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
fontStyle="italic"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
({formatDurationRounded(getLastChecked(monitor?.checks))}{" "}
|
||||
ago)
|
||||
ago
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
@@ -386,17 +387,13 @@ const PageSpeedDetails = () => {
|
||||
title="Checks since"
|
||||
value={
|
||||
<>
|
||||
{formatDate(new Date(monitor?.createdAt))}{" "}
|
||||
{formatDuration(monitor?.uptimeDuration)}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
fontStyle="italic"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
(
|
||||
{formatDurationRounded(
|
||||
new Date() - new Date(monitor?.createdAt)
|
||||
)}{" "}
|
||||
ago)
|
||||
ago
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
@@ -409,12 +406,10 @@ const PageSpeedDetails = () => {
|
||||
</Stack>
|
||||
<Typography component="h2">Score history</Typography>
|
||||
<Box height="300px">
|
||||
<PageSpeedLineChart
|
||||
pageSpeedChecks={monitor?.checks?.slice(0, 25)}
|
||||
/>
|
||||
<PageSpeedLineChart pageSpeedChecks={data} />
|
||||
</Box>
|
||||
<Typography component="h2">Performance report</Typography>
|
||||
<Stack direction="row" alignItems="center" overflow="hidden">
|
||||
<Stack direction="row" alignItems="center" overflow="hidden" flex={1}>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
|
||||
@@ -136,7 +136,7 @@ const PageSpeed = () => {
|
||||
let isActuallyLoading = isLoading && monitors.length === 0;
|
||||
|
||||
return (
|
||||
<Box className="page-speed">
|
||||
<Box className="page-speed" pt={theme.gap.xl}>
|
||||
{isActuallyLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : monitors?.length !== 0 ? (
|
||||
|
||||
@@ -96,9 +96,10 @@ const theme = createTheme({
|
||||
ml: "16px",
|
||||
mlplus: "20px",
|
||||
large: "24px",
|
||||
lgplus: "32px",
|
||||
xl: "40px",
|
||||
xxl: "60px",
|
||||
triplexl: "120px"
|
||||
triplexl: "120px",
|
||||
},
|
||||
alert: {
|
||||
info: {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 9L12 15L18 9" stroke="#667085" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 212 B |
@@ -58,6 +58,7 @@
|
||||
--env-var-color-30: #fcfcfd;
|
||||
--env-var-color-31: #1a1919;
|
||||
--env-var-color-32: #f5f5f5;
|
||||
--env-var-color-33: rgba(83, 83, 83, 0.1);
|
||||
|
||||
--env-var-radius-1: 4px;
|
||||
--env-var-radius-2: 8px;
|
||||
@@ -155,14 +156,6 @@ button:focus-visible {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.background-pattern-svg {
|
||||
position: absolute;
|
||||
top: -5%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.MuiTouchRipple-root {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||

|
||||

|
||||
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">BlueWave Uptime</a></h1>
|
||||
|
||||
<p align="center"><strong>An open source server monitoring application</strong></p>
|
||||
@@ -24,31 +23,33 @@ BlueWave Uptime is an open source server monitoring application used to track th
|
||||
- [x] Incidents at a glance
|
||||
- [x] Page speed monitoring
|
||||
- [ ] Scheduled maintenance (in the works)
|
||||
- [ ] Status pages (in the works)
|
||||
|
||||
**Roadmap (short term):**
|
||||
|
||||
- [ ] Memory, disk and CPU monitoring
|
||||
- [ ] 3rd party integrations
|
||||
- [ ] DNS monitoring
|
||||
- [ ] SSL monitoring
|
||||
|
||||
**Roadmap (long term):**
|
||||
- [ ] Memory, disk and CPU monitoring using OpenTelemetry
|
||||
|
||||
- [ ] Status pages
|
||||
|
||||
## Tech stack
|
||||
|
||||
* [ReactJs](https://react.dev/)
|
||||
* [MUI (React framework)](https://mui.com/)
|
||||
* [Node.js](https://nodejs.org/en)
|
||||
* [MongoDB](https://mongodb.com)
|
||||
- [ReactJs](https://react.dev/)
|
||||
- [MUI (React framework)](https://mui.com/)
|
||||
- [Node.js](https://nodejs.org/en)
|
||||
- [MongoDB](https://mongodb.com)
|
||||
|
||||
## Contributing
|
||||
|
||||
We love contributors. Here's how you can contribute:
|
||||
|
||||
* Check [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md).
|
||||
* Have a look at our Figma designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). We encourage you to copy to your own Figma page, then work on it as it is read-only.
|
||||
* Open an issue if you believe you've encountered a bug
|
||||
* Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
- Check [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md).
|
||||
- Have a look at our Figma designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). We encourage you to copy to your own Figma page, then work on it as it is read-only.
|
||||
- Open an issue if you believe you've encountered a bug
|
||||
- Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
|
||||
<a href="https://github.com/bluewave-labs/bluewave-uptime/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/bluewave-uptime" />
|
||||
@@ -58,12 +59,12 @@ Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
||||

|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Clone this repository to your local machine
|
||||
|
||||
1. [Docker Quickstart](#docker-quickstart)
|
||||
1. [Quickstart for Developers](#dev-quickstart)
|
||||
1. [Docker Compose](#docker-compose)
|
||||
1. [Installation (Client)](#client)
|
||||
1. [Configuration(Client)](#config-client)
|
||||
- [Environment](#env-vars-client)
|
||||
@@ -117,9 +118,41 @@ Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
||||
---
|
||||
|
||||
### Docker Quick Start
|
||||
#### <u>Quickstart for Developers</u> <a id="dev-quickstart"></a>
|
||||
|
||||
#### <u>Docker Quickstart</u> <a id="docker-quickstart"></a>
|
||||
<span style="color: red; font-weight: bold;">MAKE SURE YOU CD TO THE SPECIFIED DIRECTORIES AS PATHS IN COMMANDS ARE RELATIVE</span>
|
||||
|
||||
##### Cloning and Initial Setup
|
||||
|
||||
1. Clone this repository
|
||||
2. Checkout the `develop` branch `git checkout develop`
|
||||
|
||||
##### Docker Images Setup
|
||||
|
||||
3. <span style="color: red; font-weight: bold;">CD</span> to the `Docker` directory
|
||||
4. Run `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
|
||||
5. Run `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
|
||||
|
||||
##### Server Setup
|
||||
|
||||
6. <span style="color: red; font-weight: bold;">CD</span> to `Server` directory, run `npm install`
|
||||
7. While in `Server` directory, create a `.env` file with the [required environmental variables](#env-vars-server)
|
||||
8. While in the `Server` directory, run `npm run dev`
|
||||
|
||||
##### Client Setup
|
||||
|
||||
9. <span style="color: red; font-weight: bold;">CD</span> to `Client` directory `run npm install`
|
||||
10. While in the `Client` directory, create a `.env` file with the [required environmental variables](#env-vars-client)
|
||||
11. While in the `Client` cirectory run `npm run dev`
|
||||
|
||||
##### Access Application
|
||||
|
||||
12. Client is running at `localhost:5173`
|
||||
13. Server is running at `localhost:5000`
|
||||
|
||||
---
|
||||
|
||||
#### <u>Docker Compose</u> <a id="docker-compose"></a>
|
||||
|
||||
The fastest way to start the application is to use our Dockerfiles and [Docker Compose](https://docs.docker.com/compose/).
|
||||
|
||||
@@ -143,7 +176,7 @@ SYSTEM_EMAIL_ADDRESS=<system_email>
|
||||
SYSTEM_EMAIL_PASSWORD=<system_email_password>
|
||||
```
|
||||
|
||||
3. In the `Docker` directory, create a `client.env` file with the [required environtmental variables](#env-vars-client) for the client. Sample file:
|
||||
3. In the `Client` directory, create a `client.env` file with the [required environtmental variables](#env-vars-client) for the client. Sample file:
|
||||
|
||||
```
|
||||
VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
|
||||
@@ -151,7 +184,7 @@ VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
|
||||
|
||||
4. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images.
|
||||
|
||||
That's it, the application is ready to use on port 5173.
|
||||
That's it, the application is ready to use on port 80.
|
||||
<br/>
|
||||
|
||||
### Client
|
||||
@@ -2112,4 +2145,3 @@ const myRoute = async(req, res, next) => {
|
||||
```
|
||||
|
||||
Errors should not be handled at the controller level and should be left to the middleware to handle.
|
||||
|
||||
@@ -126,11 +126,12 @@ const loginController = async (req, res, next) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
// Check if user exists
|
||||
const user = await req.db.getUserByEmail(req, res);
|
||||
const user = await req.db.getUserByEmail(email);
|
||||
|
||||
// Compare password
|
||||
const match = await user.comparePassword(req.body.password);
|
||||
const match = await user.comparePassword(password);
|
||||
if (match !== true) {
|
||||
throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
|
||||
}
|
||||
@@ -167,7 +168,7 @@ const logoutController = async (req, res, next) => {
|
||||
try {
|
||||
// Get user
|
||||
const { email } = req.body;
|
||||
const userToLogout = await req.db.getUserByEmail({ body: { email } }, res);
|
||||
const userToLogout = await req.db.getUserByEmail(email);
|
||||
userToLogout.authToken = null;
|
||||
await userToLogout.save();
|
||||
|
||||
@@ -216,7 +217,7 @@ const userEditController = async (req, res, next) => {
|
||||
// Add user email to body for DB operation
|
||||
req.body.email = email;
|
||||
// Get user
|
||||
const user = await req.db.getUserByEmail(req, res);
|
||||
const user = await req.db.getUserByEmail(email);
|
||||
// Compare passwords
|
||||
const match = await user.comparePassword(req.body.password);
|
||||
// If not a match, throw a 403
|
||||
@@ -350,7 +351,8 @@ const recoveryRequestController = async (req, res, next) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await req.db.getUserByEmail(req, res);
|
||||
const { email } = req.body;
|
||||
const user = await req.db.getUserByEmail(email);
|
||||
if (user) {
|
||||
const recoveryToken = await req.db.requestRecoveryToken(req, res);
|
||||
const name = user.firstName;
|
||||
@@ -462,41 +464,16 @@ const deleteUserController = async (req, res, next) => {
|
||||
const decodedToken = jwt.decode(token);
|
||||
const { _id, email } = decodedToken;
|
||||
|
||||
// TODO fix this hack
|
||||
const decodedTokenCastedAsRequest = {
|
||||
query: {},
|
||||
params: {
|
||||
userId: _id,
|
||||
},
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
};
|
||||
|
||||
// Validate user
|
||||
try {
|
||||
await deleteUserParamValidation.validateAsync(
|
||||
decodedTokenCastedAsRequest.body
|
||||
);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
const user = await req.db.getUserByEmail(decodedTokenCastedAsRequest);
|
||||
const user = await req.db.getUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new Error(errorMessages.DB_USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 1. Find all the monitors associated with the user id
|
||||
const monitors = await req.db.getMonitorsByUserId(
|
||||
decodedTokenCastedAsRequest
|
||||
);
|
||||
const monitors = await req.db.getMonitorsByUserId({
|
||||
params: { userId: _id },
|
||||
});
|
||||
|
||||
if (monitors) {
|
||||
//2. Remove all jobs, delete checks and alerts
|
||||
@@ -514,7 +491,7 @@ const deleteUserController = async (req, res, next) => {
|
||||
await req.db.deleteMonitorsByUserId(user._id);
|
||||
|
||||
// 4. Delete the user by id
|
||||
await req.db.deleteUser(decodedTokenCastedAsRequest);
|
||||
await req.db.deleteUser(user._id);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
getMonitorsByUserIdQueryValidation,
|
||||
} = require("../validation/joi");
|
||||
|
||||
const sslChecker = require("ssl-checker");
|
||||
const SERVICE_NAME = "monitorController";
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
const { runInNewContext } = require("vm");
|
||||
@@ -33,6 +34,73 @@ const getAllMonitors = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns monitor stats for monitor with matching ID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorStatsById = async (req, res, next) => {
|
||||
try {
|
||||
//Validation
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const monitorStats = await req.db.getMonitorStatsById(req);
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONTIOR_STATS_BY_ID,
|
||||
data: monitorStats,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getMonitorCertificate = async (req, res, next) => {
|
||||
try {
|
||||
//validation
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const monitor = await req.db.getMonitorById(req, res);
|
||||
const monitorUrl = new URL(monitor.url);
|
||||
const certificate = await sslChecker(monitorUrl.hostname);
|
||||
if (certificate && certificate.validTo) {
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_CERTIFICATE,
|
||||
data: {
|
||||
certificateDate: new Date(certificate.validTo).toLocaleDateString(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_CERTIFICATE,
|
||||
data: { certificateDate: "N/A" },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns monitor with matching ID
|
||||
* @async
|
||||
@@ -269,6 +337,8 @@ const editMonitor = async (req, res, next) => {
|
||||
|
||||
module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
createMonitor,
|
||||
|
||||
@@ -64,6 +64,7 @@ const {
|
||||
|
||||
const {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
createMonitor,
|
||||
@@ -142,6 +143,7 @@ module.exports = {
|
||||
resetPassword,
|
||||
checkAdmin,
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
createMonitor,
|
||||
|
||||
@@ -33,7 +33,6 @@ const getChecksCount = async (req) => {
|
||||
const monitorId = req.params.monitorId;
|
||||
const dateRange = req.query.dateRange;
|
||||
const filter = req.query.filter;
|
||||
console.log(filter);
|
||||
// Build query
|
||||
const checksQuery = { monitorId: monitorId };
|
||||
// Filter checks by "day", "week", or "month"
|
||||
|
||||
@@ -22,6 +22,251 @@ const getAllMonitors = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to calculate uptime duration based on the most recent check.
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Uptime duration in ms.
|
||||
*/
|
||||
const calculateUptimeDuration = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const latestCheck = new Date(checks[0].createdAt);
|
||||
let latestDownCheck = 0;
|
||||
|
||||
for (let i = 0; i < checks.length; i++) {
|
||||
if (checks[i].status === false) {
|
||||
latestDownCheck = new Date(checks[i].createdAt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no down check is found, uptime is from the last check to now
|
||||
if (latestDownCheck === 0) {
|
||||
return Date.now() - new Date(checks[checks.length - 1].createdAt);
|
||||
}
|
||||
|
||||
// Otherwise the uptime is from the last check to the last down check
|
||||
return latestCheck - latestDownCheck;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get duration since last check
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getLastChecked = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
// Data is sorted newest->oldest, so last check is the most recent
|
||||
return new Date() - new Date(checks[0].createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get latestResponseTime
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getLatestResponseTime = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return checks[0].responseTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get average 24h response time
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getAverageResponseTime24Hours = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const aggResponseTime = checks.reduce((sum, check) => {
|
||||
return sum + check.responseTime;
|
||||
}, 0);
|
||||
return aggResponseTime / checks.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get precentage 24h uptime
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
|
||||
const getUptimePercentage = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const upCount = checks.reduce((count, check) => {
|
||||
return check.status === true ? count + 1 : count;
|
||||
}, 0);
|
||||
return (upCount / checks.length) * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get all incidents
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
|
||||
const getIncidents = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0; // Handle case when no checks are available
|
||||
}
|
||||
return checks.reduce((acc, check) => {
|
||||
return check.status === false ? (acc += 1) : acc;
|
||||
}, 0);
|
||||
};
|
||||
/**
|
||||
* Helper function to get all incidents
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {Array<Boolean>} Array of booleans representing up/down.
|
||||
*/
|
||||
const getStatusBarValues = (monitor, checks) => {
|
||||
const checksIn60Mins = Math.floor((60 * 60 * 1000) / monitor.interval);
|
||||
const noBlankChecks = checksIn60Mins - checks.length;
|
||||
|
||||
const statusBarValues = checks.map((check) => {
|
||||
return {
|
||||
status: check.status,
|
||||
responseTime: check.responseTime,
|
||||
value: 75,
|
||||
};
|
||||
});
|
||||
|
||||
for (let i = 0; i < noBlankChecks; i++) {
|
||||
statusBarValues.push({ status: undefined, responseTime: 0, value: 75 });
|
||||
}
|
||||
return statusBarValues.reverse();
|
||||
};
|
||||
/**
|
||||
* Get stats by monitor ID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Monitor>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorStatsById = async (req) => {
|
||||
const filterLookup = {
|
||||
hour: new Date(new Date().getTime() - 60 * 60 * 1000),
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
};
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
let { status, limit, sortOrder, dateRange, numToDisplay, normalize } =
|
||||
req.query;
|
||||
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Get monitor
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
|
||||
// Determine if this is a pagespeed monitor or an http/ping monitor
|
||||
let model =
|
||||
monitor.type === "http" || monitor.type === "ping"
|
||||
? Check
|
||||
: PageSpeedCheck;
|
||||
|
||||
// Build monitor stats object
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
};
|
||||
|
||||
// Start building query
|
||||
const checksQuery = { monitorId: monitor._id };
|
||||
|
||||
// Get all checks
|
||||
const checksAll = await model.find(checksQuery).sort({
|
||||
createdAt: sortOrder,
|
||||
});
|
||||
|
||||
if (monitor.type === "http" || monitor.type === "ping") {
|
||||
const checksQuery24Hours = {
|
||||
...checksQuery,
|
||||
createdAt: { $gte: filterLookup.day },
|
||||
};
|
||||
const checksQuery30Days = {
|
||||
...checksQuery,
|
||||
createdAt: { $gte: filterLookup.month },
|
||||
};
|
||||
const checksQuery60Mins = {
|
||||
...checksQuery,
|
||||
createdAt: { $gte: filterLookup.hour },
|
||||
};
|
||||
|
||||
const [checks24Hours, checks30Days, checks60Mins] = await Promise.all([
|
||||
model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
|
||||
model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
|
||||
model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
|
||||
]);
|
||||
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.avgResponseTime24hours =
|
||||
getAverageResponseTime24Hours(checks24Hours);
|
||||
monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
|
||||
monitorStats.uptime30Days = getUptimePercentage(checks30Days);
|
||||
monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins);
|
||||
}
|
||||
|
||||
//Get checks for dateRange
|
||||
if (status !== undefined) {
|
||||
checksQuery.status = status;
|
||||
}
|
||||
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[dateRange] };
|
||||
}
|
||||
|
||||
let dateRangeChecks = await model
|
||||
.find(checksQuery)
|
||||
.sort({
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
|
||||
const incidents = getIncidents(dateRangeChecks);
|
||||
|
||||
// If more than numToDisplay checks, pick every nth check
|
||||
if (
|
||||
numToDisplay !== undefined &&
|
||||
dateRangeChecks &&
|
||||
dateRangeChecks.length > numToDisplay
|
||||
) {
|
||||
const n = Math.ceil(dateRangeChecks.length / numToDisplay);
|
||||
dateRangeChecks = dateRangeChecks.filter((_, index) => index % n === 0);
|
||||
}
|
||||
|
||||
// Normalize checks if requested
|
||||
if (normalize !== undefined) {
|
||||
dateRangeChecks = NormalizeData(dateRangeChecks, 1, 100);
|
||||
}
|
||||
|
||||
// Add common stats and stats that depend on the dateRange
|
||||
monitorStats.uptimeDuration = calculateUptimeDuration(checksAll);
|
||||
monitorStats.lastChecked = getLastChecked(checksAll);
|
||||
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
|
||||
monitorStats.incidents = incidents;
|
||||
monitorStats.checks = dateRangeChecks;
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a monitor by ID
|
||||
* @async
|
||||
@@ -30,69 +275,10 @@ const getAllMonitors = async (req, res) => {
|
||||
* @returns {Promise<Monitor>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorById = async (req, res) => {
|
||||
const getMonitorById = async (monitorId) => {
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
let { status, limit, sortOrder, filter, numToDisplay, normalize } =
|
||||
req.query;
|
||||
|
||||
const filterLookup = {
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
};
|
||||
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
if (sortOrder === "asc") {
|
||||
sortOrder = 1;
|
||||
} else if (sortOrder === "desc") {
|
||||
sortOrder = -1;
|
||||
} else sortOrder = -1;
|
||||
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
|
||||
const checksQuery = { monitorId: monitor._id };
|
||||
|
||||
if (status !== undefined) {
|
||||
checksQuery.status = status;
|
||||
}
|
||||
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (filter !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[filter] };
|
||||
}
|
||||
|
||||
// Determine model type
|
||||
let model =
|
||||
monitor.type === "http" || monitor.type === "ping"
|
||||
? Check
|
||||
: PageSpeedCheck;
|
||||
|
||||
let checks = await model
|
||||
.find(checksQuery)
|
||||
.sort({
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
|
||||
// If more than numToDisplay checks, pick every nth check
|
||||
if (numToDisplay !== undefined && checks && checks.length > numToDisplay) {
|
||||
const n = Math.ceil(checks.length / numToDisplay);
|
||||
checks = checks.filter(
|
||||
(_, index) => index % n === 0 || index === checks.length - 1
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize checks if requested
|
||||
if (normalize !== undefined) {
|
||||
checks = NormalizeData(checks, 1, 100);
|
||||
}
|
||||
const notifications = await Notification.find({ monitorId: monitor._id });
|
||||
const monitorWithChecks = { ...monitor.toObject(), checks, notifications };
|
||||
return monitorWithChecks;
|
||||
return monitor;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -263,6 +449,7 @@ const editMonitor = async (req, res) => {
|
||||
|
||||
module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
createMonitor,
|
||||
|
||||
@@ -51,11 +51,11 @@ const insertUser = async (req, res) => {
|
||||
* @returns {Promise<UserModel>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getUserByEmail = async (req, res) => {
|
||||
const getUserByEmail = async (email) => {
|
||||
try {
|
||||
// Need the password to be able to compare, removed .select()
|
||||
// We can strip the hash before returing the user
|
||||
const user = await UserModel.findOne({ email: req.body.email }).select(
|
||||
const user = await UserModel.findOne({ email: email }).select(
|
||||
"-profileImage"
|
||||
);
|
||||
if (user) {
|
||||
@@ -126,8 +126,7 @@ const updateUser = async (req, res) => {
|
||||
* @returns {Promise<UserModel>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const deleteUser = async (req, res) => {
|
||||
const userId = req.params.userId;
|
||||
const deleteUser = async (userId) => {
|
||||
try {
|
||||
const deletedUser = await UserModel.findByIdAndDelete(userId);
|
||||
if (!deletedUser) {
|
||||
|
||||
+3
-1
@@ -16,6 +16,7 @@ const maintenanceWindowRouter = require("./routes/maintenanceWindowRoute");
|
||||
const { connectDbAndRunServer } = require("./configs/db");
|
||||
const queueRouter = require("./routes/queueRoute");
|
||||
const JobQueue = require("./service/jobQueue");
|
||||
const NetworkService = require("./service/networkService");
|
||||
const EmailService = require("./service/emailService");
|
||||
const PageSpeedService = require("./service/pageSpeedService");
|
||||
|
||||
@@ -114,8 +115,9 @@ const startApp = async () => {
|
||||
|
||||
// Create services
|
||||
await connectDbAndRunServer(app, db);
|
||||
const jobQueue = await JobQueue.createJobQueue(db);
|
||||
const emailService = new EmailService();
|
||||
const networkService = new NetworkService(db, emailService);
|
||||
const jobQueue = await JobQueue.createJobQueue(db, networkService);
|
||||
const pageSpeedService = new PageSpeedService();
|
||||
|
||||
const cleanup = async () => {
|
||||
|
||||
+1
-62
@@ -57,6 +57,7 @@ const CheckSchema = mongoose.Schema(
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
@@ -68,66 +69,4 @@ const CheckSchema = mongoose.Schema(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Pre-save middleware to handle status change notifications.
|
||||
*
|
||||
* This middleware checks if the status of the monitor associated
|
||||
* with the check has changed and sends notifications if necessary.
|
||||
*/
|
||||
CheckSchema.pre("save", async function (next) {
|
||||
try {
|
||||
const monitor = await mongoose.model("Monitor").findById(this.monitorId);
|
||||
|
||||
if (monitor) {
|
||||
const notifications = await Notification.find({
|
||||
monitorId: this.monitorId,
|
||||
});
|
||||
|
||||
// Check if there are any notifications
|
||||
if (notifications && notifications.length > 0) {
|
||||
// Only send email if monitor status has changed
|
||||
if (monitor.status !== this.status) {
|
||||
const emailService = new EmailService();
|
||||
|
||||
if (monitor.status === true && this.status === false) {
|
||||
// Notify users that the monitor is down
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await emailService.buildAndSendEmail(
|
||||
"serverIsDownTemplate",
|
||||
{ monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
notification.address,
|
||||
`Monitor ${monitor.name} is down`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (monitor.status === false && this.status === true) {
|
||||
// Notify users that the monitor is up
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await emailService.buildAndSendEmail(
|
||||
"serverIsUpTemplate",
|
||||
{ monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
notification.address,
|
||||
`Monitor ${monitor.name} is back up`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update monitor status
|
||||
monitor.status = this.status;
|
||||
await monitor.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Check", CheckSchema);
|
||||
Generated
+10
-3
@@ -27,6 +27,7 @@
|
||||
"nodemailer": "^6.9.14",
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.4",
|
||||
"ssl-checker": "2.0.10",
|
||||
"winston": "^3.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -983,9 +984,9 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -5026,6 +5027,12 @@
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ssl-checker": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/ssl-checker/-/ssl-checker-2.0.10.tgz",
|
||||
"integrity": "sha512-SS6rrZocToJWHM1p6iVNb583ybB3UqT1fymCHSWuEdXDUqKA6O1D5Fb8KJVmhj3XKXE82IEWcr+idJrc4jUzFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"nodemailer": "^6.9.14",
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.4",
|
||||
"ssl-checker": "2.0.10",
|
||||
"winston": "^3.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,6 +4,8 @@ const { verifyOwnership } = require("../middleware/verifyOwnership");
|
||||
const Monitor = require("../models/Monitor");
|
||||
|
||||
router.get("/", monitorController.getAllMonitors);
|
||||
router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
|
||||
router.get("/certificate/:monitorId", monitorController.getMonitorCertificate);
|
||||
router.get("/:monitorId", monitorController.getMonitorById);
|
||||
router.get("/user/:userId", monitorController.getMonitorsByUserId);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class JobQueue {
|
||||
* @constructor
|
||||
* @throws {Error}
|
||||
*/
|
||||
constructor() {
|
||||
constructor(networkService) {
|
||||
this.queue = new Queue(QUEUE_NAME, {
|
||||
connection,
|
||||
});
|
||||
@@ -32,11 +32,11 @@ class JobQueue {
|
||||
* @returns {Promise<JobQueue>} - Returns a new JobQueue
|
||||
*
|
||||
*/
|
||||
static async createJobQueue(db) {
|
||||
static async createJobQueue(db, networkService) {
|
||||
const queue = new JobQueue();
|
||||
try {
|
||||
queue.db = db;
|
||||
queue.networkService = new NetworkService(db);
|
||||
queue.networkService = networkService;
|
||||
const monitors = await db.getAllMonitors();
|
||||
for (const monitor of monitors) {
|
||||
await queue.addJob(monitor.id, monitor);
|
||||
|
||||
@@ -3,8 +3,9 @@ const ping = require("ping");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
class NetworkService {
|
||||
constructor(db) {
|
||||
constructor(db, emailService) {
|
||||
this.db = db;
|
||||
this.emailService = emailService;
|
||||
this.TYPE_PING = "ping";
|
||||
this.TYPE_HTTP = "http";
|
||||
this.TYPE_PAGESPEED = "pagespeed";
|
||||
@@ -12,6 +13,33 @@ class NetworkService {
|
||||
this.NETWORK_ERROR = 5000;
|
||||
}
|
||||
|
||||
async handleNotification(job, isAlive) {
|
||||
const { _id } = job.data;
|
||||
const monitor = await this.db.getMonitorById(_id);
|
||||
|
||||
// If monitor status changes, update monitor status and send notification
|
||||
if (monitor.status !== isAlive) {
|
||||
monitor.status = !monitor.status;
|
||||
await monitor.save();
|
||||
|
||||
let template =
|
||||
isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
let status = isAlive === true ? "up" : "down";
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(_id);
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await this.emailService.buildAndSendEmail(
|
||||
template,
|
||||
{ monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
notification.address,
|
||||
`Monitor ${monitor.name} is ${status}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the response time of an asynchronous operation.
|
||||
* @param {Function} operation - An asynchronous operation to measure.
|
||||
@@ -42,10 +70,12 @@ class NetworkService {
|
||||
return response;
|
||||
};
|
||||
|
||||
let isAlive;
|
||||
|
||||
try {
|
||||
const { responseTime, response } =
|
||||
await this.measureResponseTime(operation);
|
||||
const isAlive = response.alive;
|
||||
isAlive = response.alive;
|
||||
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
@@ -54,12 +84,15 @@ class NetworkService {
|
||||
};
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: false,
|
||||
status: isAlive,
|
||||
responseTime: error.responseTime,
|
||||
};
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleNotification(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,13 +108,15 @@ class NetworkService {
|
||||
return response;
|
||||
};
|
||||
|
||||
let isAlive;
|
||||
|
||||
// attempt connection
|
||||
try {
|
||||
const { responseTime, response } =
|
||||
await this.measureResponseTime(operation);
|
||||
|
||||
// check if response is in the 200 range, if so, service is up
|
||||
const isAlive = response.status >= 200 && response.status < 300;
|
||||
isAlive = response.status >= 200 && response.status < 300;
|
||||
|
||||
//Create a check with relevant data
|
||||
const checkData = {
|
||||
@@ -92,9 +127,10 @@ class NetworkService {
|
||||
};
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: false,
|
||||
status: isAlive,
|
||||
responseTime: error.responseTime,
|
||||
};
|
||||
// The server returned a response
|
||||
@@ -104,6 +140,8 @@ class NetworkService {
|
||||
checkData.statusCode = this.NETWORK_ERROR;
|
||||
}
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleNotification(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +160,8 @@ class NetworkService {
|
||||
* @throws {Error} Throws an error if there is an issue with fetching or processing the PageSpeed insights.
|
||||
*/
|
||||
async handlePagespeed(job) {
|
||||
let isAlive;
|
||||
|
||||
try {
|
||||
const url = job.data.url;
|
||||
const response = await axios.get(
|
||||
@@ -145,9 +185,10 @@ class NetworkService {
|
||||
// Total Blocking Time 30%
|
||||
// Cumulative Layout Shift 25%
|
||||
|
||||
isAlive = true;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: true,
|
||||
status: isAlive,
|
||||
accessibility: (categories.accessibility?.score || 0) * 100,
|
||||
bestPractices: (categories["best-practices"]?.score || 0) * 100,
|
||||
seo: (categories.seo?.score || 0) * 100,
|
||||
@@ -163,15 +204,18 @@ class NetworkService {
|
||||
|
||||
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: false,
|
||||
status: isAlive,
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performance: 0,
|
||||
};
|
||||
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
|
||||
} finally {
|
||||
this.handleNotification(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,11 +71,13 @@ const successMessages = {
|
||||
|
||||
//Monitor Controller
|
||||
MONITOR_GET_ALL: "Got all monitors successfully",
|
||||
MONTIOR_STATS_BY_ID: "Got monitor stats by Id successfully",
|
||||
MONTIOR_GET_BY_ID: "Got monitor by Id successfully",
|
||||
MONITOR_GET_BY_USER_ID: (userId) => `Got monitor for ${userId} successfully"`,
|
||||
MONITOR_CREATE: "Monitor created successfully",
|
||||
MONITOR_DELETE: "Monitor deleted successfully",
|
||||
MONITOR_EDIT: "Monitor edited successfully",
|
||||
MONITOR_CERTIFICATE: "Got monitor certificate successfully",
|
||||
|
||||
//Job Queue
|
||||
JOB_QUEUE_DELETE_JOB: "Job removed successfully",
|
||||
|
||||
@@ -126,7 +126,7 @@ const getMonitorByIdQueryValidation = joi.object({
|
||||
status: joi.boolean(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
filter: joi.string().valid("day", "week", "month"),
|
||||
dateRange: joi.string().valid("day", "week", "month"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user