Merge branch 'develop' of https://github.com/bluewave-labs/bluewave-uptime into develop
Adding develop branch to my repo. Hadn't updated since this branch was added.
This commit is contained in:
commit
d97782ef90
19 files changed
+769
-488
No files matched your search
@@ -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,68 @@ const CheckEmail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
sessionStorage.removeItem("email");
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="check-email-page">
|
||||
<Stack className="check-email-page auth" overflow="hidden">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
/>
|
||||
<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"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
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={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" }}>
|
||||
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 +148,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 +157,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,108 @@ 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">
|
||||
<Stack className="forgot-password-page auth" overflow="hidden">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
/>
|
||||
<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"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
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={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%" maxWidth={400} 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -154,6 +154,13 @@ 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">
|
||||
@@ -209,7 +216,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>
|
||||
|
||||
@@ -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,74 @@ const NewPasswordConfirmed = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="password-confirmed-page">
|
||||
<Stack className="password-confirmed-page auth" overflow="hidden">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
/>
|
||||
<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"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
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={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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -107,18 +107,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">
|
||||
@@ -267,21 +265,19 @@ 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={theme.gap.large}>
|
||||
<Check
|
||||
text="Must be at least 8 characters long"
|
||||
variant={
|
||||
|
||||
@@ -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,138 @@ const SetNewPassword = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="set-new-password-page">
|
||||
<Stack className="set-new-password-page auth" overflow="hidden">
|
||||
<img
|
||||
className="background-pattern-svg"
|
||||
src={background}
|
||||
alt="background pattern"
|
||||
/>
|
||||
<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"
|
||||
p={theme.gap.xl}
|
||||
pb={theme.gap.triplexl}
|
||||
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={theme.gap.large} 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="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>
|
||||
</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%" }}
|
||||
/>
|
||||
</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;
|
||||
@@ -106,13 +29,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;
|
||||
}
|
||||
@@ -135,6 +58,13 @@
|
||||
.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) {
|
||||
height: var(--env-var-nav-bar-height);
|
||||
@@ -144,3 +74,10 @@
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
@@ -119,7 +118,7 @@ const DetailsPage = () => {
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`,
|
||||
`/monitors/stats/${monitorId}?filter=${dateRange}&numToDisplay=50&normalize=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
@@ -139,67 +138,6 @@ const DetailsPage = () => {
|
||||
|
||||
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 (
|
||||
@@ -243,7 +181,7 @@ const DetailsPage = () => {
|
||||
</Typography>{" "}
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
Last time checked{" "}
|
||||
{formatDurationRounded(getLastChecked(monitor?.checks))} ago.
|
||||
{formatDurationRounded(monitor?.lastChecked)} ago.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -277,16 +215,13 @@ const DetailsPage = () => {
|
||||
>
|
||||
<StatBox
|
||||
title="Currently up for"
|
||||
value={formatDuration(calculateUptimeDuration(monitor.checks))}
|
||||
value={formatDuration(monitor?.uptimeDuration)}
|
||||
/>
|
||||
<StatBox
|
||||
title="Last checked"
|
||||
value={`${formatDuration(getLastChecked(monitor.checks))} ago`}
|
||||
/>
|
||||
<StatBox
|
||||
title="Incidents"
|
||||
value={countIncidents(monitor.checks)}
|
||||
value={`${formatDuration(monitor?.lastChecked)} ago`}
|
||||
/>
|
||||
<StatBox title="Incidents" value={monitor?.incidents} />
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
@@ -331,7 +266,7 @@ const DetailsPage = () => {
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<MonitorDetailsAreaChart checks={monitor.checks} />
|
||||
<MonitorDetailsAreaChart checks={monitor.checks.reverse()} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.ml}>
|
||||
|
||||
@@ -5,7 +5,11 @@ 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 {
|
||||
formatDate,
|
||||
formatDuration,
|
||||
formatDurationRounded,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import { getLastChecked } from "../../../Utils/monitorUtils";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import Button from "../../../Components/Button";
|
||||
@@ -185,7 +189,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}`,
|
||||
@@ -369,14 +373,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 +389,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,9 +408,7 @@ const PageSpeedDetails = () => {
|
||||
</Stack>
|
||||
<Typography component="h2">Score history</Typography>
|
||||
<Box height="300px">
|
||||
<PageSpeedLineChart
|
||||
pageSpeedChecks={monitor?.checks?.slice(0, 25)}
|
||||
/>
|
||||
<PageSpeedLineChart pageSpeedChecks={monitor?.checks?.reverse()} />
|
||||
</Box>
|
||||
<Typography component="h2">Performance report</Typography>
|
||||
<Stack direction="row" alignItems="center" overflow="hidden">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,6 +33,38 @@ 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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns monitor with matching ID
|
||||
* @async
|
||||
@@ -269,6 +301,7 @@ const editMonitor = async (req, res, next) => {
|
||||
|
||||
module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
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,232 @@ 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 = (checks) => {
|
||||
return checks.map((check) => {
|
||||
return check.status;
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 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, filter, 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);
|
||||
|
||||
let model =
|
||||
monitor.type === "http" || monitor.type === "ping"
|
||||
? Check
|
||||
: PageSpeedCheck;
|
||||
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
};
|
||||
// Get all checks to calculate stats
|
||||
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 });
|
||||
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.avgResponseTime24hours =
|
||||
getAverageResponseTime24Hours(checks24Hours);
|
||||
monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
|
||||
monitorStats.uptime30Days = getUptimePercentage(checks30Days);
|
||||
monitorStats.statusBar = getStatusBarValues(checks60Mins);
|
||||
}
|
||||
|
||||
//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] };
|
||||
}
|
||||
|
||||
let dateRangeChecks = await model
|
||||
.find(checksQuery)
|
||||
.sort({
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
|
||||
// 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
|
||||
const incidents = getIncidents(dateRangeChecks);
|
||||
monitorStats.uptimeDuration = caluclteUptimeDuration(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
|
||||
@@ -263,6 +489,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) {
|
||||
|
||||
@@ -4,6 +4,7 @@ const { verifyOwnership } = require("../middleware/verifyOwnership");
|
||||
const Monitor = require("../models/Monitor");
|
||||
|
||||
router.get("/", monitorController.getAllMonitors);
|
||||
router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
|
||||
router.get("/:monitorId", monitorController.getMonitorById);
|
||||
router.get("/user/:userId", monitorController.getMonitorsByUserId);
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ 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",
|
||||
|
||||
Reference in new issue
Block a user