diff --git a/Client/src/Components/Label/index.jsx b/Client/src/Components/Label/index.jsx
index 0799a87d7..886f66fb9 100644
--- a/Client/src/Components/Label/index.jsx
+++ b/Client/src/Components/Label/index.jsx
@@ -138,6 +138,11 @@ const StatusLabel = ({ status, text, customStyles }) => {
bgColor: theme.palette.error.bg,
borderColor: theme.palette.error.light,
},
+ pending: {
+ dotColor: theme.palette.warning.main,
+ bgColor: theme.palette.warning.bg,
+ borderColor: theme.palette.warning.light,
+ },
"cannot resolve": {
dotColor: theme.palette.unresolved.main,
bgColor: theme.palette.unresolved.bg,
@@ -170,7 +175,7 @@ const StatusLabel = ({ status, text, customStyles }) => {
};
StatusLabel.propTypes = {
- status: PropTypes.oneOf(["up", "down", "cannot resolve"]),
+ status: PropTypes.oneOf(["up", "down", "pending", "cannot resolve"]),
text: PropTypes.string,
customStyles: PropTypes.object,
};
diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
index 8c4630b19..9809ac710 100644
--- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
@@ -1,25 +1,13 @@
import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
-import {
- Box,
- Button,
- ButtonGroup,
- Divider,
- IconButton,
- Modal,
- Stack,
- TextField,
- Typography,
-} from "@mui/material";
+import { Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
-import EditSvg from "../../../assets/icons/edit.svg?react";
import Field from "../../Inputs/Field";
import { credentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import BasicTable from "../../BasicTable";
-import Remove from "../../../assets/icons/trash-bin.svg?react";
import Select from "../../Inputs/Select";
import LoadingButton from "@mui/lab/LoadingButton";
@@ -54,6 +42,7 @@ const TeamPanel = () => {
const [members, setMembers] = useState([]);
const [filter, setFilter] = useState("all");
const [errors, setErrors] = useState({});
+ const [isSendingInvite, setIsSendingInvite] = useState(false);
useEffect(() => {
const fetchTeam = async () => {
@@ -173,6 +162,7 @@ const TeamPanel = () => {
};
const handleInviteMember = async () => {
+ setIsSendingInvite(true);
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
setToInvite((prev) => ({ ...prev, role: ["user"] }));
@@ -185,24 +175,28 @@ const TeamPanel = () => {
if (error) {
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
- } else
- try {
- await networkService.requestInvitationToken(
- authToken,
- toInvite.email,
- toInvite.role
- );
+ return;
+ }
- closeInviteModal();
- createToast({
- body: "Member invited. They will receive an email with details on how to create their account.",
- });
- } catch (error) {
- createToast({
- body: error.message || "Unknown error.",
- });
- }
+ try {
+ await networkService.requestInvitationToken(
+ authToken,
+ toInvite.email,
+ toInvite.role
+ );
+ closeInviteModal();
+ createToast({
+ body: "Member invited. They will receive an email with details on how to create their account.",
+ });
+ } catch (error) {
+ createToast({
+ body: error.message || "Unknown error.",
+ });
+ } finally {
+ setIsSendingInvite(false);
+ }
};
+
const closeInviteModal = () => {
setIsOpen(false);
setToInvite({ email: "", role: ["0"] });
@@ -307,13 +301,14 @@ const TeamPanel = () => {
-
+
{
mt={theme.spacing(8)}
justifyContent="flex-end"
>
-
+
Send invite
diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
index e32d673cc..317ab1320 100644
--- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
+++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
@@ -28,6 +28,26 @@ export const createPageSpeed = createAsyncThunk(
}
);
+export const getPagespeedMonitorById = createAsyncThunk(
+ "monitors/getMonitorById",
+ async (data, thunkApi) => {
+ try {
+ const { authToken, monitorId } = data;
+ const res = await networkService.getMonitorByid(authToken, monitorId);
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data) {
+ return thunkApi.rejectWithValue(error.response.data);
+ }
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
+ }
+ }
+);
+
export const getPageSpeedByTeamId = createAsyncThunk(
"pageSpeedMonitors/getPageSpeedByTeamId",
async (token, thunkApi) => {
@@ -109,6 +129,25 @@ export const deletePageSpeed = createAsyncThunk(
}
}
);
+export const pausePageSpeed = createAsyncThunk(
+ "pageSpeedMonitors/pausePageSpeed",
+ async (data, thunkApi) => {
+ try {
+ const { authToken, monitorId } = data;
+ const res = await networkService.pauseMonitorById(authToken, monitorId);
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data) {
+ return thunkApi.rejectWithValue(error.response.data);
+ }
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
+ }
+ }
+);
const pageSpeedMonitorSlice = createSlice({
name: "pageSpeedMonitor",
@@ -124,7 +163,7 @@ const pageSpeedMonitorSlice = createSlice({
extraReducers: (builder) => {
builder
// *****************************************************
- // Monitors by userId
+ // Monitors by teamId
// *****************************************************
.addCase(getPageSpeedByTeamId.pending, (state) => {
@@ -143,6 +182,23 @@ const pageSpeedMonitorSlice = createSlice({
: "Getting page speed monitors failed";
})
+ // *****************************************************
+ .addCase(getPagespeedMonitorById.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.success = action.payload.success;
+ state.msg = action.payload.msg;
+ })
+ .addCase(getPagespeedMonitorById.rejected, (state, action) => {
+ state.isLoading = false;
+ state.success = false;
+ state.msg = action.payload
+ ? action.payload.msg
+ : "Failed to get pagespeed monitor";
+ })
+
// *****************************************************
// Create Monitor
// *****************************************************
@@ -163,7 +219,7 @@ const pageSpeedMonitorSlice = createSlice({
})
// *****************************************************
- // Create Monitor
+ // Update Monitor
// *****************************************************
.addCase(updatePageSpeed.pending, (state) => {
state.isLoading = true;
@@ -198,6 +254,24 @@ const pageSpeedMonitorSlice = createSlice({
state.msg = action.payload
? action.payload.msg
: "Failed to delete page speed monitor";
+ })
+ // *****************************************************
+ // Pause Monitor
+ // *****************************************************
+ .addCase(pausePageSpeed.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(pausePageSpeed.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.success = action.payload.success;
+ state.msg = action.payload.msg;
+ })
+ .addCase(pausePageSpeed.rejected, (state, action) => {
+ state.isLoading = false;
+ state.success = false;
+ state.msg = action.payload
+ ? action.payload.msg
+ : "Failed to pause page speed monitor";
});
},
});
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index 2c381812c..895bd1493 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -236,7 +236,7 @@ const uptimeMonitorsSlice = createSlice({
state.success = false;
state.msg = action.payload
? action.payload.msg
- : "Failed to pause uptime monitor";
+ : "Failed to get uptime monitor";
})
// *****************************************************
// update Monitor
diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx
index 637c71ddc..1ee171034 100644
--- a/Client/src/Pages/Monitors/Configure/index.jsx
+++ b/Client/src/Pages/Monitors/Configure/index.jsx
@@ -189,6 +189,18 @@ const Configure = () => {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
+ const statusColor = {
+ true: theme.palette.success.main,
+ false: theme.palette.error.main,
+ undefined: theme.palette.warning.main,
+ };
+
+ const statusMsg = {
+ true: "Your site is up.",
+ false: "Your site is down.",
+ undefined: "Pending...",
+ };
+
return (
{Object.keys(monitor).length === 0 ? (
@@ -210,13 +222,7 @@ const Configure = () => {
flex={1}
>
-
+
{parsedUrl?.host ? (
{
- Your site is {monitor?.status ? "up" : "down"}.
+ {statusMsg[monitor?.status ?? undefined]}
{
>
)}
-
+
diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx
index f3239fb42..4d9917586 100644
--- a/Client/src/Pages/Monitors/Details/index.jsx
+++ b/Client/src/Pages/Monitors/Details/index.jsx
@@ -4,7 +4,6 @@ import {
Box,
Button,
Popover,
- Skeleton,
Stack,
Tooltip,
Typography,
@@ -34,78 +33,9 @@ import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import { StatBox, ChartBox, IconBox } from "./styled";
import { DownBarChart, UpBarChart } from "./Charts";
+import SkeletonLayout from "./skeleton";
import "./index.css";
-/**
- * Renders a skeleton layout.
- *
- * @returns {JSX.Element}
- */
-const SkeletonLayout = () => {
- const theme = useTheme();
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
/**
* Details page component displaying monitor details and related information.
* @component
@@ -238,6 +168,19 @@ const DetailsPage = ({ isAdmin }) => {
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
+
+ const statusColor = {
+ true: theme.palette.success.main,
+ false: theme.palette.error.main,
+ undefined: theme.palette.warning.main,
+ };
+
+ const statusMsg = {
+ true: "Your site is up.",
+ false: "Your site is down.",
+ undefined: "Pending...",
+ };
+
return (
{loading ? (
@@ -267,7 +210,7 @@ const DetailsPage = ({ isAdmin }) => {
gap={theme.spacing(2)}
>
{
>
diff --git a/Client/src/Pages/Monitors/Details/skeleton.jsx b/Client/src/Pages/Monitors/Details/skeleton.jsx
new file mode 100644
index 000000000..b267f413e
--- /dev/null
+++ b/Client/src/Pages/Monitors/Details/skeleton.jsx
@@ -0,0 +1,73 @@
+import { Box, Skeleton, Stack, useTheme } from "@mui/material";
+
+/**
+ * Renders a skeleton layout.
+ *
+ * @returns {JSX.Element}
+ */
+const SkeletonLayout = () => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/Monitors/Home/monitorData.jsx b/Client/src/Pages/Monitors/Home/monitorData.jsx
index 2351a59e9..afe943f4e 100644
--- a/Client/src/Pages/Monitors/Home/monitorData.jsx
+++ b/Client/src/Pages/Monitors/Home/monitorData.jsx
@@ -66,7 +66,7 @@ export const buildData = (monitors, isAdmin, navigate) => {
percentageColor,
status:
monitor.status === undefined
- ? "unknown"
+ ? "pending"
: monitor.status === true
? "up"
: "down",
diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx
index 92dd5bb36..005583f2c 100644
--- a/Client/src/Pages/PageSpeed/Configure/index.jsx
+++ b/Client/src/Pages/PageSpeed/Configure/index.jsx
@@ -1,12 +1,14 @@
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
-import { Box, Button, Modal, Skeleton, Stack, Typography } from "@mui/material";
+import { Box, Button, Modal, Stack, Typography } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router";
import {
deletePageSpeed,
+ getPagespeedMonitorById,
getPageSpeedByTeamId,
updatePageSpeed,
+ pausePageSpeed,
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
@@ -17,63 +19,18 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
-
+import LoadingButton from "@mui/lab/LoadingButton";
+import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
+import SkeletonLayout from "./skeleton";
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 { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const { monitorId } = useParams();
const [monitor, setMonitor] = useState({});
const [errors, setErrors] = useState({});
@@ -89,15 +46,25 @@ const PageSpeedConfigure = () => {
];
useEffect(() => {
- const data = monitors.find((monitor) => monitor._id === monitorId);
- if (!data) {
- logger.error("Error fetching pagespeed monitor of id: " + monitorId);
- navigate("/not-found", { replace: true });
- }
- setMonitor({
- ...data,
- });
- }, [monitorId, monitors, navigate]);
+ const fetchMonitor = async () => {
+ try {
+ const action = await dispatch(
+ getPagespeedMonitorById({ authToken, monitorId })
+ );
+
+ if (getPagespeedMonitorById.fulfilled.match(action)) {
+ const monitor = action.payload.data;
+ setMonitor(monitor);
+ } else if (getPagespeedMonitorById.rejected.match(action)) {
+ throw new Error(action.error.message);
+ }
+ } catch (error) {
+ logger.error("Error fetching monitor of id: " + monitorId);
+ navigate("/not-found", { replace: true });
+ }
+ };
+ fetchMonitor();
+ }, [dispatch, authToken, monitorId, navigate]);
const handleChange = (event, id) => {
let { value } = event.target;
@@ -119,6 +86,21 @@ const PageSpeedConfigure = () => {
});
};
+ const handlePause = async () => {
+ try {
+ const action = await dispatch(pausePageSpeed({ authToken, monitorId }));
+ if (pausePageSpeed.fulfilled.match(action)) {
+ const monitor = action.payload.data;
+ setMonitor(monitor);
+ } else if (pausePageSpeed.rejected.match(action)) {
+ throw new Error(action.error.message);
+ }
+ } catch (error) {
+ logger.error("Error pausing monitor: " + monitorId);
+ createToast({ body: "Failed to pause monitor" });
+ }
+ };
+
const handleSave = async (event) => {
event.preventDefault();
const action = await dispatch(
@@ -143,11 +125,9 @@ const PageSpeedConfigure = () => {
}
};
- let loading = Object.keys(monitor).length === 0;
-
return (
- {loading ? (
+ {Object.keys(monitor).length === 0 ? (
) : (
<>
@@ -195,7 +175,9 @@ const PageSpeedConfigure = () => {
-
-
+
{
-
+
>
diff --git a/Client/src/Pages/PageSpeed/Configure/skeleton.jsx b/Client/src/Pages/PageSpeed/Configure/skeleton.jsx
new file mode 100644
index 000000000..301a00e52
--- /dev/null
+++ b/Client/src/Pages/PageSpeed/Configure/skeleton.jsx
@@ -0,0 +1,51 @@
+import { Box, Skeleton, Stack } from "@mui/material";
+import { useTheme } from "@emotion/react";
+
+/**
+ * Renders a skeleton layout.
+ *
+ * @returns {JSX.Element}
+ */
+const SkeletonLayout = () => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
index 00c02a362..4c8204153 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
@@ -1,4 +1,6 @@
-import { Box, Button, Stack, Typography } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
+import LoadingButton from "@mui/lab/LoadingButton";
+
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
@@ -12,11 +14,11 @@ import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMon
import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
import { logger } from "../../../Utils/Logger";
-
const CreatePageSpeed = () => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
+ const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const MS_PER_MINUTE = 60000;
const { user, authToken } = useSelector((state) => state.auth);
@@ -226,7 +228,8 @@ const CreatePageSpeed = () => {
-
+
diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx
index fc615d848..d1f15ea8c 100644
--- a/Client/src/Pages/Settings/index.jsx
+++ b/Client/src/Pages/Settings/index.jsx
@@ -16,6 +16,9 @@ const Settings = ({ isAdmin }) => {
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const dispatch = useDispatch();
+
+ // TODO Handle saving
+
const handleClearStats = async () => {
try {
const action = await dispatch(
@@ -156,13 +159,14 @@ const Settings = ({ isAdmin }) => {
-
+
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index 0d831f5b6..091d41e2a 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -6,6 +6,9 @@ const {
editMonitorBodyValidation,
getMonitorsByTeamIdQueryValidation,
pauseMonitorParamValidation,
+ getMonitorStatsByIdParamValidation,
+ getMonitorStatsByIdQueryValidation,
+ getCertificateParamValidation,
getMonitorAggregateStatsParamValidation,
getMonitorAggregateStatsQueryValidation,
} = require("../validation/joi");
@@ -84,7 +87,8 @@ const getMonitorAggregateStats = async (req, res, next) => {
*/
const getMonitorStatsById = async (req, res, next) => {
try {
- //Validation
+ await getMonitorStatsByIdParamValidation.validateAsync(req.params);
+ await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
@@ -108,7 +112,7 @@ const getMonitorStatsById = async (req, res, next) => {
const getMonitorCertificate = async (req, res, next) => {
try {
- //validation
+ await getCertificateParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
@@ -398,6 +402,7 @@ const pauseMonitor = async (req, res, next) => {
await req.jobQueue.addJob(monitor._id, monitor);
}
monitor.isActive = !monitor.isActive;
+ monitor.status = undefined;
monitor.save();
return res.status(200).json({
success: true,
diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js
index 6f959f758..29233b73a 100644
--- a/Server/db/mongo/MongoDB.js
+++ b/Server/db/mongo/MongoDB.js
@@ -65,6 +65,7 @@ const {
const {
getAllMonitors,
getMonitorAggregateStats,
+ testMethod,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
@@ -145,7 +146,6 @@ module.exports = {
resetPassword,
checkSuperadmin,
getAllMonitors,
- getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index b913519b6..ab867ff47 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -35,7 +35,7 @@ const calculateUptimeDuration = (checks) => {
const latestCheck = new Date(checks[0].createdAt);
let latestDownCheck = 0;
- for (let i = 0; i < checks.length; i++) {
+ for (let i = checks.length; i <= 0; i--) {
if (checks[i].status === false) {
latestDownCheck = new Date(checks[i].createdAt);
break;
@@ -77,11 +77,11 @@ const getLatestResponseTime = (checks) => {
};
/**
- * Helper function to get average 24h response time
+ * Helper function to get average response time
* @param {Array} checks Array of check objects.
* @returns {number} Timestamp of the most recent check.
*/
-const getAverageResponseTime24Hours = (checks) => {
+const getAverageResponseTime = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
@@ -121,108 +121,6 @@ const getIncidents = (checks) => {
return check.status === false ? (acc += 1) : acc;
}, 0);
};
-/**
- * Helper function to get all incidents
- * @param {Array} checks Array of check objects.
- * @returns {Array} Array of booleans representing up/down.
- */
-const getStatusBarValues = (monitor, checks) => {
- const checksIn60Mins = Math.floor((60 * 60 * 1000) / monitor.interval);
- const noBlankChecks = checksIn60Mins - checks.length;
-
- const statusBarValues = checks.map((check) => {
- return {
- status: check.status,
- responseTime: check.responseTime,
- value: 75,
- };
- });
-
- for (let i = 0; i < noBlankChecks; i++) {
- statusBarValues.push({ status: undefined, responseTime: 0, value: 75 });
- }
- return statusBarValues.reverse();
-};
-
-/**
- * Get aggregate monitor stats for charts
- * @async
- * @param {Express.Request} req
- * @param {Express.Response} res
- * @returns {Promise}
- * @throws {Error}
- */
-const getMonitorAggregateStats = async (monitorId, dateRange) => {
- const startDates = {
- 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 startDate = startDates[dateRange];
- if (!startDate) {
- throw new Error("Invalid date range specified");
- }
- const endDate = new Date();
-
- const checks = await Check.find({
- monitorId: monitorId,
- createdAt: {
- $gte: startDate,
- $lte: endDate,
- },
- });
-
- let groupedChecks;
- // Group checks by hour if range is day
- if (dateRange === "day") {
- groupedChecks = checks.reduce((acc, check) => {
- const hour = new Date(check.createdAt).getHours();
- if (!acc[hour]) {
- acc[hour] = { hour, checks: [] };
- }
- acc[hour].checks.push(check);
- return acc;
- }, {});
- }
-
- // Group checks by day if range is week or month
- else {
- groupedChecks = checks.reduce((acc, check) => {
- const day = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part
- if (!acc[day]) {
- acc[day] = { day, checks: [] };
- }
- acc[day].checks.push(check);
- return acc;
- }, {});
- }
-
- // Map grouped checks to stats
- stats = Object.values(groupedChecks).map((group) => {
- const totalChecks = group.checks.length;
- const totalIncidents = group.checks.filter(
- (check) => check.status === false
- ).length;
- const avgResponseTime =
- group.checks.reduce((sum, check) => sum + check.responseTime, 0) /
- totalChecks;
-
- return {
- hour: group.hour,
- day: group.day,
- totalChecks,
- totalIncidents,
- avgResponseTime,
- };
- });
-
- return stats;
- } catch (error) {
- throw error;
- }
-};
/**
* Get stats by monitor ID
@@ -233,42 +131,36 @@ const getMonitorAggregateStats = async (monitorId, dateRange) => {
* @throws {Error}
*/
const getMonitorStatsById = async (req) => {
- const filterLookup = {
- hour: new Date(new Date().getTime() - 60 * 60 * 1000),
+ const startDates = {
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)),
};
-
+ const endDate = new Date();
try {
+ // Get monitor
const { monitorId } = req.params;
- let { status, limit, sortOrder, dateRange, numToDisplay, normalize } =
- req.query;
-
+ let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
+ const monitor = await Monitor.findById(monitorId);
+ if (monitor === null || monitor === undefined) {
+ throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
+ }
// 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);
- if (monitor === null || monitor === undefined) {
- throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
- }
-
- // Determine if this is a pagespeed monitor or an http/ping monitor
let model =
monitor.type === "http" || monitor.type === "ping"
? Check
: PageSpeedCheck;
- // Build monitor stats object
const monitorStats = {
...monitor.toObject(),
};
- // Start building query
+ // Build checks query
const checksQuery = { monitorId: monitor._id };
// Get all checks
@@ -276,83 +168,88 @@ const getMonitorStatsById = async (req) => {
createdAt: sortOrder,
});
+ const checksQueryForDateRange = {
+ ...checksQuery,
+ createdAt: {
+ $gte: startDates[dateRange],
+ $lte: endDate,
+ },
+ };
+
+ const checksForDateRange = await model
+ .find(checksQueryForDateRange)
+ .sort({ createdAt: sortOrder });
+
if (monitor.type === "http" || monitor.type === "ping") {
- const checksQuery24Hours = {
- ...checksQuery,
- createdAt: { $gte: filterLookup.day },
- };
- const checksQuery7Days = {
- ...checksQuery,
- createdAt: { $gte: filterLookup.week },
- };
- const checksQuery30Days = {
- ...checksQuery,
- createdAt: { $gte: filterLookup.month },
- };
- const checksQuery60Mins = {
- ...checksQuery,
- createdAt: { $gte: filterLookup.hour },
- };
-
- const [checks24Hours, checks7Days, checks30Days, checks60Mins] =
- await Promise.all([
- model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
- model.find(checksQuery7Days).sort({ createdAt: sortOrder }),
- model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
- model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
- ]);
-
// HTTP/PING Specific stats
- monitorStats.avgResponseTime24hours =
- getAverageResponseTime24Hours(checks24Hours);
- monitorStats.uptime7Days = getUptimePercentage(checks7Days);
- monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
- monitorStats.uptime30Days = getUptimePercentage(checks30Days);
- monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins);
+ monitorStats.avgResponseTime = getAverageResponseTime(checksForDateRange);
+ monitorStats.uptime = getUptimePercentage(checksForDateRange);
+
+ // Aggregate data
+ let groupedChecks;
+ // Group checks by hour if range is day
+ if (dateRange === "day") {
+ groupedChecks = checksForDateRange.reduce((acc, check) => {
+ const time = new Date(check.createdAt);
+ time.setMinutes(0, 0, 0);
+ if (!acc[time]) {
+ acc[time] = { time, checks: [] };
+ }
+ acc[time].checks.push(check);
+ return acc;
+ }, {});
+ }
+
+ // Map grouped checks to stats
+ const aggregateData = Object.values(groupedChecks).map((group) => {
+ const totalChecks = group.checks.length;
+ const totalIncidents = group.checks.filter(
+ (check) => check.status === false
+ ).length;
+ const avgResponseTime =
+ group.checks.reduce((sum, check) => sum + check.responseTime, 0) /
+ totalChecks;
+
+ return {
+ time: group.time,
+ totalChecks,
+ totalIncidents,
+ avgResponseTime,
+ };
+ });
+ monitorStats.aggregateData = aggregateData;
}
- //Get checks for dateRange
- if (status !== undefined) {
- checksQuery.status = status;
- }
-
- // Filter checks by "day", "week", or "month"
- if (dateRange !== undefined) {
- checksQuery.createdAt = { $gte: filterLookup[dateRange] };
- }
-
- let dateRangeChecks = await model
- .find(checksQuery)
- .sort({
- createdAt: sortOrder,
- })
- .limit(limit);
-
- const incidents = getIncidents(dateRangeChecks);
+ monitorStats.incidents = getIncidents(checksForDateRange);
// If more than numToDisplay checks, pick every nth check
+
+ let nthChecks = checksForDateRange;
+
if (
numToDisplay !== undefined &&
- dateRangeChecks &&
- dateRangeChecks.length > numToDisplay
+ checksForDateRange &&
+ checksForDateRange.length > numToDisplay
) {
- const n = Math.ceil(dateRangeChecks.length / numToDisplay);
- dateRangeChecks = dateRangeChecks.filter((_, index) => index % n === 0);
+ const n = Math.ceil(checksForDateRange.length / numToDisplay);
+ nthChecks = checksForDateRange.filter((_, index) => index % n === 0);
}
// Normalize checks if requested
if (normalize !== undefined) {
- dateRangeChecks = NormalizeData(dateRangeChecks, 1, 100);
+ const normailzedChecks = NormalizeData(checksForDateRange, 1, 100);
+ monitorStats.checks = normailzedChecks;
+ } else {
+ monitorStats.checks = nthChecks;
}
- // Add common stats and stats that depend on the dateRange
monitorStats.uptimeDuration = calculateUptimeDuration(checksAll);
monitorStats.lastChecked = getLastChecked(checksAll);
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
- monitorStats.incidents = incidents;
- monitorStats.checks = dateRangeChecks;
+
return monitorStats;
} catch (error) {
+ error.methodName = "getMonitorStatsById";
throw error;
}
};
@@ -540,7 +437,6 @@ const editMonitor = async (candidateId, candidateMonitor) => {
module.exports = {
getAllMonitors,
- getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
diff --git a/Server/middleware/handleErrors.js b/Server/middleware/handleErrors.js
index 1e1e955c5..db51c1af6 100644
--- a/Server/middleware/handleErrors.js
+++ b/Server/middleware/handleErrors.js
@@ -5,7 +5,10 @@ const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
const message = error.message || errorMessages.FRIENDLY_ERROR;
const service = error.service || errorMessages.UNKNOWN_SERVICE;
- logger.error(error.message, { service: service });
+ logger.error(error.message, {
+ service: service,
+ methodName: error.methodName,
+ });
res.status(status).json({ success: false, msg: message });
};
diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js
index fdc147d1b..d91275e23 100644
--- a/Server/models/Monitor.js
+++ b/Server/models/Monitor.js
@@ -24,7 +24,7 @@ const MonitorSchema = mongoose.Schema(
},
status: {
type: Boolean,
- default: true,
+ default: undefined,
},
type: {
type: String,
@@ -46,7 +46,7 @@ const MonitorSchema = mongoose.Schema(
},
uptimePercentage: {
type: Number,
- default: undefined,
+ default: undefined,
},
notifications: [
{
diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js
index ed15e0f94..5248c88fa 100644
--- a/Server/service/jobQueue.js
+++ b/Server/service/jobQueue.js
@@ -238,7 +238,6 @@ class JobQueue {
console.log("Adding job", payload.url);
// Execute job immediately
await this.queue.add(jobName, payload);
-
await this.queue.add(jobName, payload, {
repeat: {
every: payload.interval,
diff --git a/Server/service/networkService.js b/Server/service/networkService.js
index f5c333a60..863349dbf 100644
--- a/Server/service/networkService.js
+++ b/Server/service/networkService.js
@@ -15,44 +15,60 @@ class NetworkService {
this.NETWORK_ERROR = 5000;
}
- async handleNotification(job, isAlive) {
+ async handleNotification(monitor, isAlive) {
try {
- const { _id } = job.data;
- const monitor = await this.db.getMonitorById(_id);
- if (monitor === null || monitor === undefined) {
- logger.error(`Null Monitor: ${_id}`, {
- method: "handleNotification",
- service: this.SERVICE_NAME,
- jobId: job.id,
- });
- return;
- }
+ let template =
+ isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
+ let status = isAlive === true ? "up" : "down";
- // If monitor status changes, update monitor status and send notification
- if (monitor.status !== isAlive) {
- monitor.status = !monitor.status;
- await monitor.save();
-
- let template =
- isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
- let status = isAlive === true ? "up" : "down";
-
- const notifications = await this.db.getNotificationsByMonitorId(_id);
- for (const notification of notifications) {
- if (notification.type === "email") {
- await this.emailService.buildAndSendEmail(
- template,
- { monitorName: monitor.name, monitorUrl: monitor.url },
- notification.address,
- `Monitor ${monitor.name} is ${status}`
- );
- }
+ const notifications = await this.db.getNotificationsByMonitorId(
+ monitor._id
+ );
+ for (const notification of notifications) {
+ if (notification.type === "email") {
+ await this.emailService.buildAndSendEmail(
+ template,
+ { monitorName: monitor.name, monitorUrl: monitor.url },
+ notification.address,
+ `Monitor ${monitor.name} is ${status}`
+ );
}
}
} catch (error) {
logger.error(error.message, {
method: "handleNotification",
service: this.SERVICE_NAME,
+ monitorId: monitor._id,
+ });
+ }
+ }
+
+ async handleStatusUpdate(job, isAlive) {
+ try {
+ const { _id } = job.data;
+ const monitor = await this.db.getMonitorById(_id);
+
+ if (monitor === null || monitor === undefined) {
+ logger.error(`Null Monitor: ${_id}`, {
+ method: "handleStatusUpdate",
+ service: this.SERVICE_NAME,
+ jobId: job.id,
+ });
+ return;
+ }
+ if (monitor.status === undefined || monitor.status !== isAlive) {
+ const oldStatus = monitor.status;
+ monitor.status = isAlive;
+ await monitor.save();
+
+ if (oldStatus !== undefined && oldStatus !== isAlive) {
+ this.handleNotification(monitor, isAlive);
+ }
+ }
+ } catch (error) {
+ logger.error(error.message, {
+ method: "handleStatusUpdate",
+ service: this.SERVICE_NAME,
jobId: job.id,
});
}
@@ -113,7 +129,7 @@ class NetworkService {
};
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
@@ -161,7 +177,7 @@ class NetworkService {
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
@@ -241,7 +257,7 @@ class NetworkService {
};
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index 629deddf5..d0ef24751 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -1,4 +1,5 @@
const joi = require("joi");
+const { normalize } = require("path");
//****************************************
// Custom Validators
@@ -177,6 +178,22 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
),
});
+const getMonitorStatsByIdParamValidation = joi.object({
+ monitorId: joi.string().required(),
+});
+const getMonitorStatsByIdQueryValidation = joi.object({
+ status: joi.string(),
+ limit: joi.number(),
+ sortOrder: joi.string().valid("asc", "desc"),
+ dateRange: joi.string().valid("day", "week", "month"),
+ numToDisplay: joi.number(),
+ normalize: joi.boolean(),
+});
+
+const getCertificateParamValidation = joi.object({
+ monitorId: joi.string().required(),
+});
+
const createMonitorBodyValidation = joi.object({
_id: joi.string(),
userId: joi.string().required(),
@@ -364,6 +381,9 @@ module.exports = {
getMonitorByIdQueryValidation,
getMonitorsByTeamIdValidation,
getMonitorsByTeamIdQueryValidation,
+ getMonitorStatsByIdParamValidation,
+ getMonitorStatsByIdQueryValidation,
+ getCertificateParamValidation,
getMonitorAggregateStatsParamValidation,
getMonitorAggregateStatsQueryValidation,
editMonitorBodyValidation,