This commit is contained in:
Alvaro Torres committed 2024-08-14 10:28:14 -05:00
commit 0dfe44ac61
41 files changed
+732 -498

No files matched your search

+26 -3
View File
@@ -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>
);
};
+1 -1
View File
@@ -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 -1
View File
@@ -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;
-34
View File
@@ -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;
}
-203
View File
@@ -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;
+36 -3
View File
@@ -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);
}
+154 -10
View File
@@ -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>
);
}
+30 -6
View File
@@ -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);
}
}
);
+24 -45
View File
@@ -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;
}
-2
View File
@@ -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>
);
+1 -1
View File
@@ -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={{
+10 -7
View File
@@ -86,10 +86,9 @@ const CheckEmail = () => {
return (
<Stack className="check-email-page auth" overflow="hidden">
<img
<Box
className="background-pattern-svg"
src={background}
alt="background pattern"
sx={{ backgroundImage: `url(${background})` }}
/>
<Stack
direction="row"
@@ -105,8 +104,8 @@ const CheckEmail = () => {
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": {
@@ -121,11 +120,15 @@ const CheckEmail = () => {
},
}}
>
<Stack gap={theme.gap.large} alignItems="center" textAlign="center">
<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} sx={{ width: "max-content" }}>
<Typography mt={theme.gap.xs}>
We sent a password reset link to{" "}
<Typography className="email-sent-to" component="span">
{email || "username@email.com"}
+10 -7
View File
@@ -88,10 +88,9 @@ const ForgotPassword = () => {
return (
<Stack className="forgot-password-page auth" overflow="hidden">
<img
<Box
className="background-pattern-svg"
src={background}
alt="background pattern"
sx={{ backgroundImage: `url(${background})` }}
/>
<Stack
direction="row"
@@ -107,8 +106,8 @@ const ForgotPassword = () => {
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": {
@@ -123,7 +122,11 @@ const ForgotPassword = () => {
},
}}
>
<Stack gap={theme.gap.large} alignItems="center" textAlign="center">
<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>
@@ -131,7 +134,7 @@ const ForgotPassword = () => {
No worries, we'll send you reset instructions.
</Typography>
</Box>
<Box width="100%" maxWidth={400} textAlign="left">
<Box width="100%" textAlign="left">
<form noValidate spellCheck={false} onSubmit={handleSubmit}>
<Field
type="email"
+11 -8
View File
@@ -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>
@@ -163,7 +167,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">Log In</Typography>
<Typography>Enter your password</Typography>
@@ -336,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"
@@ -355,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": {
@@ -23,10 +23,9 @@ const NewPasswordConfirmed = () => {
return (
<Stack className="password-confirmed-page auth" overflow="hidden">
<img
<Box
className="background-pattern-svg"
src={background}
alt="background pattern"
sx={{ backgroundImage: `url(${background})` }}
/>
<Stack
direction="row"
@@ -42,8 +41,8 @@ const NewPasswordConfirmed = () => {
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": {
@@ -58,7 +57,11 @@ const NewPasswordConfirmed = () => {
},
}}
>
<Stack gap={theme.gap.large} alignItems="center" textAlign="center">
<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>
+36 -16
View File
@@ -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>
@@ -127,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,
@@ -171,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>
@@ -200,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,
@@ -244,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>
@@ -277,9 +279,17 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
error={errors.confirm}
/>
</form>
<Stack gap={theme.gap.small} mb={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"
@@ -291,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"
@@ -305,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"
@@ -326,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,
@@ -509,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"
@@ -528,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": {
+30 -10
View File
@@ -105,10 +105,9 @@ const SetNewPassword = () => {
return (
<Stack className="set-new-password-page auth" overflow="hidden">
<img
<Box
className="background-pattern-svg"
src={background}
alt="background pattern"
sx={{ backgroundImage: `url(${background})` }}
/>
<Stack
direction="row"
@@ -124,8 +123,8 @@ const SetNewPassword = () => {
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.large}
mx="auto"
sx={{
"& > .MuiStack-root": {
@@ -140,7 +139,11 @@ const SetNewPassword = () => {
},
}}
>
<Stack gap={theme.gap.large} textAlign="center">
<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>
@@ -175,7 +178,12 @@ const SetNewPassword = () => {
</form>
<Stack gap={theme.gap.small} mb={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"
@@ -187,7 +195,12 @@ const SetNewPassword = () => {
}
/>
<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"
@@ -201,7 +214,14 @@ const SetNewPassword = () => {
}
/>
<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"
@@ -220,7 +240,7 @@ const SetNewPassword = () => {
onClick={handleSubmit}
level="primary"
label="Reset password"
sx={{ width: "100%" }}
sx={{ width: "100%", maxWidth: 400 }}
/>
</Stack>
</Stack>
+55 -6
View File
@@ -8,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);
}
@@ -55,6 +53,8 @@
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);
}
@@ -66,18 +66,67 @@
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: var(--env-var-font-size-large-plus);
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;
}
}
+1 -1
View File
@@ -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 />
) : (
+1 -1
View File
@@ -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 {
+49 -4
View File
@@ -113,12 +113,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/stats/${monitorId}?filter=${dateRange}&numToDisplay=50&normalize=true`,
`/monitors/stats/${monitorId}?dateRange=${dateRange}&numToDisplay=50&normalize=true`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -136,10 +137,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();
let loading = Object.keys(monitor).length === 0;
return (
<Box className="monitor-details">
{loading ? (
@@ -222,6 +236,35 @@ const DetailsPage = () => {
value={`${formatDuration(monitor?.lastChecked)} ago`}
/>
<StatBox title="Incidents" value={monitor?.incidents} />
<StatBox title="Certificate Expiry" value={certificateExpiry} />
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.large}
>
<StatBox
title="Latest response time"
value={monitor?.latestResponseTime}
/>
<StatBox
title="Average Repsonse Time (24 hours)"
value={parseFloat(monitor?.avgResponseTime24hours)
.toFixed(2)
.replace(/\.?0+$/, "")}
/>
<StatBox
title="Uptime (24 hours)"
value={`${parseFloat(monitor?.uptime24Hours)
.toFixed(2)
.replace(/\.?0+$/, "")}%`}
/>
<StatBox
title="Uptime(30 days)"
value={`${parseFloat(monitor?.uptime30Days)
.toFixed(2)
.replace(/\.?0+$/, "")}%`}
/>
</Stack>
<Box>
<Stack
@@ -266,7 +309,9 @@ const DetailsPage = () => {
</ButtonGroup>
</Stack>
<Box sx={{ height: "200px" }}>
<MonitorDetailsAreaChart checks={monitor.checks.reverse()} />
<MonitorDetailsAreaChart
checks={[...monitor.checks].reverse()}
/>
</Box>
</Box>
<Stack gap={theme.gap.ml}>
+24 -13
View File
@@ -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>
+2 -4
View File
@@ -6,11 +6,9 @@ import { useTheme } from "@emotion/react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import {
formatDate,
formatDuration,
formatDurationRounded,
} from "../../../Utils/timeUtils";
import { getLastChecked } from "../../../Utils/monitorUtils";
import axiosInstance from "../../../Utils/axiosConfig";
import Button from "../../../Components/Button";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
@@ -365,7 +363,7 @@ const PageSpeedDetails = () => {
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.xl}
gap={theme.gap.large}
flexWrap="wrap"
>
<StatBox
@@ -411,7 +409,7 @@ const PageSpeedDetails = () => {
<PageSpeedLineChart pageSpeedChecks={monitor?.checks?.reverse()} />
</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"
+1 -1
View File
@@ -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 ? (
+2 -1
View File
@@ -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: {
+3
View File
@@ -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

-8
View File
@@ -155,14 +155,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;
}
+50 -18
View File
@@ -6,7 +6,6 @@
![](https://img.shields.io/github/issues-pr/bluewave-labs/bluewave-uptime)
![](https://img.shields.io/github/issues/bluewave-labs/bluewave-uptime)
<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).
![Alt](https://repobeats.axiom.co/api/embed/c35d999c82dbb31e967427ea4166c14da4172e73.svg "Repobeats analytics image")
## 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.
+37
View File
@@ -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");
@@ -65,6 +66,41 @@ const getMonitorStatsById = async (req, res, next) => {
}
};
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
@@ -302,6 +338,7 @@ const editMonitor = async (req, res, next) => {
module.exports = {
getAllMonitors,
getMonitorStatsById,
getMonitorCertificate,
getMonitorById,
getMonitorsByUserId,
createMonitor,
+31 -24
View File
@@ -149,7 +149,7 @@ const getMonitorStatsById = async (req) => {
try {
const { monitorId } = req.params;
let { status, limit, sortOrder, filter, numToDisplay, normalize } =
let { status, limit, sortOrder, dateRange, numToDisplay, normalize } =
req.query;
// This effectively removes limit, returning all checks
@@ -161,37 +161,44 @@ const getMonitorStatsById = async (req) => {
// 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(),
};
// Get all checks to calculate stats
// 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") {
// Get checks in 24 hours
checksQuery.createdAt = { $gte: filterLookup.day };
const checks24Hours = await model.find(checksQuery).sort({
createdAt: sortOrder,
});
// Get checks in 30 days
checksQuery.createdAt = { $gte: filterLookup.month };
const checks30Days = await model.find(checksQuery).sort({
createdAt: sortOrder,
});
// Get checks in 60 mins
checksQuery.createdAt = { $gte: filterLookup.hour };
const checks60Mins = await model
.find(checksQuery)
.sort({ createdAt: sortOrder });
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 =
@@ -202,14 +209,13 @@ const getMonitorStatsById = async (req) => {
}
//Get checks for dateRange
if (status !== undefined) {
checksQuery.status = status;
}
// Filter checks by "day", "week", or "month"
if (filter !== undefined) {
checksQuery.createdAt = { $gte: filterLookup[filter] };
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: filterLookup[dateRange] };
}
let dateRangeChecks = await model
@@ -219,6 +225,8 @@ const getMonitorStatsById = async (req) => {
})
.limit(limit);
const incidents = getIncidents(dateRangeChecks);
// If more than numToDisplay checks, pick every nth check
if (
numToDisplay !== undefined &&
@@ -233,10 +241,9 @@ const getMonitorStatsById = async (req) => {
if (normalize !== undefined) {
dateRangeChecks = NormalizeData(dateRangeChecks, 1, 100);
}
// Add common stats
const incidents = getIncidents(dateRangeChecks);
monitorStats.uptimeDuration = caluclteUptimeDuration(checksAll);
// 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;
+30 -34
View File
@@ -57,6 +57,7 @@ const CheckSchema = mongoose.Schema(
*
* @type {Date}
*/
expiry: {
type: Date,
default: Date.now,
@@ -84,43 +85,38 @@ CheckSchema.pre("save", async function (next) {
});
// 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`
);
}
}
}
// Only send email if monitor status has changed
if (monitor.status !== this.status) {
const emailService = new EmailService();
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();
let template = "";
let status = "";
if (monitor.status === true && this.status === false) {
template = "serverIsDownTemplate";
status = "down";
}
if (monitor.status === false && this.status === true) {
// Notify users that the monitor is up
template = "serverIsUpTemplate";
status = "up";
}
for (const notification of notifications) {
if (notification.type === "email") {
await emailService.buildAndSendEmail(
template,
{ monitorName: monitor.name, monitorUrl: monitor.url },
notification.address,
`Monitor ${monitor.name} is ${status}`
);
}
}
// Update monitor status
monitor.status = this.status;
await monitor.save();
}
}
} catch (error) {
+10 -4
View File
@@ -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": {
@@ -996,10 +997,9 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.1.tgz",
"integrity": "sha512-78pWJsQTceInlyaeBQeYZ/QgZeWS8hGeKiIJiDKQe3hEyBb7sEMq0K4gjx+Va6WHTYO4zI/RRl8qGRzn0YMadA==",
"license": "MIT",
"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.0",
"form-data": "^4.0.0",
@@ -5046,6 +5046,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",
+1
View File
@@ -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": {
+1
View File
@@ -5,6 +5,7 @@ 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);
+1
View File
@@ -77,6 +77,7 @@ const successMessages = {
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",
+1 -1
View File
@@ -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(),
});