20 files changed
+1398
-832
No files matched your search
@@ -9,7 +9,7 @@ import {
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
@@ -73,6 +73,10 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [data]);
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { AreaChart, Area, XAxis, Tooltip, ResponsiveContainer } from "recharts";
|
||||
import { NormalizeData } from "../ChartUtils";
|
||||
import "./index.css";
|
||||
|
||||
const CustomToolTip = ({ active, payload, label }) => {
|
||||
@@ -28,7 +27,7 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const MonitorDetailsAreaChart = ({ checks, filter }) => {
|
||||
const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
// SQUASH ERROR, NOT PERMANENT SOLUTION
|
||||
const error = console.error;
|
||||
console.error = (...args) => {
|
||||
@@ -47,45 +46,12 @@ const MonitorDetailsAreaChart = ({ checks, filter }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const filterChecks = (checks, filter, numToDisplay) => {
|
||||
const limits = {
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
week: 24 * 60 * 60 * 1000 * 7,
|
||||
month: 24 * 60 * 60 * 1000 * 30, //TODO better monthly calculations
|
||||
};
|
||||
|
||||
const now = new Date().getTime();
|
||||
let result = [];
|
||||
for (let i = 0; i < checks.length; i++) {
|
||||
const checkTime = new Date(checks[i].createdAt).getTime();
|
||||
if (now - checkTime < limits[filter]) {
|
||||
result.push(checks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// If more than numToDisplay checks, pick every nth check
|
||||
if (result.length > numToDisplay) {
|
||||
const n = Math.ceil(result.length / numToDisplay);
|
||||
result = result.filter((_, idx) => {
|
||||
return idx % n === 0;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
let normalizedChecks = [];
|
||||
if (checks && checks.length > 0) {
|
||||
const filteredChecks = filterChecks(checks, filter, 75);
|
||||
normalizedChecks = NormalizeData(filteredChecks, 10, 100);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
width={500}
|
||||
height={400}
|
||||
data={normalizedChecks}
|
||||
data={checks}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 0,
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import "./index.css";
|
||||
import PropTypes from "prop-types";
|
||||
import { BarChart, Bar, ResponsiveContainer, Cell } from "recharts";
|
||||
import { NormalizeData } from "../ChartUtils";
|
||||
|
||||
const ResponseTimeChart = ({ checks = [] }) => {
|
||||
const normalizedChecks = NormalizeData(checks, 1, 100);
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
width={150}
|
||||
height={40}
|
||||
data={normalizedChecks}
|
||||
data={checks}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Bar maxBarSize={10} dataKey="responseTime">
|
||||
{normalizedChecks.map((check, index) => (
|
||||
{checks.map((check, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={
|
||||
|
||||
@@ -51,7 +51,7 @@ export const getUptimeMonitorsByUserId = createAsyncThunk(
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${user._id}?limit=25&type=http&type=ping&sortOrder=desc`,
|
||||
`/monitors/user/${user._id}?limit=25&type=http&type=ping&sortOrder=desc&normalize=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Typography, ButtonGroup, Stack, Box } from "@mui/material";
|
||||
import { Typography, ButtonGroup, Stack, Box, Skeleton } from "@mui/material";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import Button from "../../Components/Button";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
@@ -12,12 +12,39 @@ import Select from "../../Components/Inputs/Select";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ ml: "auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Incidents = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [monitors, setMonitors] = useState({});
|
||||
const [selectedMonitor, setSelectedMonitor] = useState("0");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// TODO do something with these filters
|
||||
const [filter, setFilter] = useState("all");
|
||||
@@ -34,6 +61,7 @@ const Incidents = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIncidents = async () => {
|
||||
setLoading(true);
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${authState.user._id}?status=false`,
|
||||
{
|
||||
@@ -51,6 +79,7 @@ const Incidents = () => {
|
||||
}, {});
|
||||
setMonitors(monitorLookup);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
fetchIncidents();
|
||||
@@ -92,8 +121,8 @@ const Incidents = () => {
|
||||
data: <StatusLabel status="down" text="Down" />,
|
||||
},
|
||||
{ id: idx + 1, data: new Date(incident.createdAt).toLocaleString() },
|
||||
{ id: idx + 2, data: incident.statusCode },
|
||||
{ id: idx + 3, data: monitors[incident.monitorId].name },
|
||||
{ id: idx + 2, data: monitors[incident.monitorId].name },
|
||||
{ id: idx + 3, data: incident.statusCode },
|
||||
],
|
||||
};
|
||||
});
|
||||
@@ -102,7 +131,9 @@ const Incidents = () => {
|
||||
return (
|
||||
<>
|
||||
<Stack className="incidents" gap={theme.gap.large}>
|
||||
{hasAnyIncidents ? (
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : hasAnyIncidents ? (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
|
||||
<Typography component="h1">Incident history for: </Typography>
|
||||
@@ -155,8 +186,7 @@ const Incidents = () => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Stack alignItems="center" mt={theme.gap.xxl} gap={theme.gap.small}>
|
||||
<Typography>No incidents, yet.</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
@@ -165,15 +195,25 @@ const Incidents = () => {
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
mb: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Box
|
||||
padding={theme.gap.xxl}
|
||||
gap={theme.gap.small}
|
||||
border={1}
|
||||
borderRadius={`${theme.shape.borderRadius}px`}
|
||||
borderColor={theme.palette.otherColors.graishWhite}
|
||||
backgroundColor={theme.palette.otherColors.white}
|
||||
>
|
||||
<Typography textAlign="center">No incidents, yet.</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect, useState } from "react";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import { Box, Modal, Stack, Typography } from "@mui/material";
|
||||
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
|
||||
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
|
||||
@@ -36,6 +36,54 @@ const parseUrl = (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.gap.small }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.medium}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure page displays monitor configurations and allows for editing actions.
|
||||
* @component
|
||||
@@ -47,7 +95,7 @@ const Configure = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.uptimeMonitors);
|
||||
const [monitor, setMonitor] = useState();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [errors, setErrors] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
@@ -164,214 +212,228 @@ const Configure = () => {
|
||||
|
||||
var lastChecked = formatDurationRounded(getLastChecked(monitor?.checks));
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
return (
|
||||
<Box className="configure-monitor">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<form className="configure-monitor-form" noValidate spellCheck="false">
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
{parsedUrl?.host ? (
|
||||
<Typography
|
||||
component="h1"
|
||||
mb={theme.gap.small}
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{parsedUrl.host || "..."}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Typography lineHeight={parsedUrl?.host ? 1 : theme.gap.large}>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: monitor?.status
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-24)",
|
||||
}}
|
||||
>
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {duration}.{" "}
|
||||
{lastChecked ? `Last time checked ${lastChecked} ago.` : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.medium}
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: "#f4f4f4",
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<form
|
||||
className="configure-monitor-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
style={{
|
||||
marginTop: theme.gap.medium,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Pause"
|
||||
animate="rotate180"
|
||||
img={<PauseCircleOutlineIcon />}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
pl: theme.gap.small,
|
||||
pr: theme.gap.medium,
|
||||
"& svg": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="error"
|
||||
label="Remove"
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
px: theme.gap.ml,
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p" sx={{ mt: theme.gap.small }}>
|
||||
Here you can select the URL of the host, together with the type of
|
||||
monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Field
|
||||
type={monitor?.type === "http" ? "url" : "text"}
|
||||
https={protocol === "https"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
placeholder="google.com"
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Friendly name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.medium}>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={
|
||||
monitor?.notifications?.some(
|
||||
(notification) => notification.type === "email"
|
||||
) || false
|
||||
}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor?.notifications?.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={`calc(${theme.gap.ml} * 2)`}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
{parsedUrl?.host ? (
|
||||
<Typography
|
||||
component="h1"
|
||||
mb={theme.gap.small}
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{parsedUrl.host || "..."}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Typography lineHeight={parsedUrl?.host ? 1 : theme.gap.large}>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: monitor?.status
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-24)",
|
||||
}}
|
||||
>
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {duration}.{" "}
|
||||
{lastChecked ? `Last time checked ${lastChecked} ago.` : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Select
|
||||
id="monitor-interval-configure"
|
||||
label="Check frequency"
|
||||
value={monitor?.interval / MS_PER_MINUTE || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
level="primary"
|
||||
label="Save"
|
||||
sx={{ px: theme.gap.large }}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.medium}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Pause"
|
||||
animate="rotate180"
|
||||
img={<PauseCircleOutlineIcon />}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
pl: theme.gap.small,
|
||||
pr: theme.gap.medium,
|
||||
"& svg": {
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="error"
|
||||
label="Remove"
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
px: theme.gap.ml,
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p" sx={{ mt: theme.gap.small }}>
|
||||
Here you can select the URL of the host, together with the
|
||||
type of monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Field
|
||||
type={monitor?.type === "http" ? "url" : "text"}
|
||||
https={protocol === "https"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
placeholder="google.com"
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Friendly name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.medium}>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={
|
||||
monitor?.notifications?.some(
|
||||
(notification) => notification.type === "email"
|
||||
) || false
|
||||
}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor?.notifications?.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={`calc(${theme.gap.ml} * 2)`}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Select
|
||||
id="monitor-interval-configure"
|
||||
label="Check frequency"
|
||||
value={monitor?.interval / MS_PER_MINUTE || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
level="primary"
|
||||
label="Save"
|
||||
sx={{ px: theme.gap.large }}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-monitor"
|
||||
aria-describedby="delete-monitor-confirmation"
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TablePagination,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import axiosInstance from "../../../../Utils/axiosConfig";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
|
||||
const PaginationTable = ({ monitorId, filter }) => {
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
const [paginationController, setPaginationController] = useState({
|
||||
page: 0,
|
||||
rowsPerPage: 5,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/checks/${monitorId}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [
|
||||
authToken,
|
||||
monitorId,
|
||||
filter,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
rowsPerPage: parseInt(event.target.value, 10),
|
||||
page: 0,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(check.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{check.statusCode}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
onPageChange={handlePageChange}
|
||||
page={paginationController.page}
|
||||
count={checksCount}
|
||||
rowsPerPage={paginationController.rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
PaginationTable.propTypes = {
|
||||
monitorId: PropTypes.string.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PaginationTable;
|
||||
@@ -42,9 +42,6 @@
|
||||
padding: var(--env-var-spacing-1) 18px;
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.monitor-details .stat-box:not(:first-of-type) {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
.monitor-details .stat-box h6.MuiTypography-root {
|
||||
margin-bottom: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import BasicTable from "../../../Components/BasicTable";
|
||||
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
@@ -13,6 +12,7 @@ import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
|
||||
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
|
||||
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
|
||||
import PaginationTable from "./PaginationTable";
|
||||
import {
|
||||
formatDuration,
|
||||
formatDurationRounded,
|
||||
@@ -30,72 +30,114 @@ const StatBox = ({ title, value }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="20%" height={34} />
|
||||
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
<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.gap.small }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.large}
|
||||
>
|
||||
<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.gap.ml}
|
||||
>
|
||||
<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.gap.ml}>
|
||||
<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
|
||||
*/
|
||||
const DetailsPage = () => {
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [data, setData] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const [filter, setFilter] = useState("day");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=asc&filter=${filter}&numToDisplay=50&normalize=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
setMonitor(res.data.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
}
|
||||
}, [authToken, monitorId, navigate, filter]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=asc`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
setMonitor(res.data.data);
|
||||
const data = {
|
||||
cols: [
|
||||
{ id: 1, name: "Status" },
|
||||
{ id: 2, name: "Date & Time" },
|
||||
{ id: 3, name: "Message" },
|
||||
],
|
||||
rows: res.data.data.checks.map((check, idx) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
|
||||
return {
|
||||
id: check._id,
|
||||
data: [
|
||||
{
|
||||
id: idx,
|
||||
data: (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: idx + 1,
|
||||
data: new Date(check.createdAt).toLocaleString(),
|
||||
},
|
||||
{ id: idx + 2, data: check.statusCode },
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
setData(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken]);
|
||||
}, [fetchMonitor]);
|
||||
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Function to calculate uptime duration based on the most recent check.
|
||||
@@ -158,135 +200,152 @@ const DetailsPage = () => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
return (
|
||||
<Box className="monitor-details">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to Monitors"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
<Typography component="h1" sx={{ lineHeight: 1 }}>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography mt={theme.gap.small}>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: monitor?.status
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-24)",
|
||||
}}
|
||||
>
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {formatDurationRounded(monitor?.interval)}. Last
|
||||
time checked{" "}
|
||||
{formatDurationRounded(getLastChecked(monitor?.checks))} ago.
|
||||
</Typography>
|
||||
</Box>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
animate="rotate90"
|
||||
img={
|
||||
<SettingsIcon
|
||||
style={{ width: theme.gap.mlplus, height: theme.gap.mlplus }}
|
||||
/>
|
||||
}
|
||||
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
|
||||
label="Back to Monitors"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: "#f4f4f4",
|
||||
px: theme.gap.medium,
|
||||
"& svg": {
|
||||
mr: "6px",
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<StatBox
|
||||
title="Currently up for"
|
||||
value={formatDuration(calculateUptimeDuration(monitor.checks))}
|
||||
/>
|
||||
<StatBox
|
||||
title="Last checked"
|
||||
value={`${formatDuration(getLastChecked(monitor.checks))} ago`}
|
||||
/>
|
||||
<StatBox title="Incidents" value={countIncidents(monitor.checks)} />
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.gap.ml}
|
||||
>
|
||||
<Typography component="h2" mb={theme.gap.small}>
|
||||
Response Times
|
||||
</Typography>
|
||||
<ButtonGroup>
|
||||
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
<Typography component="h1" sx={{ lineHeight: 1 }}>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography mt={theme.gap.small}>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: monitor?.status
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-24)",
|
||||
}}
|
||||
>
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
Last time checked{" "}
|
||||
{formatDurationRounded(getLastChecked(monitor?.checks))} ago.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Day"
|
||||
onClick={() => setFilter("day")}
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
animate="rotate90"
|
||||
img={
|
||||
<SettingsIcon
|
||||
style={{
|
||||
minWidth: theme.gap.mlplus,
|
||||
minHeight: theme.gap.mlplus,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "day" && theme.palette.otherColors.fillGray,
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: "#f4f4f4",
|
||||
px: theme.gap.medium,
|
||||
"& svg": {
|
||||
mr: "6px",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Week"
|
||||
onClick={() => setFilter("week")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "week" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.large}
|
||||
>
|
||||
<StatBox
|
||||
title="Currently up for"
|
||||
value={formatDuration(calculateUptimeDuration(monitor.checks))}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Month"
|
||||
onClick={() => setFilter("month")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "month" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
<StatBox
|
||||
title="Last checked"
|
||||
value={`${formatDuration(getLastChecked(monitor.checks))} ago`}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<StatBox
|
||||
title="Incidents"
|
||||
value={countIncidents(monitor.checks)}
|
||||
/>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.gap.ml}
|
||||
>
|
||||
<Typography component="h2" mb={theme.gap.small}>
|
||||
Response Times
|
||||
</Typography>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Day"
|
||||
onClick={() => setFilter("day")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "day" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Week"
|
||||
onClick={() => setFilter("week")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "week" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Month"
|
||||
onClick={() => setFilter("month")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "month" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<MonitorDetailsAreaChart
|
||||
checks={monitor.checks}
|
||||
filter={filter}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.ml}>
|
||||
<Typography component="h2">History</Typography>
|
||||
{/* TODO New Table */}
|
||||
<PaginationTable monitorId={monitorId} filter={filter} />
|
||||
{/* <BasicTable data={data} paginated={true} /> */}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<MonitorDetailsAreaChart checks={monitor.checks} filter={filter} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2" mb={theme.gap.ml}>
|
||||
History
|
||||
</Typography>
|
||||
<BasicTable data={data} paginated={true} reversed={true} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
export default DetailsPage;
|
||||
@@ -9,6 +9,13 @@
|
||||
.monitors h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.monitors p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.monitors h2:has(+ p),
|
||||
.monitors p.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.monitors .MuiStack-root > button:not(.MuiIconButton-root) {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
height: var(--env-var-height-2);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
@@ -71,6 +72,38 @@ const Host = ({ params }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Skeleton variant="rounded" width="50%" height={36} />
|
||||
<Skeleton variant="rounded" width="15%" height={36} />
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</Stack>
|
||||
<Stack gap={theme.gap.large} p={theme.gap.xl} backgroundColor="#f9fafb">
|
||||
<Skeleton variant="rounded" width="50%" height={25} />
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Monitors = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
@@ -112,7 +145,7 @@ const Monitors = () => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getUptimeMonitorsByUserId(authState.authToken));
|
||||
}, []);
|
||||
}, [authState.authToken, dispatch]);
|
||||
|
||||
const up = monitorState.monitors.reduce((acc, cur) => {
|
||||
return cur.status === true ? acc + 1 : acc;
|
||||
@@ -204,47 +237,89 @@ const Monitors = () => {
|
||||
};
|
||||
});
|
||||
|
||||
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="monitors" gap={theme.gap.large}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography component="h1">
|
||||
Hello, {authState.user.firstName}
|
||||
</Typography>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create new monitor"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<ServerStatus title="Up" value={up} state="up" />
|
||||
<ServerStatus title="Down" value={down} state="down" />
|
||||
<ServerStatus title="Paused" value={0} state="pause" />
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
p={theme.gap.xl}
|
||||
sx={{
|
||||
border: `solid 1px ${theme.palette.otherColors.graishWhite}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Typography component="h2">Current monitors</Typography>
|
||||
<Box className="current-monitors-counter">
|
||||
{monitorState.monitors.length}
|
||||
</Box>
|
||||
{/* TODO - add search bar */}
|
||||
</Stack>
|
||||
<BasicTable data={data} paginated={true} />
|
||||
</Stack>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography component="h1">
|
||||
Hello, {authState.user.firstName}
|
||||
</Typography>
|
||||
{monitorState.monitors?.length !== 0 && (
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create new monitor"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{monitorState.monitors?.length === 0 ? (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
backgroundColor={theme.palette.otherColors.white}
|
||||
p={theme.gap.xxl}
|
||||
gap={theme.gap.xs}
|
||||
border={1}
|
||||
borderRadius={`${theme.shape.borderRadius}px`}
|
||||
borderColor={theme.palette.otherColors.graishWhite}
|
||||
>
|
||||
<Typography component="h2">No monitors found</Typography>
|
||||
<Typography>
|
||||
It looks like you don’t have any monitors set up yet.
|
||||
</Typography>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create your first monitor"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ mt: theme.gap.large }}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<ServerStatus title="Up" value={up} state="up" />
|
||||
<ServerStatus title="Down" value={down} state="down" />
|
||||
<ServerStatus title="Paused" value={0} state="pause" />
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
p={theme.gap.xl}
|
||||
border={1}
|
||||
borderColor={theme.palette.otherColors.graishWhite}
|
||||
backgroundColor={theme.palette.otherColors.white}
|
||||
sx={{
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Typography component="h2">Current monitors</Typography>
|
||||
<Box className="current-monitors-counter">
|
||||
{monitorState.monitors.length}
|
||||
</Box>
|
||||
{/* TODO - add search bar */}
|
||||
</Stack>
|
||||
<BasicTable data={data} paginated={true} />
|
||||
</Stack>{" "}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Menu
|
||||
className="actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { PieChart } from "@mui/x-charts/PieChart";
|
||||
import { useDrawingArea } from "@mui/x-charts";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -126,6 +126,53 @@ const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack direction="row" gap={theme.gap.small}>
|
||||
<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.gap.small }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xl}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Skeleton variant="rounded" width="30%" height={90} sx={{ flex: 1 }} />
|
||||
<Skeleton variant="rounded" width="30%" height={90} sx={{ flex: 1 }} />
|
||||
<Skeleton variant="rounded" width="30%" height={90} sx={{ flex: 1 }} />
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="25%" height={24} />
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="25%" height={24} />
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PageSpeedDetails = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
@@ -136,17 +183,22 @@ const PageSpeedDetails = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=desc`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/${monitorId}?sortOrder=desc`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setMonitor(res.data.data);
|
||||
setAudits(res.data.data.checks[0].audits);
|
||||
setMonitor(res.data.data);
|
||||
setAudits(res.data.data.checks[0].audits);
|
||||
} catch (error) {
|
||||
console.error("Error fetching pagespeed monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
}
|
||||
};
|
||||
|
||||
fetchMonitor();
|
||||
@@ -244,360 +296,373 @@ const PageSpeedDetails = () => {
|
||||
const [highlightedItem, setHighLightedItem] = useState(null);
|
||||
const [expand, setExpand] = useState(false);
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="page-speed-details" gap={theme.gap.large}>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate("/pagespeed")}
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<GreenCheck />
|
||||
<Box>
|
||||
<Typography component="h1" mb={theme.gap.xs} sx={{ lineHeight: 1 }}>
|
||||
{monitor?.url}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ color: "var(--env-var-color-17)" }}
|
||||
>
|
||||
Your pagespeed monitor is live.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
animate="rotate90"
|
||||
img={
|
||||
<SettingsIcon
|
||||
style={{ width: theme.gap.mlplus, height: theme.gap.mlplus }}
|
||||
/>
|
||||
}
|
||||
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
px: theme.gap.medium,
|
||||
"& svg": {
|
||||
mr: "6px",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xl}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatBox
|
||||
icon={<LastCheckedIcon />}
|
||||
title="Last checked"
|
||||
value={
|
||||
<>
|
||||
{formatDate(getLastChecked(monitor?.checks, false))}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
fontStyle="italic"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
({formatDurationRounded(getLastChecked(monitor?.checks))} ago)
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
icon={<ClockIcon />}
|
||||
title="Checks since"
|
||||
value={
|
||||
<>
|
||||
{formatDate(new Date(monitor?.createdAt))}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
fontStyle="italic"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
(
|
||||
{formatDurationRounded(
|
||||
new Date() - new Date(monitor?.createdAt)
|
||||
)}{" "}
|
||||
ago)
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
></StatBox>
|
||||
<StatBox
|
||||
icon={<IntervalCheckIcon />}
|
||||
title="Checks every"
|
||||
value={formatDurationRounded(monitor?.interval)}
|
||||
></StatBox>
|
||||
</Stack>
|
||||
<Typography component="h2">Score history</Typography>
|
||||
<Box height="300px">
|
||||
<PageSpeedLineChart pageSpeedChecks={monitor?.checks?.slice(0, 25)} />
|
||||
</Box>
|
||||
<Typography component="h2">Performance report</Typography>
|
||||
<Stack direction="row" alignItems="center" overflow="hidden">
|
||||
<Stack
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
minWidth="300px"
|
||||
flex={1}
|
||||
px={theme.gap.xl}
|
||||
py={theme.gap.ml}
|
||||
>
|
||||
<Box onMouseLeave={() => setExpand(false)}>
|
||||
{expand ? (
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: 100,
|
||||
color: colorMap.bg,
|
||||
},
|
||||
],
|
||||
outerRadius: 67,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
...pieData,
|
||||
]}
|
||||
width={pieSize.width}
|
||||
height={pieSize.height}
|
||||
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
|
||||
onHighlightChange={setHighLightedItem}
|
||||
slotProps={{
|
||||
legend: { hidden: true },
|
||||
}}
|
||||
tooltip={{ trigger: "none" }}
|
||||
sx={{
|
||||
"&:has(.MuiPieArcLabel-faded) .pie-label": {
|
||||
fill: "rgba(0,0,0,0) !important",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PieCenterLabel
|
||||
value={performance}
|
||||
color={colorMap.text}
|
||||
setExpand={setExpand}
|
||||
/>
|
||||
{pieData?.map((pie) => (
|
||||
<PieValueLabel
|
||||
key={pie.id}
|
||||
value={Math.round(pie.data[0].value * 10) / 10}
|
||||
startAngle={pie.startAngle}
|
||||
endAngle={pie.endAngle}
|
||||
color={pie.data[0].color}
|
||||
highlighted={highlightedItem?.seriesId === pie.id}
|
||||
/>
|
||||
))}
|
||||
</PieChart>
|
||||
) : (
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: 100,
|
||||
color: colorMap.bg,
|
||||
},
|
||||
],
|
||||
outerRadius: 67,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: performance,
|
||||
color: colorMap.stroke,
|
||||
},
|
||||
],
|
||||
innerRadius: 63,
|
||||
outerRadius: 70,
|
||||
paddingAngle: 5,
|
||||
cornerRadius: 2,
|
||||
startAngle: 0,
|
||||
endAngle: (performance / 100) * 360,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
]}
|
||||
width={pieSize.width}
|
||||
height={pieSize.height}
|
||||
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
|
||||
tooltip={{ trigger: "none" }}
|
||||
>
|
||||
<PieCenterLabel
|
||||
value={performance}
|
||||
color={colorMap.text}
|
||||
setExpand={setExpand}
|
||||
/>
|
||||
</PieChart>
|
||||
)}
|
||||
</Box>
|
||||
<Typography mt={theme.gap.medium}>
|
||||
Values are estimated and may vary.{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 500,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
See calculator
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Box
|
||||
px={theme.gap.xl}
|
||||
py={theme.gap.ml}
|
||||
height="100%"
|
||||
flex={1}
|
||||
sx={{
|
||||
borderLeft: `solid 1px ${theme.palette.otherColors.graishWhite}`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
mb={theme.gap.medium}
|
||||
pb={theme.gap.ml}
|
||||
color={theme.palette.secondary.main}
|
||||
textAlign="center"
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate("/pagespeed")}
|
||||
sx={{
|
||||
borderBottom: `solid 1px ${theme.palette.otherColors.graishWhite}`,
|
||||
borderBottomStyle: "dashed",
|
||||
width: "fit-content",
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
>
|
||||
The{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
/>
|
||||
<Stack direction="row" gap={theme.gap.small}>
|
||||
<GreenCheck />
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
mb={theme.gap.xs}
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{monitor?.url}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ color: "var(--env-var-color-17)" }}
|
||||
>
|
||||
Your pagespeed monitor is live.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
animate="rotate90"
|
||||
img={
|
||||
<SettingsIcon
|
||||
style={{ width: theme.gap.mlplus, height: theme.gap.mlplus }}
|
||||
/>
|
||||
}
|
||||
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 500,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: theme.palette.otherColors.fillGray,
|
||||
px: theme.gap.medium,
|
||||
"& svg": {
|
||||
mr: "6px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
performance score is calculated
|
||||
</Typography>{" "}
|
||||
directly from these{" "}
|
||||
<Typography component="span" fontWeight={600}>
|
||||
metrics
|
||||
</Typography>
|
||||
.
|
||||
</Typography>
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xl}
|
||||
flexWrap="wrap"
|
||||
pt={theme.gap.ml}
|
||||
gap={theme.gap.ml}
|
||||
>
|
||||
{Object.keys(audits).map((key) => {
|
||||
if (key === "_id") return;
|
||||
|
||||
let audit = audits[key];
|
||||
let metricParams = getColors(audit.score * 100);
|
||||
|
||||
let shape = (
|
||||
<Box
|
||||
sx={{
|
||||
width: theme.gap.medium,
|
||||
height: theme.gap.medium,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: metricParams.stroke,
|
||||
}}
|
||||
></Box>
|
||||
);
|
||||
if (metricParams.shape === "square")
|
||||
shape = (
|
||||
<Box
|
||||
sx={{
|
||||
width: theme.gap.medium,
|
||||
height: theme.gap.medium,
|
||||
backgroundColor: metricParams.stroke,
|
||||
}}
|
||||
></Box>
|
||||
);
|
||||
else if (metricParams.shape === "triangle")
|
||||
shape = (
|
||||
<Box
|
||||
sx={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
ml: `calc((${theme.gap.medium} - ${theme.gap.small}) / -2)`,
|
||||
borderLeft: `${theme.gap.small} solid transparent`,
|
||||
borderRight: `${theme.gap.small} solid transparent`,
|
||||
borderBottom: `${theme.gap.medium} solid ${metricParams.stroke}`,
|
||||
}}
|
||||
></Box>
|
||||
);
|
||||
|
||||
// Find the position where the number ends and the unit begins
|
||||
const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
|
||||
let value;
|
||||
let unit;
|
||||
if (match) {
|
||||
value = match[1];
|
||||
unit = match[2];
|
||||
} else {
|
||||
value = audit.displayValue;
|
||||
<StatBox
|
||||
icon={<LastCheckedIcon />}
|
||||
title="Last checked"
|
||||
value={
|
||||
<>
|
||||
{formatDate(getLastChecked(monitor?.checks, false))}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
fontStyle="italic"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
({formatDurationRounded(getLastChecked(monitor?.checks))}{" "}
|
||||
ago)
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="metric"
|
||||
key={`${key}-box`}
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
{shape}
|
||||
<Box>
|
||||
<Typography sx={{ lineHeight: 1 }}>
|
||||
{audit.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: metricParams.text,
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
<Typography
|
||||
component="span"
|
||||
ml="2px"
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
{unit}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
<StatBox
|
||||
icon={<ClockIcon />}
|
||||
title="Checks since"
|
||||
value={
|
||||
<>
|
||||
{formatDate(new Date(monitor?.createdAt))}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
fontStyle="italic"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
(
|
||||
{formatDurationRounded(
|
||||
new Date() - new Date(monitor?.createdAt)
|
||||
)}{" "}
|
||||
ago)
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
></StatBox>
|
||||
<StatBox
|
||||
icon={<IntervalCheckIcon />}
|
||||
title="Checks every"
|
||||
value={formatDurationRounded(monitor?.interval)}
|
||||
></StatBox>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Typography component="h2">Score history</Typography>
|
||||
<Box height="300px">
|
||||
<PageSpeedLineChart
|
||||
pageSpeedChecks={monitor?.checks?.slice(0, 25)}
|
||||
/>
|
||||
</Box>
|
||||
<Typography component="h2">Performance report</Typography>
|
||||
<Stack direction="row" alignItems="center" overflow="hidden">
|
||||
<Stack
|
||||
alignItems="center"
|
||||
textAlign="center"
|
||||
minWidth="300px"
|
||||
flex={1}
|
||||
px={theme.gap.xl}
|
||||
py={theme.gap.ml}
|
||||
>
|
||||
<Box onMouseLeave={() => setExpand(false)}>
|
||||
{expand ? (
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: 100,
|
||||
color: colorMap.bg,
|
||||
},
|
||||
],
|
||||
outerRadius: 67,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
...pieData,
|
||||
]}
|
||||
width={pieSize.width}
|
||||
height={pieSize.height}
|
||||
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
|
||||
onHighlightChange={setHighLightedItem}
|
||||
slotProps={{
|
||||
legend: { hidden: true },
|
||||
}}
|
||||
tooltip={{ trigger: "none" }}
|
||||
sx={{
|
||||
"&:has(.MuiPieArcLabel-faded) .pie-label": {
|
||||
fill: "rgba(0,0,0,0) !important",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PieCenterLabel
|
||||
value={performance}
|
||||
color={colorMap.text}
|
||||
setExpand={setExpand}
|
||||
/>
|
||||
{pieData?.map((pie) => (
|
||||
<PieValueLabel
|
||||
key={pie.id}
|
||||
value={Math.round(pie.data[0].value * 10) / 10}
|
||||
startAngle={pie.startAngle}
|
||||
endAngle={pie.endAngle}
|
||||
color={pie.data[0].color}
|
||||
highlighted={highlightedItem?.seriesId === pie.id}
|
||||
/>
|
||||
))}
|
||||
</PieChart>
|
||||
) : (
|
||||
<PieChart
|
||||
series={[
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: 100,
|
||||
color: colorMap.bg,
|
||||
},
|
||||
],
|
||||
outerRadius: 67,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: performance,
|
||||
color: colorMap.stroke,
|
||||
},
|
||||
],
|
||||
innerRadius: 63,
|
||||
outerRadius: 70,
|
||||
paddingAngle: 5,
|
||||
cornerRadius: 2,
|
||||
startAngle: 0,
|
||||
endAngle: (performance / 100) * 360,
|
||||
cx: pieSize.width / 2,
|
||||
},
|
||||
]}
|
||||
width={pieSize.width}
|
||||
height={pieSize.height}
|
||||
margin={{ left: 0, top: 0, right: 0, bottom: 0 }}
|
||||
tooltip={{ trigger: "none" }}
|
||||
>
|
||||
<PieCenterLabel
|
||||
value={performance}
|
||||
color={colorMap.text}
|
||||
setExpand={setExpand}
|
||||
/>
|
||||
</PieChart>
|
||||
)}
|
||||
</Box>
|
||||
<Typography mt={theme.gap.medium}>
|
||||
Values are estimated and may vary.{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 500,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
See calculator
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Box
|
||||
px={theme.gap.xl}
|
||||
py={theme.gap.ml}
|
||||
height="100%"
|
||||
flex={1}
|
||||
sx={{
|
||||
borderLeft: `solid 1px ${theme.palette.otherColors.graishWhite}`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
mb={theme.gap.medium}
|
||||
pb={theme.gap.ml}
|
||||
color={theme.palette.secondary.main}
|
||||
textAlign="center"
|
||||
sx={{
|
||||
borderBottom: `solid 1px ${theme.palette.otherColors.graishWhite}`,
|
||||
borderBottomStyle: "dashed",
|
||||
}}
|
||||
>
|
||||
The{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 500,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
performance score is calculated
|
||||
</Typography>{" "}
|
||||
directly from these{" "}
|
||||
<Typography component="span" fontWeight={600}>
|
||||
metrics
|
||||
</Typography>
|
||||
.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
pt={theme.gap.ml}
|
||||
gap={theme.gap.ml}
|
||||
>
|
||||
{Object.keys(audits).map((key) => {
|
||||
if (key === "_id") return;
|
||||
|
||||
let audit = audits[key];
|
||||
let metricParams = getColors(audit.score * 100);
|
||||
|
||||
let shape = (
|
||||
<Box
|
||||
sx={{
|
||||
width: theme.gap.medium,
|
||||
height: theme.gap.medium,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: metricParams.stroke,
|
||||
}}
|
||||
></Box>
|
||||
);
|
||||
if (metricParams.shape === "square")
|
||||
shape = (
|
||||
<Box
|
||||
sx={{
|
||||
width: theme.gap.medium,
|
||||
height: theme.gap.medium,
|
||||
backgroundColor: metricParams.stroke,
|
||||
}}
|
||||
></Box>
|
||||
);
|
||||
else if (metricParams.shape === "triangle")
|
||||
shape = (
|
||||
<Box
|
||||
sx={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
ml: `calc((${theme.gap.medium} - ${theme.gap.small}) / -2)`,
|
||||
borderLeft: `${theme.gap.small} solid transparent`,
|
||||
borderRight: `${theme.gap.small} solid transparent`,
|
||||
borderBottom: `${theme.gap.medium} solid ${metricParams.stroke}`,
|
||||
}}
|
||||
></Box>
|
||||
);
|
||||
|
||||
// Find the position where the number ends and the unit begins
|
||||
const match = audit.displayValue.match(
|
||||
/(\d+\.?\d*)\s*([a-zA-Z]+)/
|
||||
);
|
||||
let value;
|
||||
let unit;
|
||||
if (match) {
|
||||
value = match[1];
|
||||
unit = match[2];
|
||||
} else {
|
||||
value = audit.displayValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="metric"
|
||||
key={`${key}-box`}
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
{shape}
|
||||
<Box>
|
||||
<Typography sx={{ lineHeight: 1 }}>
|
||||
{audit.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: metricParams.text,
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
<Typography
|
||||
component="span"
|
||||
ml="2px"
|
||||
sx={{
|
||||
color: theme.palette.secondary.main,
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
{unit}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
import { Box, Grid, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
|
||||
@@ -57,20 +57,89 @@ const Card = ({ data }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack gap={theme.gap.xs}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.gap.large}
|
||||
>
|
||||
<Box width="80%">
|
||||
<Skeleton variant="rounded" width="25%" height={24} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={19.5}
|
||||
sx={{ mt: theme.gap.xs }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" flexWrap="wrap" gap={theme.gap.large}>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const PageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { monitors, isLoading } = useSelector(
|
||||
(state) => state.pageSpeedMonitors
|
||||
);
|
||||
useEffect(() => {
|
||||
dispatch(getPageSpeedByUserId(authToken));
|
||||
}, []);
|
||||
|
||||
// will show skeletons only on initial load
|
||||
// since monitor state is being added to redux persist, there's no reason to display skeletons on every render
|
||||
let isActuallyLoading = isLoading && monitors.length === 0;
|
||||
|
||||
return (
|
||||
<Box className="page-speed">
|
||||
{monitors?.length !== 0 ? (
|
||||
{isActuallyLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : monitors?.length !== 0 ? (
|
||||
<Stack gap={theme.gap.xs}>
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -161,6 +161,10 @@ button:focus-visible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body .MuiSkeleton-root {
|
||||
background-color: var(--env-var-color-15);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
|
||||
@@ -2,6 +2,7 @@ const {
|
||||
createCheckParamValidation,
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
} = require("../validation/joi");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
@@ -35,6 +36,7 @@ const createCheck = async (req, res, next) => {
|
||||
const getChecks = async (req, res, next) => {
|
||||
try {
|
||||
await getChecksParamValidation.validateAsync(req.params);
|
||||
await getChecksQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -45,10 +47,13 @@ const getChecks = async (req, res, next) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const checks = await req.db.getChecks(req.params.monitorId);
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, msg: successMessages.CHECK_GET, data: checks });
|
||||
const checks = await req.db.getChecks(req);
|
||||
const checksCount = await req.db.getChecksCount(req);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.CHECK_GET,
|
||||
data: { checksCount, checks },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
|
||||
@@ -88,6 +88,7 @@ const {
|
||||
|
||||
const {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
deleteChecks,
|
||||
} = require("./modules/checkModule");
|
||||
@@ -145,6 +146,7 @@ module.exports = {
|
||||
deleteAllMonitors,
|
||||
editMonitor,
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
deleteChecks,
|
||||
createAlert,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const Check = require("../../../models/Check");
|
||||
|
||||
const filterLookup = {
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
};
|
||||
/**
|
||||
* Create a check for a monitor
|
||||
* @async
|
||||
@@ -22,6 +26,19 @@ const createCheck = async (checkData) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getChecksCount = async (req) => {
|
||||
const monitorId = req.params.monitorId;
|
||||
const filter = req.query.filter;
|
||||
// Build query
|
||||
const checksQuery = { monitorId: monitorId };
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (filter !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[filter] };
|
||||
}
|
||||
const count = await Check.countDocuments(checksQuery);
|
||||
return count;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all checks for a monitor
|
||||
* @async
|
||||
@@ -30,9 +47,37 @@ const createCheck = async (checkData) => {
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const getChecks = async (monitorId) => {
|
||||
const getChecks = async (req) => {
|
||||
try {
|
||||
const checks = await Check.find({ monitorId });
|
||||
const { monitorId } = req.params;
|
||||
let { sortOrder, limit, filter, page, rowsPerPage } = req.query;
|
||||
|
||||
// Default limit to 0 if not provided
|
||||
if (limit === undefined) limit = 0;
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
if (sortOrder === "asc") {
|
||||
sortOrder = 1;
|
||||
} else if (sortOrder === "desc") {
|
||||
sortOrder = -1;
|
||||
} else sortOrder = -1;
|
||||
|
||||
// Build query
|
||||
const checksQuery = { monitorId: monitorId };
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (filter !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[filter] };
|
||||
}
|
||||
|
||||
// Need to skip and limit here
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
const checks = await Check.find(checksQuery)
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage)
|
||||
.sort({ createdAt: sortOrder });
|
||||
return checks;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -55,4 +100,4 @@ const deleteChecks = async (monitorId) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
module.exports = { createCheck, getChecks, deleteChecks };
|
||||
module.exports = { createCheck, getChecksCount, getChecks, deleteChecks };
|
||||
@@ -3,6 +3,7 @@ const Check = require("../../../models/Check");
|
||||
const PageSpeedCheck = require("../../../models/PageSpeedCheck");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
const Notification = require("../../../models/Notification");
|
||||
const { NormalizeData } = require("../../../utils/dataUtils");
|
||||
|
||||
/**
|
||||
* Get all monitors
|
||||
@@ -32,7 +33,14 @@ const getAllMonitors = async (req, res) => {
|
||||
const getMonitorById = async (req, res) => {
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
let { status, limit, sortOrder } = req.query;
|
||||
let { status, limit, sortOrder, filter, numToDisplay, normalize } =
|
||||
req.query;
|
||||
|
||||
const filterLookup = {
|
||||
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)),
|
||||
};
|
||||
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
@@ -52,18 +60,35 @@ const getMonitorById = async (req, res) => {
|
||||
checksQuery.status = status;
|
||||
}
|
||||
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (filter !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[filter] };
|
||||
}
|
||||
|
||||
// Determine model type
|
||||
let model =
|
||||
monitor.type === "http" || monitor.type === "ping"
|
||||
? Check
|
||||
: PageSpeedCheck;
|
||||
|
||||
const checks = await model
|
||||
let checks = await model
|
||||
.find(checksQuery)
|
||||
.sort({
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
|
||||
// If more than numToDisplay checks, pick every nth check
|
||||
if (numToDisplay !== undefined && checks && checks.length > numToDisplay) {
|
||||
const n = Math.ceil(checks.length / numToDisplay);
|
||||
checks = checks.filter((_, index) => index % n === 0);
|
||||
}
|
||||
|
||||
// Normalize checks if requested
|
||||
if (normalize) {
|
||||
checks = NormalizeData(checks, 10, 100);
|
||||
}
|
||||
|
||||
const notifications = await Notification.find({ monitorId: monitor._id });
|
||||
const monitorWithChecks = { ...monitor.toObject(), checks, notifications };
|
||||
return monitorWithChecks;
|
||||
@@ -82,7 +107,7 @@ const getMonitorById = async (req, res) => {
|
||||
*/
|
||||
const getMonitorsByUserId = async (req, res) => {
|
||||
try {
|
||||
let { limit, type, status, sortOrder } = req.query;
|
||||
let { limit, type, status, sortOrder, normalize } = req.query;
|
||||
const monitorQuery = { userId: req.params.userId };
|
||||
|
||||
if (type !== undefined) {
|
||||
@@ -115,13 +140,18 @@ const getMonitorsByUserId = async (req, res) => {
|
||||
: PageSpeedCheck;
|
||||
|
||||
// Checks are order newest -> oldest
|
||||
const checks = await model
|
||||
let checks = await model
|
||||
.find(checksQuery)
|
||||
.sort({
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
|
||||
//Normalize checks if requested
|
||||
if (normalize === true) {
|
||||
checks = NormalizeData(checks, 10, 100);
|
||||
}
|
||||
|
||||
// Get notifications
|
||||
const notifications = await Notification.find({
|
||||
monitorId: monitor._id,
|
||||
|
||||
@@ -11,7 +11,7 @@ const calculatePercentile = (arr, percentile) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const NormalizeData = (checks, rangeMin, rangeMax) => {
|
||||
const NormalizeData = (checks, rangeMin, rangeMax) => {
|
||||
if (checks.length > 1) {
|
||||
// Get the 5th and 95th percentile
|
||||
const min = calculatePercentile(checks, 0);
|
||||
@@ -31,7 +31,7 @@ export const NormalizeData = (checks, rangeMin, rangeMax) => {
|
||||
Math.min(rangeMax, normalizedResponseTime)
|
||||
);
|
||||
return {
|
||||
...check,
|
||||
...check._doc,
|
||||
responseTime: normalizedResponseTime,
|
||||
originalResponseTime: originalResponseTime,
|
||||
};
|
||||
@@ -44,3 +44,7 @@ export const NormalizeData = (checks, rangeMin, rangeMax) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
NormalizeData,
|
||||
};
|
||||
@@ -126,6 +126,9 @@ const getMonitorByIdQueryValidation = joi.object({
|
||||
status: joi.boolean(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
filter: joi.string().valid("day", "week", "month"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
|
||||
const getMonitorsByUserIdValidation = joi.object({
|
||||
@@ -136,6 +139,7 @@ const getMonitorsByUserIdQueryValidation = joi.object({
|
||||
status: joi.boolean(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
@@ -228,6 +232,14 @@ const getChecksParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
|
||||
const getChecksQueryValidation = joi.object({
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
filter: joi.string().valid("day", "week", "month"),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
});
|
||||
|
||||
const deleteChecksParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
@@ -307,6 +319,7 @@ module.exports = {
|
||||
createCheckParamValidation,
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
deleteUserParamValidation,
|
||||
getPageSpeedCheckParamValidation,
|
||||
|
||||
Reference in new issue
Block a user