Merge monitor-details-additional-data

This commit is contained in:
Daniel Cojocea committed 2024-09-04 14:31:49 -04:00
commit 6bc25fc7da
20 files changed
+509 -425

No files matched your search

+6 -1
View File
@@ -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,
};
@@ -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
+18 -15
View File
@@ -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 (
<Stack className="configure-monitor" gap={theme.spacing(12)}>
{Object.keys(monitor).length === 0 ? (
@@ -210,13 +222,7 @@ const Configure = () => {
flex={1}
>
<Stack direction="row" gap={theme.spacing(2)}>
<PulseDot
color={
monitor?.status
? theme.palette.success.main
: theme.palette.error.main
}
/>
<PulseDot color={statusColor[monitor?.status ?? undefined]} />
<Box>
{parsedUrl?.host ? (
<Typography
@@ -233,13 +239,9 @@ const Configure = () => {
<Typography
component="span"
lineHeight={theme.spacing(12)}
sx={{
color: monitor?.status
? theme.palette.success.main
: theme.palette.error.text,
}}
sx={{ color: statusColor[monitor?.status ?? undefined] }}
>
Your site is {monitor?.status ? "up" : "down"}.
{statusMsg[monitor?.status ?? undefined]}
</Typography>
</Box>
<Box
@@ -275,14 +277,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>
+16 -77
View File
@@ -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 (
<>
<Skeleton variant="rounded" width="20%" height={34} />
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
<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} />
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.spacing(4) }}
/>
</Box>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(12)}
>
<Skeleton variant="rounded" width="100%" height={80} />
<Skeleton variant="rounded" width="100%" height={80} />
<Skeleton variant="rounded" width="100%" height={80} />
</Stack>
<Box>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="20%"
height={24}
sx={{ alignSelf: "flex-end" }}
/>
<Skeleton variant="rounded" width="20%" height={34} />
</Stack>
<Box sx={{ height: "200px" }}>
<Skeleton variant="rounded" width="100%" height="100%" />
</Box>
</Box>
<Stack gap={theme.spacing(8)}>
<Skeleton variant="rounded" width="20%" height={24} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={50} />
</Stack>
</Stack>
</>
);
};
/**
* 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 (
<Box className="monitor-details">
{loading ? (
@@ -267,7 +210,7 @@ const DetailsPage = ({ isAdmin }) => {
gap={theme.spacing(2)}
>
<Tooltip
title={`Your site is ${monitor?.status ? "up" : "down"}.`}
title={statusMsg[monitor?.status ?? undefined]}
disableInteractive
slotProps={{
popper: {
@@ -284,11 +227,7 @@ const DetailsPage = ({ isAdmin }) => {
>
<Box>
<PulseDot
color={
monitor?.status
? theme.palette.success.main
: theme.palette.error.main
}
color={statusColor[monitor?.status ?? undefined]}
/>
</Box>
</Tooltip>
@@ -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 (
<>
<Skeleton variant="rounded" width="20%" height={34} />
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
<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} />
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.spacing(4) }}
/>
</Box>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(12)}
>
<Skeleton variant="rounded" width="100%" height={80} />
<Skeleton variant="rounded" width="100%" height={80} />
<Skeleton variant="rounded" width="100%" height={80} />
</Stack>
<Box>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="20%"
height={24}
sx={{ alignSelf: "flex-end" }}
/>
<Skeleton variant="rounded" width="20%" height={34} />
</Stack>
<Box sx={{ height: "200px" }}>
<Skeleton variant="rounded" width="100%" height="100%" />
</Box>
</Box>
<Stack gap={theme.spacing(8)}>
<Skeleton variant="rounded" width="20%" height={24} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={50} />
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;
@@ -66,7 +66,7 @@ export const buildData = (monitors, isAdmin, navigate) => {
percentageColor,
status:
monitor.status === undefined
? "unknown"
? "pending"
: monitor.status === true
? "up"
: "down",
+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>
+7 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+74 -178
View File
@@ -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<Boolean>} 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<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
@@ -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,
+4 -1
View File
@@ -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 });
};
+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
@@ -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);
}
}
+20
View File
@@ -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,