Merge monitor-details-additional-data
This commit is contained in:
commit
6bc25fc7da
20 files changed
+509
-425
No files matched your search
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user