Merge branch 'develop' into fix/missing-be-validation

This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-09-03 20:40:52 -07:00
commit e2529612d9
18 files changed
+448 -158

No files matched your search

+7 -1
View File
@@ -126,6 +126,7 @@ ColoredLabel.propTypes = {
*/
const StatusLabel = ({ status, text, customStyles }) => {
console.log(status);
const theme = useTheme();
const colors = {
up: {
@@ -138,6 +139,11 @@ const StatusLabel = ({ status, text, customStyles }) => {
bgColor: theme.palette.error.bg,
borderColor: theme.palette.error.light,
},
unknown: {
dotColor: theme.palette.unresolved.main,
bgColor: theme.palette.unresolved.bg,
borderColor: theme.palette.unresolved.light,
},
"cannot resolve": {
dotColor: theme.palette.unresolved.main,
bgColor: theme.palette.unresolved.bg,
@@ -170,7 +176,7 @@ const StatusLabel = ({ status, text, customStyles }) => {
};
StatusLabel.propTypes = {
status: PropTypes.oneOf(["up", "down", "cannot resolve"]),
status: PropTypes.oneOf(["up", "down", "unknown", "cannot resolve"]),
text: PropTypes.string,
customStyles: PropTypes.object,
};
@@ -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 = () => {
</Button>
</ButtonGroup>
</Stack>
<Button
<LoadingButton
loading={isSendingInvite}
variant="contained"
color="primary"
onClick={() => setIsOpen(true)}
>
Invite a team member
</Button>
</LoadingButton>
</Stack>
<BasicTable
data={tableData}
@@ -389,14 +384,19 @@ const TeamPanel = () => {
mt={theme.spacing(8)}
justifyContent="flex-end"
>
<Button variant="text" color="info" onClick={closeInviteModal}>
<LoadingButton
loading={isSendingInvite}
variant="text"
color="info"
onClick={closeInviteModal}
>
Cancel
</Button>
</LoadingButton>
<LoadingButton
variant="contained"
color="primary"
onClick={handleInviteMember}
loading={false}
loading={isSendingInvite}
disabled={Object.keys(errors).length !== 0}
>
Send invite
@@ -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";
});
},
});
@@ -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
@@ -275,14 +275,15 @@ const Configure = () => {
</>
)}
</LoadingButton>
<Button
<LoadingButton
loading={isLoading}
variant="contained"
color="error"
sx={{ px: theme.spacing(8) }}
onClick={() => setIsOpen(true)}
>
Remove
</Button>
</LoadingButton>
</Box>
</Stack>
<ConfigBox>
+63 -70
View File
@@ -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 (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.spacing(4) }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</Box>
<Stack
direction="row"
gap={theme.spacing(6)}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton variant="rounded" width={100} height={34} />
<Skeleton variant="rounded" width={100} height={34} />
</Stack>
</Stack>
<Skeleton variant="rounded" width="100%" height={500} />
<Stack direction="row" justifyContent="flex-end">
<Skeleton variant="rounded" width="15%" height={34} />
</Stack>
</Stack>
</>
);
};
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 (
<Stack className="configure-pagespeed" gap={theme.spacing(12)}>
{loading ? (
{Object.keys(monitor).length === 0 ? (
<SkeletonLayout />
) : (
<>
@@ -195,7 +175,9 @@ const PageSpeedConfigure = () => {
</Typography>
</Box>
<Box alignSelf="flex-end" ml="auto">
<Button
<LoadingButton
onClick={handlePause}
loading={isLoading}
variant="contained"
color="secondary"
sx={{
@@ -210,10 +192,20 @@ const PageSpeedConfigure = () => {
},
}}
>
<PauseCircleOutlineIcon />
Pause
</Button>
<Button
{monitor?.isActive ? (
<>
<PauseCircleOutlineIcon />
Pause
</>
) : (
<>
<PlayCircleOutlineRoundedIcon />
Resume
</>
)}
</LoadingButton>
<LoadingButton
loading={isLoading}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
@@ -222,7 +214,7 @@ const PageSpeedConfigure = () => {
}}
>
Remove
</Button>
</LoadingButton>
</Box>
</Stack>
<Stack
@@ -322,7 +314,8 @@ const PageSpeedConfigure = () => {
</Stack>
</Stack>
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
<LoadingButton
loading={isLoading}
type="submit"
variant="contained"
color="primary"
@@ -330,7 +323,7 @@ const PageSpeedConfigure = () => {
sx={{ px: theme.spacing(12) }}
>
Save
</Button>
</LoadingButton>
</Stack>
</Stack>
</>
@@ -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 (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.spacing(4) }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</Box>
<Stack
direction="row"
gap={theme.spacing(6)}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton variant="rounded" width={100} height={34} />
<Skeleton variant="rounded" width={100} height={34} />
</Stack>
</Stack>
<Skeleton variant="rounded" width="100%" height={500} />
<Stack direction="row" justifyContent="flex-end">
<Skeleton variant="rounded" width="15%" height={34} />
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;
@@ -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 = () => {
</Stack>
</Stack>
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
<LoadingButton
loading={isLoading}
type="submit"
variant="contained"
color="primary"
@@ -234,7 +237,7 @@ const CreatePageSpeed = () => {
sx={{ px: theme.spacing(12), mt: theme.spacing(12) }}
>
Create
</Button>
</LoadingButton>
</Stack>
</Stack>
</Stack>
+6 -2
View File
@@ -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 }) => {
</Box>
</ConfigBox>
<Stack direction="row" justifyContent="flex-end">
<Button
<LoadingButton
loading={false}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
>
Save
</Button>
</LoadingButton>
</Stack>
</Stack>
</Box>
+1 -1
View File
@@ -116,7 +116,7 @@ class NetworkService {
* @param {string} [sortOrder] - The order in which to sort the retrieved statistics.
* @param {number} [limit] - The maximum number of statistics to retrieve.
* @param {string} [dateRange] - The date range for which to retrieve statistics.
* @param {number} [numToDisplay] - The number of statistics to display.
* @param {number} [numToDisplay] - The number of checks to display.
* @param {boolean} [normalize] - Whether to normalize the retrieved statistics.
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*/
+42
View File
@@ -9,6 +9,8 @@ const {
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
getCertificateParamValidation,
getMonitorAggregateStatsParamValidation,
getMonitorAggregateStatsQueryValidation,
} = require("../validation/joi");
const sslChecker = require("ssl-checker");
@@ -37,6 +39,44 @@ const getAllMonitors = async (req, res, next) => {
}
};
/**
* Returns agregate stats for a monitor
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
const getMonitorAggregateStats = async (req, res, next) => {
try {
await getMonitorAggregateStatsParamValidation.validateAsync(req.params);
await getMonitorAggregateStatsQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const { monitorId } = req.params;
const dateRange = req.query.dateRange;
const aggregateStats = await req.db.getMonitorAggregateStats(
monitorId,
dateRange
);
return res.json({
success: true,
msg: successMessages.MONTIOR_STATS_BY_ID,
data: aggregateStats,
});
} catch (error) {
next(error);
}
};
/**
* Returns monitor stats for monitor with matching ID
* @async
@@ -362,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,
@@ -379,6 +420,7 @@ const pauseMonitor = async (req, res, next) => {
module.exports = {
getAllMonitors,
getMonitorAggregateStats,
getMonitorStatsById,
getMonitorCertificate,
getMonitorById,
+2
View File
@@ -64,6 +64,7 @@ const {
const {
getAllMonitors,
getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
@@ -144,6 +145,7 @@ module.exports = {
resetPassword,
checkSuperadmin,
getAllMonitors,
getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
+94 -5
View File
@@ -143,6 +143,87 @@ const getStatusBarValues = (monitor, checks) => {
}
return statusBarValues.reverse();
};
/**
* Get aggregate monitor stats for charts
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Monitor>}
* @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
* @async
@@ -200,6 +281,10 @@ const getMonitorStatsById = async (req) => {
...checksQuery,
createdAt: { $gte: filterLookup.day },
};
const checksQuery7Days = {
...checksQuery,
createdAt: { $gte: filterLookup.week },
};
const checksQuery30Days = {
...checksQuery,
createdAt: { $gte: filterLookup.month },
@@ -209,15 +294,18 @@ const getMonitorStatsById = async (req) => {
createdAt: { $gte: filterLookup.hour },
};
const [checks24Hours, checks30Days, checks60Mins] = await Promise.all([
model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
]);
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);
@@ -452,6 +540,7 @@ const editMonitor = async (candidateId, candidateMonitor) => {
module.exports = {
getAllMonitors,
getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
+2 -2
View File
@@ -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: [
{
+1
View File
@@ -3,6 +3,7 @@ const monitorController = require("../controllers/monitorController");
const { isAllowed } = require("../middleware/isAllowed");
router.get("/", monitorController.getAllMonitors);
router.get("/aggregate/:monitorId", monitorController.getMonitorAggregateStats);
router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
router.get("/certificate/:monitorId", monitorController.getMonitorCertificate);
router.get("/:monitorId", monitorController.getMonitorById);
-1
View File
@@ -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,
+49 -33
View File
@@ -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);
}
}
+9
View File
@@ -218,6 +218,13 @@ const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),
});
const getMonitorAggregateStatsParamValidation = joi.object({
monitorId: joi.string().required(),
});
const getMonitorAggregateStatsQueryValidation = joi.object({
dateRange: joi.string().valid("day", "week", "month"),
});
//****************************************
// Alerts
//****************************************
@@ -377,6 +384,8 @@ module.exports = {
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
getCertificateParamValidation,
getMonitorAggregateStatsParamValidation,
getMonitorAggregateStatsQueryValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
editUserParamValidation,