diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..61471d77b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - Browser [e.g. Chrome, Safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..bbcbbe7d6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/Client/index.html b/Client/index.html
index 2b6c6b546..3407fb5cd 100644
--- a/Client/index.html
+++ b/Client/index.html
@@ -1,16 +1,14 @@
-
+
+
+
+
+
+ BlueWave Uptime
+
-
-
-
-
- BlueWave Uptime
-
-
-
-
-
-
-
+
+
+
+
diff --git a/Client/public/bluewave_favicon.ico b/Client/public/bluewave_favicon.ico
new file mode 100644
index 000000000..487946975
Binary files /dev/null and b/Client/public/bluewave_favicon.ico differ
diff --git a/Client/public/vite.svg b/Client/public/vite.svg
deleted file mode 100644
index e7b8dfb1b..000000000
--- a/Client/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/Client/src/App.jsx b/Client/src/App.jsx
index cad8a4ebd..12db9dab9 100644
--- a/Client/src/App.jsx
+++ b/Client/src/App.jsx
@@ -26,6 +26,7 @@ import PageSpeed from "./Pages/PageSpeed";
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow";
import PageSpeedDetails from "./Pages/PageSpeed/Details";
+import PageSpeedConfigure from "./Pages/PageSpeed/Configure";
function App() {
const AdminCheckedRegister = withAdminCheck(Register);
@@ -103,6 +104,10 @@ function App() {
path="pagespeed/:monitorId"
element={ }
/>
+ }
+ />
} />
diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
index 567de0b68..91657f542 100644
--- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
+++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
@@ -79,7 +79,6 @@ const MonitorDetailsAreaChart = ({ checks }) => {
MonitorDetailsAreaChart.propTypes = {
checks: PropTypes.array,
- filter: PropTypes.string,
};
CustomToolTip.propTypes = {
diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx
index 6fb8ea612..0b2fe56fe 100644
--- a/Client/src/Components/Inputs/Field/index.jsx
+++ b/Client/src/Components/Inputs/Field/index.jsx
@@ -12,7 +12,7 @@ import Visibility from "@mui/icons-material/Visibility";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import Button from "../../Button";
import "./index.css";
-import { useState } from "react";
+import { forwardRef, useState } from "react";
/**
* @param {Object} props
@@ -30,140 +30,151 @@ import { useState } from "react";
* @param {function} props.onChange - Function called on input change.
* @param {string} [props.error] - Error message to display for the input field.
* @param {boolean} [props.disabled] - Indicates if the input field is disabled.
+ * @param {React.Ref} [ref] - Ref forwarded to the underlying `TextField` component. Allows for direct interactions such as focusing.
*/
-const Field = ({
- type = "text",
- id,
- label,
- https,
- isRequired,
- isOptional,
- optionalLabel,
- hasCopy,
- autoComplete,
- placeholder,
- value,
- onChange,
- error,
- disabled,
-}) => {
- const theme = useTheme();
+const Field = forwardRef(
+ (
+ {
+ type = "text",
+ id,
+ label,
+ https,
+ isRequired,
+ isOptional,
+ optionalLabel,
+ hasCopy,
+ autoComplete,
+ placeholder,
+ value,
+ onChange,
+ error,
+ disabled,
+ },
+ ref
+ ) => {
+ const theme = useTheme();
- // TODO - are we using this feature anywhere ?
- const [copy, setCopy] = useState(false);
- const handleCopy = () => {
- setCopy(true);
- setTimeout(() => setCopy(false), 1000);
- };
+ // TODO - are we using this feature anywhere ?
+ const [copy, setCopy] = useState(false);
+ const handleCopy = () => {
+ setCopy(true);
+ setTimeout(() => setCopy(false), 1000);
+ };
- const [isVisible, setVisible] = useState(false);
+ const [isVisible, setVisible] = useState(false);
- return (
-
- {label && (
-
- {label}
- {isRequired ? * : ""}
- {isOptional ? (
-
- {optionalLabel || "(optional)"}
-
- ) : (
- ""
- )}
-
- )}
-
-
- {https ? "https" : "http"}://
-
-
- ),
- endAdornment:
- type === "password" ? (
-
- setVisible((show) => !show)}
- tabIndex={-1}
- sx={{
- color: theme.palette.section.borderColor,
- padding: `calc(${theme.gap.xs} / 2)`,
- "&:focus": {
- outline: "none",
- },
- "& .MuiTouchRipple-root": {
- pointerEvents: "none",
- display: "none",
- },
- }}
- >
- {!isVisible ? : }
-
-
+ return (
+
+ {label && (
+
+ {label}
+ {isRequired ? * : ""}
+ {isOptional ? (
+
+ {optionalLabel || "(optional)"}
+
) : (
- hasCopy && (
-
- }
- onClick={handleCopy}
+ ""
+ )}
+
+ )}
+
+
+ {https ? "https" : "http"}://
+
+
+ ),
+ endAdornment:
+ type === "password" ? (
+
+ setVisible((show) => !show)}
+ tabIndex={-1}
sx={{
- borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
- lineHeight: 0,
+ color: theme.palette.section.borderColor,
+ padding: `calc(${theme.gap.xs} / 2)`,
+ "&:focus": {
+ outline: "none",
+ },
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
- />
+ >
+ {!isVisible ? : }
+
- )
- ),
- }}
- />
- {error && (
-
- {error}
-
- )}
-
- );
-};
+ ) : (
+ hasCopy && (
+
+ }
+ onClick={handleCopy}
+ sx={{
+ borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
+ lineHeight: 0,
+ "& .MuiTouchRipple-root": {
+ pointerEvents: "none",
+ display: "none",
+ },
+ }}
+ />
+
+ )
+ ),
+ }}
+ />
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+ }
+);
Field.propTypes = {
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
index 05f3d8412..355b09ca6 100644
--- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
+++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
@@ -352,7 +352,7 @@ const ProfilePanel = () => {
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
- width: 400,
+ width: 500,
bgcolor: "white",
border: "solid 1px #f2f2f2",
borderRadius: `${theme.shape.borderRadius}px`,
diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
index 6524a3417..4377edb35 100644
--- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
+++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
@@ -68,6 +68,58 @@ export const getPageSpeedByUserId = createAsyncThunk(
}
);
+export const updatePageSpeed = createAsyncThunk(
+ "pageSpeedMonitors/updatePageSpeed",
+ async (data, thunkApi) => {
+ try {
+ const { authToken, monitor } = data;
+ const updatedFields = {
+ name: monitor.name,
+ description: monitor.description,
+ interval: monitor.interval,
+ // notifications: monitor.notifications,
+ };
+ const res = await axiosInstance.put(
+ `/monitors/${monitor._id}`,
+ updatedFields,
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data) {
+ return thunkApi.rejectWithValue(error.response.data);
+ }
+ return thunkApi.rejectWithValue(error.message);
+ }
+ }
+);
+
+export const deletePageSpeed = createAsyncThunk(
+ "pageSpeedMonitors/deletePageSpeed",
+ async (data, thunkApi) => {
+ try {
+ const { authToken, monitor } = data;
+ const res = await axiosInstance.delete(`/monitors/${monitor._id}`, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data) {
+ return thunkApi.rejectWithValue(error.response.data);
+ }
+ return thunkApi.rejectWithValue(error.message);
+ }
+ }
+);
+
const pageSpeedMonitorSlice = createSlice({
name: "pageSpeedMonitor",
initialState,
@@ -137,6 +189,44 @@ const pageSpeedMonitorSlice = createSlice({
state.msg = action.payload
? action.payload.msg
: "Failed to create page speed monitor";
+ })
+
+ // *****************************************************
+ // Create Monitor
+ // *****************************************************
+ .addCase(updatePageSpeed.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(updatePageSpeed.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.success = action.payload.success;
+ state.msg = action.payload.msg;
+ })
+ .addCase(updatePageSpeed.rejected, (state, action) => {
+ state.isLoading = false;
+ state.success = false;
+ state.msg = action.payload
+ ? action.payload.msg
+ : "Failed to update page speed monitor";
+ })
+
+ // *****************************************************
+ // Delete Monitor
+ // *****************************************************
+ .addCase(deletePageSpeed.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(deletePageSpeed.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.success = action.payload.success;
+ state.msg = action.payload.msg;
+ })
+ .addCase(deletePageSpeed.rejected, (state, action) => {
+ state.isLoading = false;
+ state.success = false;
+ state.msg = action.payload
+ ? action.payload.msg
+ : "Failed to delete page speed monitor";
});
},
});
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index 4c2acd3ec..e69e6d41d 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -80,7 +80,7 @@ export const updateUptimeMonitor = createAsyncThunk(
notifications: monitor.notifications,
};
const res = await axiosInstance.put(
- `/monitors/edit/${monitor._id}`,
+ `/monitors/${monitor._id}`,
updatedFields,
{
headers: {
diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css
index d30ed9462..048700db6 100644
--- a/Client/src/Layouts/HomeLayout/index.css
+++ b/Client/src/Layouts/HomeLayout/index.css
@@ -1,6 +1,3 @@
-.home-layout * {
- box-sizing: border-box;
-}
.home-layout {
display: grid;
position: relative;
@@ -10,7 +7,7 @@
grid-template-columns: var(--env-var-side-bar-width) auto;
grid-template-rows: var(--env-var-nav-bar-height) auto;
background-color: var(--env-var-color-30);
- height: 100vh;
+ min-height: 100vh;
}
.home-layout aside {
diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx
index d40cb078b..2868b5d75 100644
--- a/Client/src/Pages/Auth/Login.jsx
+++ b/Client/src/Pages/Auth/Login.jsx
@@ -1,18 +1,221 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
-
-import "./index.css";
-import Logomark from "../../assets/Images/bwl-logo-2.svg?react";
-import Button from "../../Components/Button";
-import background from "../../assets/Images/background_pattern_decorative.png";
-import axiosInstance from "../../Utils/axiosConfig";
+import { Box, Stack, Typography } from "@mui/material";
+import { useTheme } from "@emotion/react";
import { credentials } from "../../Validation/validation";
import { login } from "../../Features/Auth/authSlice";
import { useDispatch } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
+import Button from "../../Components/Button";
+import axiosInstance from "../../Utils/axiosConfig";
import Field from "../../Components/Inputs/Field";
-import { Checkbox, FormControlLabel, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
+import background from "../../assets/Images/background_pattern_decorative.png";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import Mail from "../../assets/icons/mail.svg?react";
+import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+
+import "./index.css";
+
+/**
+ * Displays the initial landing page.
+ *
+ * @param {Object} props
+ * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
+ * @returns {JSX.Element}
+ */
+const LandingPage = ({ onContinue }) => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+ Log In
+ We are pleased to see you again!
+
+
+ }
+ onClick={onContinue}
+ sx={{
+ width: "100%",
+ "& svg": {
+ mr: theme.gap.small,
+ },
+ }}
+ />
+
+
+
+ By continuing, you agree to our{" "}
+ Terms of Service and{" "}
+ Privacy Policy.
+
+
+
+ >
+ );
+};
+
+/**
+ * Renders the first step of the login process.
+ *
+ * @param {Object} props
+ * @param {Object} props.form - Form state object.
+ * @param {Object} props.errors - Object containing form validation errors.
+ * @param {Function} props.onSubmit - Callback function to handle form submission.
+ * @param {Function} props.onChange - Callback function to handle form input changes.
+ * @param {Function} props.onBack - Callback function to handle "Back" button click.
+ * @returns {JSX.Element}
+ */
+const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
+ const theme = useTheme();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ Log In
+ Enter your email address
+
+
+
+
+
+ }
+ onClick={onBack}
+ sx={{
+ mb: theme.gap.medium,
+ px: theme.gap.ml,
+ "& svg.MuiSvgIcon-root": {
+ mr: theme.gap.xs,
+ },
+ }}
+ />
+
+
+
+ >
+ );
+};
+
+/**
+ * Renders the second step of the login process, including a password input field.
+ *
+ * @param {Object} props
+ * @param {Object} props.form - Form state object.
+ * @param {Object} props.errors - Object containing form validation errors.
+ * @param {Function} props.onSubmit - Callback function to handle form submission.
+ * @param {Function} props.onChange - Callback function to handle form input changes.
+ * @param {Function} props.onBack - Callback function to handle "Back" button click.
+ * @returns {JSX.Element}
+ */
+const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ Log In
+ Enter your password
+
+
+
+
+
+ }
+ onClick={onBack}
+ sx={{
+ mb: theme.gap.medium,
+ px: theme.gap.ml,
+ "& svg.MuiSvgIcon-root": {
+ mr: theme.gap.xs,
+ },
+ }}
+ />
+
+
+
+
+ Forgot password?
+
+ navigate("/forgot-password")}
+ >
+ Reset password
+
+
+
+ >
+ );
+};
const Login = () => {
const dispatch = useDispatch();
@@ -29,6 +232,7 @@ const Login = () => {
password: "",
});
const [errors, setErrors] = useState({});
+ const [step, setStep] = useState(0);
useEffect(() => {
axiosInstance
@@ -43,47 +247,6 @@ const Login = () => {
});
}, [navigate]);
- const handleSubmit = async (e) => {
- e.preventDefault();
-
- const { error } = credentials.validate(form, { abortEarly: false });
-
- if (error) {
- // validation errors
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
- createToast({
- body:
- error.details && error.details.length > 0
- ? error.details[0].message
- : "Error validating data.",
- });
- } else {
- const action = await dispatch(login(form));
- if (action.payload.success) {
- navigate("/monitors");
- createToast({
- body: "Welcome back! You're successfully logged in.",
- });
- } else {
- if (action.payload) {
- // dispatch errors
- createToast({
- body: action.payload.msg,
- });
- } else {
- // unknown errors
- createToast({
- body: "Unknown error.",
- });
- }
- }
- }
- };
-
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
@@ -105,91 +268,136 @@ const Login = () => {
});
};
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+
+ if (step === 1) {
+ const { error } = credentials.validate(
+ { email: form.email },
+ { abortEarly: false }
+ );
+ if (error) {
+ setErrors((prev) => ({ ...prev, email: error.details[0].message }));
+ createToast({ body: error.details[0].message });
+ } else {
+ setStep(2);
+ }
+ } else if (step === 2) {
+ const { error } = credentials.validate(form, { abortEarly: false });
+
+ if (error) {
+ // validation errors
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ setErrors(newErrors);
+ createToast({
+ body:
+ error.details && error.details.length > 0
+ ? error.details[0].message
+ : "Error validating data.",
+ });
+ } else {
+ const action = await dispatch(login(form));
+ if (action.payload.success) {
+ navigate("/monitors");
+ createToast({
+ body: "Welcome back! You're successfully logged in.",
+ });
+ } else {
+ setErrors({
+ password: "The password you provided does not match our records",
+ });
+ if (action.payload) {
+ // dispatch errors
+ createToast({
+ body: action.payload.msg,
+ });
+ } else {
+ // unknown errors
+ createToast({
+ body: "Unknown error.",
+ });
+ }
+ }
+ }
+ }
+ };
+
return (
-
+
-
-
+ )
+ )}
+
+
+ Don't have an account? —
+ {
+ navigate("/register");
+ }}
+ sx={{ userSelect: "none" }}
+ >
+ Sign Up
+
+
+
);
};
diff --git a/Client/src/Pages/Auth/index.css b/Client/src/Pages/Auth/index.css
index 4f2767f54..f762c1a24 100644
--- a/Client/src/Pages/Auth/index.css
+++ b/Client/src/Pages/Auth/index.css
@@ -2,7 +2,6 @@
/* SHARED */
/* ////// */
.register-page,
-.login-page,
.set-new-password-page,
.password-confirmed-page,
.forgot-password-page,
@@ -12,7 +11,6 @@
height: var(--env-var-height-1);
}
.register-page h1.MuiTypography-root,
-.login-page h1.MuiTypography-root,
.set-new-password-page h1.MuiTypography-root,
.password-confirmed-page h1.MuiTypography-root,
.forgot-password-page h1.MuiTypography-root,
@@ -24,8 +22,6 @@
.register-page p.MuiTypography-root,
.register-page span,
.register-page button,
-.login-page p.MuiTypography-root,
-.login-page span,
.login-page button,
.set-new-password-page p.MuiTypography-root,
.set-new-password-page button,
@@ -38,13 +34,11 @@
.check-email-page span.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
-.register-page p.MuiTypography-root,
-.login-page p.MuiTypography-root {
+.register-page p.MuiTypography-root {
color: var(--env-var-color-2);
opacity: 0.8;
}
.register-page button:not(.MuiIconButton-root),
-.login-page button:not(.MuiIconButton-root),
.set-new-password-page button:not(.MuiIconButton-root),
.password-confirmed-page button:not(.MuiIconButton-root),
.forgot-password-page button:not(.MuiIconButton-root),
@@ -54,12 +48,10 @@
line-height: 0;
}
-.register-page svg rect,
-.login-page svg rect {
+.register-page svg rect {
fill: none;
}
-.register-form,
-.login-form {
+.register-form {
margin-top: 95px;
}
@@ -77,7 +69,6 @@
color: var(--env-var-color-2);
}
-.login-page .MuiFormControlLabel-root .MuiButtonBase-root,
.set-new-password-page button:not(.MuiIconButton-root) svg,
.password-confirmed-page button:not(.MuiIconButton-root) svg,
.forgot-password-page button svg,
@@ -108,19 +99,6 @@
cursor: pointer;
font-weight: 600;
}
-.login-page .MuiFormControlLabel-root {
- margin: 0;
-}
-.login-page .MuiFormControlLabel-root span:not(.Mui-checked) svg {
- fill: var(--env-var-color-4);
-}
-.login-page .MuiFormControlLabel-root svg {
- margin-bottom: 1px;
-}
-.login-page .MuiFormControlLabel-root .MuiTypography-root {
- color: var(--env-var-color-5);
- user-select: none;
-}
/* /////////// */
/* Check Email */
@@ -128,3 +106,70 @@
.check-email-page span.MuiTypography-root {
font-weight: 600;
}
+
+/* */
+.login-page {
+ height: 100vh;
+}
+.login-page h1 {
+ font-size: 37px;
+ font-weight: 600;
+ color: var(--env-var-color-3);
+}
+.login-page button:not(.MuiIconButton-root),
+.login-page .field h3.MuiTypography-root,
+.login-page .field input,
+.login-page .field .input-error,
+.login-page p,
+.login-page span {
+ font-size: var(--env-var-font-size-medium-plus);
+}
+.login-page p {
+ color: var(--env-var-color-2-light);
+}
+.login-page .field h3.MuiTypography-root,
+.login-page .field input {
+ color: var(--env-var-color-5);
+}
+
+.login-page p + span {
+ color: var(--env-var-color-3);
+ opacity: 0.8;
+ cursor: pointer;
+ transition: opacity 300ms ease-in;
+}
+.login-page p > span {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ cursor: pointer;
+ transition: all 200ms;
+}
+.login-page p > span:hover {
+ color: var(--env-var-color-2);
+ text-underline-offset: 4px;
+}
+.login-page p + span:hover {
+ opacity: 1;
+}
+.login-page button:not(.MuiIconButton-root) {
+ user-select: none;
+ border-radius: var(--env-var-radius-2);
+ line-height: 1;
+}
+.login-page button:not(.MuiIconButton-root),
+.login-page .field .MuiInputBase-root:has(input) {
+ height: 38px;
+}
+.login-page .field svg {
+ width: 24px;
+ height: 24px;
+}
+
+.login-page > .MuiStack-root:nth-of-type(1) {
+ height: var(--env-var-nav-bar-height);
+}
+.login-page .background-pattern-svg {
+ top: 14%;
+ width: 100%;
+ max-width: 800px;
+}
diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx
new file mode 100644
index 000000000..d16c23733
--- /dev/null
+++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx
@@ -0,0 +1,142 @@
+import PropTypes from "prop-types";
+import {
+ TableContainer,
+ Table,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableBody,
+ Pagination,
+ PaginationItem,
+ Paper,
+} from "@mui/material";
+
+import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
+
+import { useState, useEffect } from "react";
+import { useSelector } from "react-redux";
+import axiosInstance from "../../../Utils/axiosConfig";
+import { StatusLabel } from "../../../Components/Label";
+
+const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
+ const { authToken, user } = useSelector((state) => state.auth);
+ const [checks, setChecks] = useState([]);
+ const [checksCount, setChecksCount] = useState(0);
+ const [paginationController, setPaginationController] = useState({
+ page: 0,
+ rowsPerPage: 12,
+ });
+
+ useEffect(() => {
+ const fetchPage = async () => {
+ try {
+ let url = `/checks/${selectedMonitor}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
+
+ if (selectedMonitor === "0") {
+ url = `/checks/user/${user._id}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
+ }
+
+ const res = await axiosInstance.get(url, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+ setChecks(res.data.data.checks);
+ setChecksCount(res.data.data.checksCount);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+ fetchPage();
+ }, [
+ authToken,
+ user,
+ selectedMonitor,
+ filter,
+ paginationController.page,
+ paginationController.rowsPerPage,
+ ]);
+
+ const handlePageChange = (_, newPage) => {
+ console.log(newPage);
+ setPaginationController({
+ ...paginationController,
+ page: newPage - 1, // 0-indexed
+ });
+ };
+
+ let paginationComponent = <>>;
+ if (checksCount > paginationController.rowsPerPage) {
+ paginationComponent = (
+ (
+
+ )}
+ />
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+ Monitor Name
+ Status
+ Date & Time
+ Message
+
+
+
+ {checks.map((check) => {
+ const status = check.status === true ? "up" : "down";
+
+ return (
+
+ {monitors[check.monitorId]?.name}
+
+
+
+
+ {new Date(check.createdAt).toLocaleString()}
+
+ {check.statusCode}
+
+ );
+ })}
+
+
+
+ {paginationComponent}
+ >
+ );
+};
+
+IncidentTable.propTypes = {
+ monitors: PropTypes.object.isRequired,
+ selectedMonitor: PropTypes.string.isRequired,
+ filter: PropTypes.string.isRequired,
+};
+
+export default IncidentTable;
diff --git a/Client/src/Pages/Incidents/index.css b/Client/src/Pages/Incidents/index.css
index 1b1d2f364..1f176d470 100644
--- a/Client/src/Pages/Incidents/index.css
+++ b/Client/src/Pages/Incidents/index.css
@@ -14,6 +14,9 @@
height: 34px;
border-color: var(--env-var-color-16);
}
+.incidents .MuiPagination-root {
+ background-color: var(--env-var-color-8);
+}
/* remove later */
.incidents .MuiSelect-select {
diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx
index 4c328a364..515f8ec0c 100644
--- a/Client/src/Pages/Incidents/index.jsx
+++ b/Client/src/Pages/Incidents/index.jsx
@@ -1,15 +1,11 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
-import { Typography, ButtonGroup, Stack, Box, Skeleton } from "@mui/material";
-import WestRoundedIcon from "@mui/icons-material/WestRounded";
+import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material";
import Button from "../../Components/Button";
import axiosInstance from "../../Utils/axiosConfig";
-import BasicTable from "../../Components/BasicTable";
-import { StatusLabel } from "../../Components/Label";
import { useTheme } from "@emotion/react";
-import { useNavigate } from "react-router";
import Select from "../../Components/Inputs/Select";
-
+import IncidentTable from "./IncidentTable";
import "./index.css";
/**
@@ -40,8 +36,8 @@ const SkeletonLayout = () => {
const Incidents = () => {
const theme = useTheme();
- const navigate = useNavigate();
const authState = useSelector((state) => state.auth);
+
const [monitors, setMonitors] = useState({});
const [selectedMonitor, setSelectedMonitor] = useState("0");
const [loading, setLoading] = useState(false);
@@ -49,21 +45,11 @@ const Incidents = () => {
// TODO do something with these filters
const [filter, setFilter] = useState("all");
- let data = {
- cols: [
- { id: 1, name: "Status" },
- { id: 2, name: "Timestamp" },
- { id: 3, name: "Monitor" },
- { id: 4, name: "Message" },
- ],
- rows: [],
- };
-
useEffect(() => {
- const fetchIncidents = async () => {
+ const fetchMonitors = async () => {
setLoading(true);
const res = await axiosInstance.get(
- `/monitors/user/${authState.user._id}?status=false`,
+ `/monitors/user/${authState.user._id}?status=false&limit=1`,
{
headers: {
Authorization: `Bearer ${authState.authToken}`,
@@ -82,141 +68,70 @@ const Incidents = () => {
setLoading(false);
};
- fetchIncidents();
+ fetchMonitors();
}, [authState]);
+ useEffect(() => {}, []);
+
const handleSelect = (event) => {
setSelectedMonitor(event.target.value);
};
- let incidents = [];
- let hasAnyIncidents = false;
- let hasSpecificIncidents = false;
- if (selectedMonitor === "0") {
- Object.values(monitors).forEach((monitor) => {
- incidents = incidents.concat(monitor.checks);
- });
- if (incidents.length !== 0) hasAnyIncidents = true;
- hasSpecificIncidents = hasAnyIncidents;
- } else {
- const monitor = monitors[selectedMonitor];
- incidents = monitor.checks;
- if (incidents.length !== 0) hasSpecificIncidents = true;
- hasAnyIncidents = true;
- }
- incidents = incidents
- .sort((a, b) => {
- return new Date(b.createdAt) - new Date(a.createdAt);
- })
- .filter((incident) => {
- if (filter === "all") return true;
- else if (filter === "resolve") return incident.statusCode === 5000;
- else if (filter === "down") return incident.status === false;
- })
- .map((incident, idx) => {
- return {
- id: idx,
- data: [
- {
- id: idx + 0,
- data: ,
- },
- { id: idx + 1, data: new Date(incident.createdAt).toLocaleString() },
- { id: idx + 2, data: monitors[incident.monitorId].name },
- { id: idx + 3, data: incident.statusCode },
- ],
- };
- });
- data.rows = incidents;
return (
- <>
-
- {loading ? (
-
- ) : hasAnyIncidents ? (
- <>
-
- Incident history for:
-
- {hasAnyIncidents && hasSpecificIncidents && (
-
- setFilter("all")}
- sx={{
- backgroundColor:
- filter === "all" && theme.palette.otherColors.fillGray,
- }}
- />
- setFilter("down")}
- sx={{
- backgroundColor:
- filter === "down" && theme.palette.otherColors.fillGray,
- }}
- />
- setFilter("resolve")}
- sx={{
- backgroundColor:
- filter === "resolve" &&
- theme.palette.otherColors.fillGray,
- }}
- />
-
- )}
-
- {hasAnyIncidents && hasSpecificIncidents ? (
-
- ) : (
-
- The monitor you have selected, has no recorded incidents yet.
-
- )}
- >
- ) : (
-
- }
- onClick={() => navigate(-1)}
- sx={{
- backgroundColor: theme.palette.otherColors.fillGray,
- px: theme.gap.ml,
- mb: theme.gap.ml,
- "& svg.MuiSvgIcon-root": {
- mr: theme.gap.small,
- fill: theme.palette.otherColors.slateGray,
- },
- }}
+
+ {loading ? (
+
+ ) : (
+ <>
+
+
+ Incidents for
+
+
-
- No incidents, yet.
-
-
- )}
-
- >
+
+ setFilter("all")}
+ sx={{
+ backgroundColor:
+ filter === "all" && theme.palette.otherColors.fillGray,
+ }}
+ />
+ setFilter("down")}
+ sx={{
+ backgroundColor:
+ filter === "down" && theme.palette.otherColors.fillGray,
+ }}
+ />
+ setFilter("resolve")}
+ sx={{
+ backgroundColor:
+ filter === "resolve" && theme.palette.otherColors.fillGray,
+ }}
+ />
+
+
+
+ >
+ )}
+
);
};
diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css
index a799bf0af..e0311d4e7 100644
--- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css
+++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.css
@@ -1,5 +1,7 @@
-.maintenance-options > .MuiStack-root > .MuiStack-root.select-wrapper {
- width: 380px;
+.maintenance-options .MuiStack-root .MuiStack-root.select-wrapper,
+.maintenance-options .MuiStack-root .field {
+ width: 60%;
+ max-width: 380px;
}
.date-picker .MuiOutlinedInput-root,
@@ -16,6 +18,54 @@
width: 90px;
}
-.MuiInputAdornment-outlined .MuiIconButton-root svg {
+.maintenance-options .MuiInputAdornment-outlined .MuiIconButton-root svg {
width: 20px;
}
+
+.duration-config .field-text .MuiTextField-root,
+.duration-config .field {
+ width: 70px;
+ min-width: 70px;
+ max-width: 70px;
+}
+
+.duration-config > .MuiStack-root.select-wrapper {
+ width: 100px;
+}
+
+.duration-config
+ > .MuiStack-root.select-wrapper
+ > .MuiInputBase-root:has(> .MuiSelect-select) {
+ width: 100px;
+ min-width: 100px;
+}
+
+.duration-config > .select-wrapper .select-component > .MuiSelect-select {
+ text-transform: none;
+}
+
+.maintenance-options .add-monitors-fields .field.field-text {
+ width: 100%;
+}
+
+.MuiPopover-paper.MuiMenu-paper.select-dropdown
+ .MuiMenu-list
+ .MuiButtonBase-root {
+ text-transform: none;
+}
+
+.date-picker
+ .MuiOutlinedInput-root
+ .MuiInputAdornment-outlined
+ .MuiIconButton-root {
+ outline: none;
+
+ &:hover {
+ background-color: transparent;
+ }
+}
+
+.create-maintenance-window .MuiStack-root .MuiStack-root > button {
+ width: 110px;
+ height: 34px;
+}
diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx
index aea012e2c..187d9c63d 100644
--- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx
+++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx
@@ -9,6 +9,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import dayjs from "dayjs";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
+import Field from "../../../Components/Inputs/Field";
const directory = {
title: "Create a maintenance window",
@@ -45,9 +46,21 @@ const configOptionTitle = (title, description) => {
);
};
+const durationOptions = [
+ { _id: "minutes", name: "minutes" },
+ { _id: "hours", name: "hours" },
+ { _id: "days", name: "days" },
+];
+
const CreateNewMaintenanceWindow = () => {
const [values, setValues] = useState({
repeat: 1,
+ date: dayjs(),
+ startTime: dayjs(),
+ duration: "60",
+ unit: "minutes",
+ friendlyName: "",
+ AddMonitors: "",
});
const handleChange = (event, name) => {
@@ -77,22 +90,90 @@ const CreateNewMaintenanceWindow = () => {
className="date-localization-provider"
dateAdapter={AdapterDayjs}
>
-
+
+ handleChange({ target: { value: date } }, "date")
+ }
+ />
),
},
{
title: "Start time",
- description: "Your pings won’t be sent in this time frame.",
component: (
-
+
+ handleChange({ target: { value: time } }, "startTime")
+ }
+ />
),
},
+ {
+ title: "Duration",
+ component: (
+
+ handleChange(e, "duration")}
+ value={values.duration}
+ />
+ handleChange(e, "unit")}
+ id="duration-unit"
+ items={durationOptions}
+ value={values.unit}
+ />
+
+ ),
+ },
+ {
+ title: "Friendly name",
+ component: (
+ handleChange(e, "friendlyName")}
+ />
+ ),
+ },
+ {
+ title: "Add monitors",
+ component: (
+
+ handleChange(e, "AddMonitors")}
+ />
+
+ Add all monitors to this maintenance window
+
+
+ ),
+ },
];
return (
@@ -127,22 +208,46 @@ const CreateNewMaintenanceWindow = () => {
{configOptions.map((item, index) => (
- {configOptionTitle(item.title, item.description)}
+ {item.title && configOptionTitle(item.title, item.description)}
{item.component && item.component}
))}
+
+
+
+
);
diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx
index 0df88078f..08c8cbe39 100644
--- a/Client/src/Pages/Monitors/Configure/index.jsx
+++ b/Client/src/Pages/Monitors/Configure/index.jsx
@@ -460,10 +460,10 @@ const Configure = () => {
}}
>
- Really delete this monitor?
+ Do you really want to delete this monitor?
- Once deleted, this monitor cannot be retrieved back.
+ Once deleted, this monitor cannot be retrieved.
{
+const PaginationTable = ({ monitorId, dateRange }) => {
const { authToken } = useSelector((state) => state.auth);
const [checks, setChecks] = useState([]);
const [checksCount, setChecksCount] = useState(0);
@@ -28,7 +31,7 @@ const PaginationTable = ({ monitorId, filter }) => {
const fetchPage = async () => {
try {
const res = await axiosInstance.get(
- `/checks/${monitorId}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
+ `/checks/${monitorId}?sortOrder=desc&dateRange=${dateRange}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -45,7 +48,7 @@ const PaginationTable = ({ monitorId, filter }) => {
}, [
authToken,
monitorId,
- filter,
+ dateRange,
paginationController.page,
paginationController.rowsPerPage,
]);
@@ -53,69 +56,82 @@ const PaginationTable = ({ monitorId, filter }) => {
const handlePageChange = (_, newPage) => {
setPaginationController({
...paginationController,
- page: newPage,
+ page: newPage - 1, // 0-indexed
});
};
- const handleChangeRowsPerPage = (event) => {
- setPaginationController({
- ...paginationController,
- rowsPerPage: parseInt(event.target.value, 10),
- page: 0,
- });
- };
+ let paginationComponent = <>>;
+ if (checksCount > paginationController.rowsPerPage) {
+ paginationComponent = (
+ (
+
+ )}
+ />
+ );
+ }
return (
-
-
-
-
- Status
- Date & Time
- Message
-
-
-
- {checks.map((check) => {
- const status = check.status === true ? "up" : "down";
+ <>
+
+
+
+
+ Status
+ Date & Time
+ Message
+
+
+
+ {checks.map((check) => {
+ const status = check.status === true ? "up" : "down";
- return (
-
-
-
-
-
- {new Date(check.createdAt).toLocaleString()}
-
- {check.statusCode}
-
- );
- })}
-
-
-
-
-
-
-
-
+ return (
+
+
+
+
+
+ {new Date(check.createdAt).toLocaleString()}
+
+ {check.statusCode}
+
+ );
+ })}
+
+
+
+ {paginationComponent}
+ >
);
};
PaginationTable.propTypes = {
monitorId: PropTypes.string.isRequired,
- filter: PropTypes.string.isRequired,
+ dateRange: PropTypes.string.isRequired,
};
export default PaginationTable;
diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx
index 55dfd9335..4168880d5 100644
--- a/Client/src/Pages/Monitors/Details/index.jsx
+++ b/Client/src/Pages/Monitors/Details/index.jsx
@@ -113,13 +113,13 @@ const DetailsPage = () => {
const [monitor, setMonitor] = useState({});
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
- const [filter, setFilter] = useState("day");
+ const [dateRange, setDateRange] = useState("day");
const navigate = useNavigate();
const fetchMonitor = useCallback(async () => {
try {
const res = await axiosInstance.get(
- `/monitors/${monitorId}?sortOrder=asc&filter=${filter}&numToDisplay=50&normalize=true`,
+ `/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -131,7 +131,7 @@ const DetailsPage = () => {
console.error("Error fetching monitor of id: " + monitorId);
navigate("/not-found");
}
- }, [authToken, monitorId, navigate, filter]);
+ }, [authToken, monitorId, navigate, dateRange]);
useEffect(() => {
fetchMonitor();
@@ -301,44 +301,43 @@ const DetailsPage = () => {
setFilter("day")}
+ onClick={() => setDateRange("day")}
sx={{
backgroundColor:
- filter === "day" && theme.palette.otherColors.fillGray,
+ dateRange === "day" &&
+ theme.palette.otherColors.fillGray,
}}
/>
setFilter("week")}
+ onClick={() => setDateRange("week")}
sx={{
backgroundColor:
- filter === "week" && theme.palette.otherColors.fillGray,
+ dateRange === "week" &&
+ theme.palette.otherColors.fillGray,
}}
/>
setFilter("month")}
+ onClick={() => setDateRange("month")}
sx={{
backgroundColor:
- filter === "month" &&
+ dateRange === "month" &&
theme.palette.otherColors.fillGray,
}}
/>
-
+
History
{/* TODO New Table */}
-
+
{/* */}
diff --git a/Client/src/Pages/Monitors/index.css b/Client/src/Pages/Monitors/index.css
index 67da49c1f..7b40f7464 100644
--- a/Client/src/Pages/Monitors/index.css
+++ b/Client/src/Pages/Monitors/index.css
@@ -12,7 +12,6 @@
.monitors p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
-.monitors h2:has(+ p),
.monitors p.MuiTypography-root {
color: var(--env-var-color-5);
}
diff --git a/Client/src/Pages/PageSpeed/Configure/index.css b/Client/src/Pages/PageSpeed/Configure/index.css
new file mode 100644
index 000000000..577d18389
--- /dev/null
+++ b/Client/src/Pages/PageSpeed/Configure/index.css
@@ -0,0 +1,68 @@
+.configure-pagespeed form > .MuiStack-root:nth-child(2) {
+ background-color: var(--env-var-color-8);
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+ padding: var(--env-var-default-2);
+}
+.configure-pagespeed h1.MuiTypography-root {
+ font-size: var(--env-var-font-size-large-plus);
+}
+.configure-pagespeed h1.MuiTypography-root,
+.configure-pagespeed h2.MuiTypography-root {
+ color: var(--env-var-color-1);
+}
+.configure-pagespeed span.MuiTypography-root {
+ font-size: var(--env-var-font-size-small-plus);
+}
+.configure-pagespeed p.MuiTypography-root,
+.configure-pagespeed h3.MuiTypography-root {
+ color: var(--env-var-color-5);
+}
+.configure-pagespeed h1.MuiTypography-root,
+.configure-pagespeed h3.MuiTypography-root,
+.configure-pagespeed h2.MuiTypography-root {
+ font-weight: 600;
+}
+.configure-pagespeed p.MuiTypography-root,
+.configure-pagespeed h3.MuiTypography-root,
+.configure-pagespeed h3.MuiTypography-root > span.MuiTypography-root {
+ font-size: var(--env-var-font-size-medium);
+}
+.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root {
+ font-size: var(--env-var-font-size-small-plus);
+}
+.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root,
+.configure-pagespeed h3.MuiTypography-root > span.MuiTypography-root {
+ opacity: 0.6;
+}
+.configure-pagespeed .checkbox-wrapper {
+ margin-bottom: 1px;
+}
+.configure-pagespeed .MuiInputBase-root:has(> .Mui-disabled) {
+ background-color: var(--env-var-color-13);
+}
+/* to be removed */
+.configure-pagespeed .section-disabled {
+ opacity: 0.4;
+ background-color: #f1f1f2;
+ padding: 10px;
+ border-radius: 8px;
+ user-select: none;
+ pointer-events: none;
+}
+
+.configure-pagespeed button {
+ height: var(--env-var-height-2);
+}
+
+body:has(.configure-pagespeed) .select-dropdown .MuiMenuItem-root,
+.configure-pagespeed .select-wrapper .select-component > .MuiSelect-select {
+ text-transform: lowercase;
+}
+
+.configure-pagespeed .field,
+.configure-pagespeed .section-disabled,
+.configure-pagespeed .select-wrapper,
+.configure-pagespeed h3.MuiTypography-root {
+ flex: 1;
+}
diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx
new file mode 100644
index 000000000..91d250dd9
--- /dev/null
+++ b/Client/src/Pages/PageSpeed/Configure/index.jsx
@@ -0,0 +1,365 @@
+import { useEffect, useState } from "react";
+import { useTheme } from "@emotion/react";
+import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate, useParams } from "react-router";
+import {
+ deletePageSpeed,
+ getPageSpeedByUserId,
+ updatePageSpeed,
+} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
+import { monitorValidation } from "../../../Validation/validation";
+import { createToast } from "../../../Utils/toastUtils";
+import Button from "../../../Components/Button";
+import Field from "../../../Components/Inputs/Field";
+import Select from "../../../Components/Inputs/Select";
+import Checkbox from "../../../Components/Inputs/Checkbox";
+import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
+import WestRoundedIcon from "@mui/icons-material/WestRounded";
+import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
+import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
+
+import "./index.css";
+
+/**
+ * Renders a skeleton layout.
+ *
+ * @returns {JSX.Element}
+ */
+const SkeletonLayout = () => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+const PageSpeedConfigure = () => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const MS_PER_MINUTE = 60000;
+ const { authToken } = useSelector((state) => state.auth);
+ const { monitors } = useSelector((state) => state.pageSpeedMonitors);
+ const { monitorId } = useParams();
+ const [monitor, setMonitor] = useState({});
+ const [errors, setErrors] = useState({});
+
+ const frequencies = [
+ { _id: 3, name: "3 minutes" },
+ { _id: 5, name: "5 minutes" },
+ { _id: 10, name: "10 minutes" },
+ { _id: 20, name: "20 minutes" },
+ { _id: 60, name: "1 hour" },
+ { _id: 1440, name: "1 day" },
+ { _id: 10080, name: "1 week" },
+ ];
+
+ useEffect(() => {
+ const data = monitors.find((monitor) => monitor._id === monitorId);
+ if (!data) {
+ console.error("Error fetching pagespeed monitor of id: " + monitorId);
+ navigate("/not-found");
+ }
+ setMonitor({
+ ...data,
+ });
+ }, [monitorId]);
+
+ const handleChange = (event, id) => {
+ let { value } = event.target;
+ if (id === "interval") {
+ value = value * MS_PER_MINUTE;
+ }
+ setMonitor((prev) => ({ ...prev, [id]: value }));
+
+ const { error } = monitorValidation.validate(
+ { [id]: value },
+ { abortEarly: false }
+ );
+
+ setErrors((prev) => {
+ const newErrors = { ...prev };
+ if (error) newErrors[id] = error.details[0].message;
+ else delete newErrors[id];
+ return newErrors;
+ });
+ };
+
+ const handleSave = async (event) => {
+ event.preventDefault();
+ const action = await dispatch(
+ updatePageSpeed({ authToken, monitor: monitor })
+ );
+ if (action.meta.requestStatus === "fulfilled") {
+ createToast({ body: "Monitor updated successfully!" });
+ dispatch(getPageSpeedByUserId(authToken));
+ } else {
+ createToast({ body: "Failed to update monitor." });
+ }
+ };
+
+ const [isOpen, setIsOpen] = useState(false);
+ const handleRemove = async (event) => {
+ event.preventDefault();
+ const action = await dispatch(deletePageSpeed({ authToken, monitor }));
+ if (action.meta.requestStatus === "fulfilled") {
+ navigate("/pagespeed");
+ } else {
+ createToast({ body: "Failed to delete monitor." });
+ }
+ };
+
+ let loading = Object.keys(monitor).length === 0;
+
+ return (
+
+ {loading ? (
+
+ ) : (
+ <>
+ }
+ onClick={() => navigate(-1)}
+ sx={{
+ backgroundColor: theme.palette.otherColors.fillGray,
+ mb: theme.gap.large,
+ px: theme.gap.ml,
+ "& svg.MuiSvgIcon-root": {
+ mr: theme.gap.small,
+ fill: theme.palette.otherColors.slateGray,
+ },
+ }}
+ />
+
+ >
+ )}
+ setIsOpen(false)}
+ disablePortal
+ >
+
+
+ Do you really want to delete this monitor?
+
+
+ Once deleted, this monitor cannot be retrieved.
+
+
+ setIsOpen(false)}
+ />
+
+
+
+
+
+ );
+};
+
+export default PageSpeedConfigure;
diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx
index 52581f3b9..45a031da1 100644
--- a/Client/src/Pages/PageSpeed/Details/index.jsx
+++ b/Client/src/Pages/PageSpeed/Details/index.jsx
@@ -139,7 +139,7 @@ const SkeletonLayout = () => {
-
+
{
return props;
};
- const pieSize = { width: 200, height: 200 };
+ const pieSize = { width: 210, height: 200 };
const pieData = getPieData(audits);
const colorMap = getColors(performance);
@@ -321,7 +321,7 @@ const PageSpeedDetails = () => {
}}
/>
-
+ {monitor?.status ? : }
+
+
+
diff --git a/Client/src/assets/icons/mail.svg b/Client/src/assets/icons/mail.svg
new file mode 100644
index 000000000..75c22b111
--- /dev/null
+++ b/Client/src/assets/icons/mail.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/Client/src/index.css b/Client/src/index.css
index 8dc4222f5..5693ddaa5 100644
--- a/Client/src/index.css
+++ b/Client/src/index.css
@@ -1,5 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
+* {
+ box-sizing: border-box;
+}
+
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-weight: 400;
@@ -17,8 +21,10 @@
--env-var-font-familt-1: "Roboto", "Helvetica", "Arial", sans-serif;
+ --env-var-color-0: #fff;
--env-var-color-1: #101828;
--env-var-color-2: #475467;
+ --env-var-color-2-light: #838c99;
--env-var-color-3: #1570ef;
--env-var-color-4: #d0d5dd;
diff --git a/README.md b/README.md
index 6631d99be..d3e822f94 100644
--- a/README.md
+++ b/README.md
@@ -20,18 +20,22 @@ BlueWave Uptime is an open source server monitoring application used to track th
## Features
-* Completely open source, deployable on your servers
-* Website monitoring
-* Port monitoring
-* Ping monitoring
-* Incidents at a glance
-* Status pages
-* Page speed monitoring (in the works)
+- [x] Completely open source, deployable on your servers
+- [x] Website monitoring
+- [x] Port monitoring
+- [x] Ping monitoring
+- [x] Incidents at a glance
+- [x] Page speed monitoring
+- [ ] Scheduled maintenance (in the works)
+- [ ] Status pages (in the works)
-**Roadmap:**
-* 3rd party integrations
-* Domain monitoring
-* SSL monitoring
+**Roadmap (short term):**
+- [ ] 3rd party integrations
+- [ ] DNS monitoring
+- [ ] SSL monitoring
+
+**Roadmap (long term):**
+- [ ] Memory, disk and CPU monitoring using OpenTelemetry
## Tech stack
@@ -76,29 +80,41 @@ Made with [contrib.rocks](https://contrib.rocks).
###### Auth
- POST [/api/v1/auth/register](#post-register)
- POST [/api/v1/auth/login](#post-login)
- - POST [/api/v1/auth/user/{userId}](#post-auth-user-edit-id)
+ - PUT [/api/v1/auth/user/{userId}](#post-auth-user-edit-id)
- GET [/api/v1/auth/users](#get-all-users-id)
+ - GET [/api/v1/auth/users/admin](#get-all-admin-users-id)
+ - POST [/api/v1/auth/invite](#post-auth-invite-id)
+ - POST [/api/v1/auth/invite/verify/](#post-auth-invite-verify-id)
- POST [/api/v1/auth/recovery/request](#post-auth-recovery-request-id)
- POST [/api/v1/auth/recovery/validate](#post-auth-recovery-validate-id)
- POST [/api/v1/auth/recovery/reset](#post-auth-recovery-reset-id)
###### Monitors
- GET [/api/v1/monitors](#get-monitors)
- - GET [/api/v1/monitor/{id}](#get-monitor-id)
+ - GET [/api/v1/monitors/{monitorId}](#get-monitor-id)
- GET [/api/v1/monitors/user/{userId}?limit](#get-monitors-user-userid)
- POST [/api/v1/monitors](#post-monitors)
- - POST [/api/v1/monitors/delete/{monitorId}](#post-monitors-del-id)
- - POST [/api/v1/monitors/edit/{monitorId}](#post-monitors-edit-id)
+ - PUT [/api/v1/monitors/{monitorId}](#put-monitors-id)
+ - DELETE [/api/v1/monitors/{monitorId}](#delete-monitors-id)
+ - DELETE [/api/v1/monitors/all](#delete-all-monitors)
###### Checks
- POST [/api/v1/checks/{monitorId}](#post-checks)
- GET [/api/v1/checks/{monitorId}](#get-checks)
- - POST [/api/v1/checks/delete/{monitorId}](#delete-checks)
+ - DELETE [/api/v1/checks/{monitorId}](#delete-checks)
###### Alerts
- POST [/api/v1/alerts/{monitorId}](#create-alert)
- GET [/api/v1/alerts/user/{userId}](#get-alerts-user-id)
- GET [/api/v1/alerts/monitor/{monitorId}](#get-alerts-monitor-id)
- GET [/api/v1/alerts/{alertId}](#get-alert-alert-id)
- - POST [/api/v1/alerts/edit/{alertId}](#edit-alert)
- - POST [/api/v1/alerts/delete/{alertId}](#delete-alert)
+ - PUT [/api/v1/alerts/{alertId}](#edit-alert)
+ - DELETE [/api/v1/alerts/{alertId}](#delete-alert)
+ ###### Page Speed
+ - POST [/api/v1/pagespeed/:monitorId](#post-pagespeed)
+ - GET [/api/v1/pagespeed/:monitorId](#get-pagespeed)
+ - DELETE [/api/v1/pagespeed/:monitorId](#delete-pagespeed)
+ ###### Maintenance Window
+ - POST [/api/v1/maintenance-window/:monitorId](#post-maintenance-window)
+ - GET [/api/v1/maintenance-window/user/:userId](#get-maintenance-window-by-userid)
+ - GET [/api/v1/maintenance-window/monitor/:monitorId](#get-maintenance-window-by-monitor-id)
1. [Error Handling](#error-handling)
1. [Contributors](#contributors)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..6aa64730a
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,6 @@
+# Security Policy
+
+If you find a vulnerability, send it to hello@bluewavelabs.ca
+
+Please do not create an issue for security vulnerabilities.
+
diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js
index 20c2e0b8c..957eeed4e 100644
--- a/Server/controllers/checkController.js
+++ b/Server/controllers/checkController.js
@@ -3,6 +3,8 @@ const {
createCheckBodyValidation,
getChecksParamValidation,
getChecksQueryValidation,
+ getUserChecksParamValidation,
+ getUserChecksQueryValidation,
deleteChecksParamValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
@@ -60,6 +62,31 @@ const getChecks = async (req, res, next) => {
}
};
+const getUserChecks = async (req, res, next) => {
+ try {
+ await getUserChecksParamValidation.validateAsync(req.params);
+ await getUserChecksQueryValidation.validateAsync(req.query);
+ } catch (error) {
+ error.status = 422;
+ error.service = SERVICE_NAME;
+ error.message =
+ error.details?.[0]?.message || error.message || "Validation Error";
+ next(error);
+ return;
+ }
+ try {
+ const checkData = await req.db.getUserChecks(req);
+ return res.status(200).json({
+ success: true,
+ msg: successMessages.CHECK_GET,
+ data: checkData,
+ });
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ next(error);
+ }
+};
+
const deleteChecks = async (req, res, next) => {
try {
await deleteChecksParamValidation.validateAsync(req.params);
@@ -88,5 +115,6 @@ const deleteChecks = async (req, res, next) => {
module.exports = {
createCheck,
getChecks,
+ getUserChecks,
deleteChecks,
};
diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js
index 1dbc4e3b8..ed1b9e4b1 100644
--- a/Server/db/mongo/MongoDB.js
+++ b/Server/db/mongo/MongoDB.js
@@ -90,6 +90,7 @@ const {
createCheck,
getChecksCount,
getChecks,
+ getUserChecks,
deleteChecks,
} = require("./modules/checkModule");
@@ -148,6 +149,7 @@ module.exports = {
createCheck,
getChecksCount,
getChecks,
+ getUserChecks,
deleteChecks,
createAlert,
getAlertsByUserId,
diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js
index 4d315dc9a..92dc83f84 100644
--- a/Server/db/mongo/modules/checkModule.js
+++ b/Server/db/mongo/modules/checkModule.js
@@ -1,9 +1,12 @@
const Check = require("../../../models/Check");
-const filterLookup = {
+const Monitor = require("../../../models/Monitor");
+const mongoose = require("mongoose");
+const dateRangeLookup = {
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)),
};
+
/**
* Create a check for a monitor
* @async
@@ -28,13 +31,32 @@ const createCheck = async (checkData) => {
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"
- if (filter !== undefined) {
- checksQuery.createdAt = { $gte: filterLookup[filter] };
+ if (dateRange !== undefined) {
+ checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
+
+ if (filter !== undefined) {
+ checksQuery.status = false;
+ switch (filter) {
+ case "all":
+ break;
+ case "down":
+ break;
+ case "resolve":
+ checksQuery.statusCode = 5000;
+ break;
+ default:
+ console.log("default");
+ break;
+ }
+ }
+
const count = await Check.countDocuments(checksQuery);
return count;
};
@@ -50,23 +72,34 @@ const getChecksCount = async (req) => {
const getChecks = async (req) => {
try {
const { monitorId } = req.params;
- let { sortOrder, limit, filter, page, rowsPerPage } = req.query;
-
+ let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
// Default limit to 0 if not provided
- if (limit === undefined) limit = 0;
+ limit = limit === "undefined" ? 0 : limit;
// Default sort order is newest -> oldest
- if (sortOrder === "asc") {
- sortOrder = 1;
- } else if (sortOrder === "desc") {
- sortOrder = -1;
- } else sortOrder = -1;
+ sortOrder = sortOrder === "asc" ? 1 : -1;
// Build query
const checksQuery = { monitorId: monitorId };
// Filter checks by "day", "week", or "month"
+ if (dateRange !== undefined) {
+ checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
+ }
+ // Fitler checks by status
if (filter !== undefined) {
- checksQuery.createdAt = { $gte: filterLookup[filter] };
+ checksQuery.status = false;
+ switch (filter) {
+ case "all":
+ break;
+ case "down":
+ break;
+ case "resolve":
+ checksQuery.statusCode = 5000;
+ break;
+ default:
+ console.log("default");
+ break;
+ }
}
// Need to skip and limit here
@@ -84,8 +117,61 @@ const getChecks = async (req) => {
}
};
+const getUserChecks = async (req) => {
+ const { userId } = req.params;
+ let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
+
+ // Get monitorIDs
+ const userMonitors = await Monitor.find({ userId: userId });
+ const monitorIds = userMonitors.map((monitor) => monitor._id);
+
+ //Build check query
+ // Default limit to 0 if not provided
+ limit = limit === "undefined" ? 0 : limit;
+
+ // Default sort order is newest -> oldest
+ sortOrder = sortOrder === "asc" ? 1 : -1;
+
+ checksQuery = { monitorId: { $in: monitorIds } };
+
+ if (filter !== undefined) {
+ checksQuery.status = false;
+ switch (filter) {
+ case "all":
+ break;
+ case "down":
+ break;
+ case "resolve":
+ checksQuery.statusCode = 5000;
+ break;
+ default:
+ console.log("default");
+ break;
+ }
+ }
+
+ if (dateRange !== undefined) {
+ checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
+ }
+
+ // Skip and limit for pagination
+ let skip = 0;
+ if (page && rowsPerPage) {
+ skip = page * rowsPerPage;
+ }
+
+ const checksCount = await Check.countDocuments(checksQuery);
+
+ const checks = await Check.find(checksQuery)
+ .skip(skip)
+ .limit(rowsPerPage)
+ .sort({ createdAt: sortOrder });
+
+ return { checksCount, checks };
+};
+
/**
- * Delete all checks for a monitor
+ * Delete all checks for a user
* @async
* @param {string} monitorId
* @returns {number}
@@ -100,4 +186,10 @@ const deleteChecks = async (monitorId) => {
throw error;
}
};
-module.exports = { createCheck, getChecksCount, getChecks, deleteChecks };
+module.exports = {
+ createCheck,
+ getChecksCount,
+ getChecks,
+ getUserChecks,
+ deleteChecks,
+};
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index 15f431673..34255b2e8 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -81,14 +81,15 @@ const getMonitorById = async (req, res) => {
// If more than numToDisplay checks, pick every nth check
if (numToDisplay !== undefined && checks && checks.length > numToDisplay) {
const n = Math.ceil(checks.length / numToDisplay);
- checks = checks.filter((_, index) => index % n === 0);
+ checks = checks.filter(
+ (_, index) => index % n === 0 || index === checks.length - 1
+ );
}
// Normalize checks if requested
- if (normalize) {
- checks = NormalizeData(checks, 10, 100);
+ if (normalize !== undefined) {
+ checks = NormalizeData(checks, 1, 100);
}
-
const notifications = await Notification.find({ monitorId: monitor._id });
const monitorWithChecks = { ...monitor.toObject(), checks, notifications };
return monitorWithChecks;
@@ -148,7 +149,7 @@ const getMonitorsByUserId = async (req, res) => {
.limit(limit);
//Normalize checks if requested
- if (normalize === true) {
+ if (normalize !== undefined) {
checks = NormalizeData(checks, 10, 100);
}
diff --git a/Server/models/Check.js b/Server/models/Check.js
index bf4413977..aeb4f4849 100644
--- a/Server/models/Check.js
+++ b/Server/models/Check.js
@@ -4,7 +4,7 @@ const Notification = require("../models/Notification");
/**
* Check Schema for MongoDB collection.
- *
+ *
* Represents a check associated with a monitor, storing information
* about the status and response of a particular check event.
*/
@@ -12,7 +12,7 @@ const CheckSchema = mongoose.Schema(
{
/**
* Reference to the associated Monitor document.
- *
+ *
* @type {mongoose.Schema.Types.ObjectId}
*/
monitorId: {
@@ -22,7 +22,7 @@ const CheckSchema = mongoose.Schema(
},
/**
* Status of the check (true for up, false for down).
- *
+ *
* @type {Boolean}
*/
status: {
@@ -30,23 +30,23 @@ const CheckSchema = mongoose.Schema(
},
/**
* Response time of the check in milliseconds.
- *
+ *
* @type {Number}
*/
responseTime: {
- type: Number,
+ type: Number,
},
/**
* HTTP status code received during the check.
- *
+ *
* @type {Number}
*/
statusCode: {
- type: Number,
+ type: Number,
},
/**
* Message or description of the check result.
- *
+ *
* @type {String}
*/
message: {
@@ -54,7 +54,7 @@ const CheckSchema = mongoose.Schema(
},
/**
* Expiry date of the check, auto-calculated to expire after 30 days.
- *
+ *
* @type {Date}
*/
expiry: {
@@ -70,7 +70,7 @@ const CheckSchema = mongoose.Schema(
/**
* Pre-save middleware to handle status change notifications.
- *
+ *
* This middleware checks if the status of the monitor associated
* with the check has changed and sends notifications if necessary.
*/
@@ -79,7 +79,9 @@ CheckSchema.pre("save", async function (next) {
const monitor = await mongoose.model("Monitor").findById(this.monitorId);
if (monitor) {
- const notifications = await Notification.find({ monitorId: this.monitorId });
+ const notifications = await Notification.find({
+ monitorId: this.monitorId,
+ });
// Check if there are any notifications
if (notifications && notifications.length > 0) {
@@ -128,5 +130,4 @@ CheckSchema.pre("save", async function (next) {
}
});
-
module.exports = mongoose.model("Check", CheckSchema);
diff --git a/Server/routes/alertRoute.js b/Server/routes/alertRoute.js
index ba30fd117..434c0495a 100644
--- a/Server/routes/alertRoute.js
+++ b/Server/routes/alertRoute.js
@@ -29,14 +29,14 @@ router.get(
alertController.getAlertById
);
// Edit
-router.post(
- "/edit/:alertId",
+router.put(
+ "/:alertId",
verifyOwnership(Monitor, "monitorId"),
alertController.editAlert
);
//Delete
-router.post(
- "/delete/:alertId",
+router.delete(
+ "/:alertId",
verifyOwnership(Monitor, "monitorId"),
alertController.deleteAlert
);
diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js
index 0216343c8..69aa682af 100644
--- a/Server/routes/checkRoute.js
+++ b/Server/routes/checkRoute.js
@@ -15,10 +15,12 @@ router.get(
checkController.getChecks
);
-router.post(
- "/delete/:monitorId",
+router.delete(
+ "/:monitorId",
verifyOwnership(Monitor, "monitorId"),
checkController.deleteChecks
);
-module.exports = router;
\ No newline at end of file
+router.get("/user/:userId", checkController.getUserChecks);
+
+module.exports = router;
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index 1f50c9e70..2622181a3 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -14,10 +14,10 @@ router.delete(
monitorController.deleteMonitor
);
router.put(
- "/edit/:monitorId",
+ "/:monitorId",
verifyOwnership(Monitor, "monitorId"),
monitorController.editMonitor
);
-router.delete("/delete/all", monitorController.deleteAllMonitors);
+router.delete("/all", monitorController.deleteAllMonitors);
module.exports = router;
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index f938fabd9..74bf5ed92 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -235,7 +235,21 @@ const getChecksParamValidation = joi.object({
const getChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
- filter: joi.string().valid("day", "week", "month"),
+ dateRange: joi.string().valid("day", "week", "month"),
+ filter: joi.string().valid("all", "down", "resolve"),
+ page: joi.number(),
+ rowsPerPage: joi.number(),
+});
+
+const getUserChecksParamValidation = joi.object({
+ userId: joi.string().required(),
+});
+
+const getUserChecksQueryValidation = joi.object({
+ sortOrder: joi.string().valid("asc", "desc"),
+ limit: joi.number(),
+ dateRange: joi.string().valid("day", "week", "month"),
+ filter: joi.string().valid("all", "down", "resolve"),
page: joi.number(),
rowsPerPage: joi.number(),
});
@@ -320,6 +334,8 @@ module.exports = {
createCheckBodyValidation,
getChecksParamValidation,
getChecksQueryValidation,
+ getUserChecksParamValidation,
+ getUserChecksQueryValidation,
deleteChecksParamValidation,
deleteUserParamValidation,
getPageSpeedCheckParamValidation,