mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-04-24 09:58:57 -05:00
Merge branch 'develop' into feat/fe/ttl
This commit is contained in:
@@ -96,7 +96,12 @@ const Monitors = ({ isAdmin }) => {
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography component="h2" variant="h2" letterSpacing={-0.5}>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
letterSpacing={-0.2}
|
||||
>
|
||||
Actively monitoring
|
||||
</Typography>
|
||||
<Box
|
||||
|
||||
@@ -52,12 +52,35 @@ const useUtils = () => {
|
||||
"& h2": { color: theme.palette.warning.main },
|
||||
},
|
||||
};
|
||||
const pagespeedStyles = {
|
||||
up: {
|
||||
bg: theme.palette.success.bg,
|
||||
light: theme.palette.success.light,
|
||||
stroke: theme.palette.success.main,
|
||||
},
|
||||
down: {
|
||||
bg: theme.palette.error.bg,
|
||||
light: theme.palette.error.light,
|
||||
stroke: theme.palette.error.main,
|
||||
},
|
||||
paused: {
|
||||
bg: theme.palette.warning.bg,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
pending: {
|
||||
bg: theme.palette.warning.bg,
|
||||
light: theme.palette.warning.light,
|
||||
stroke: theme.palette.warning.main,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
determineState,
|
||||
statusColor,
|
||||
statusMsg,
|
||||
pagespeedStatusMsg,
|
||||
pagespeedStyles,
|
||||
statusStyles,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,37 +1,7 @@
|
||||
.configure-pagespeed span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.configure-pagespeed h3.MuiTypography-root {
|
||||
font-weight: 500;
|
||||
}
|
||||
.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
.configure-pagespeed .MuiBox-root > .field + p.MuiTypography-root,
|
||||
.configure-pagespeed h3.MuiTypography-root > span.MuiTypography-root {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.configure-pagespeed .checkbox-wrapper {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
/* to be removed */
|
||||
.configure-pagespeed .section-disabled {
|
||||
opacity: 0.4;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.configure-pagespeed button {
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
|
||||
.configure-pagespeed h3.MuiTypography-root {
|
||||
flex: 0.7;
|
||||
}
|
||||
|
||||
.configure-pagespeed .field,
|
||||
.configure-pagespeed .section-disabled,
|
||||
.configure-pagespeed .select-wrapper {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Modal, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Modal, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../../Monitors/styled";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
@@ -22,20 +23,27 @@ 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";
|
||||
import useUtils from "../../Monitors/utils";
|
||||
import "./index.css";
|
||||
|
||||
const PageSpeedConfigure = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const { determineState, statusColor } = useUtils();
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
@@ -68,24 +76,58 @@ const PageSpeedConfigure = () => {
|
||||
fetchMonitor();
|
||||
}, [dispatch, authToken, monitorId, navigate]);
|
||||
|
||||
const handleChange = (event, id) => {
|
||||
let { value } = event.target;
|
||||
if (id === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
const handleChange = (event, name) => {
|
||||
let { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
|
||||
if (name.includes("notification-")) {
|
||||
name = name.replace("notification-", "");
|
||||
let hasNotif = monitor.notifications.some(
|
||||
(notification) => notification.type === name
|
||||
);
|
||||
setMonitor((prev) => {
|
||||
const notifs = [...prev.notifications];
|
||||
if (hasNotif) {
|
||||
return {
|
||||
...prev,
|
||||
notifications: notifs.filter((notif) => notif.type !== name),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
notifications: [
|
||||
...notifs,
|
||||
name === "email"
|
||||
? { type: name, address: value }
|
||||
: // TODO - phone number
|
||||
{ type: name, phone: value },
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error)
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
setMonitor((prev) => ({ ...prev, [id]: value }));
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [id]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
if (error) newErrors[id] = error.details[0].message;
|
||||
else delete newErrors[id];
|
||||
return newErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
@@ -128,7 +170,7 @@ const PageSpeedConfigure = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="configure-pagespeed" gap={theme.spacing(12)}>
|
||||
<Stack className="configure-pagespeed" gap={theme.spacing(10)}>
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
@@ -146,22 +188,65 @@ const PageSpeedConfigure = () => {
|
||||
spellCheck="false"
|
||||
onSubmit={handleSave}
|
||||
flex={1}
|
||||
gap={theme.spacing(12)}
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Stack direction="row" gap={theme.spacing(2)}>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Box>
|
||||
<Typography component="h1" variant="h1" mb={theme.spacing(2)}>
|
||||
{monitor?.url}
|
||||
<Typography component="h1" variant="h1">
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: statusColor[determineState(monitor)],
|
||||
}}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
Your pagespeed monitor is {determineState(monitor)}.
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography component="h2" variant="h2">
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.text.tertiary,
|
||||
opacity: 0.8,
|
||||
left: -10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Editting...
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box alignSelf="flex-end" ml="auto">
|
||||
<LoadingButton
|
||||
@@ -206,102 +291,116 @@ const PageSpeedConfigure = () => {
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
p={theme.spacing(20)}
|
||||
pl={theme.spacing(15)}
|
||||
gap={theme.spacing(20)}
|
||||
sx={{
|
||||
"& h3, & p": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">Monitor display name</Typography>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
placeholder="Example monitor"
|
||||
value={monitor?.name || ""}
|
||||
onChange={(event) => handleChange(event, "name")}
|
||||
error={errors.name}
|
||||
/>
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the
|
||||
type of monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(20)}
|
||||
sx={{
|
||||
".MuiInputBase-root:has(> .Mui-disabled)": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography component="h3">URL</Typography>
|
||||
<Field
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
label="URL"
|
||||
placeholder="random.website.com"
|
||||
value={monitor?.url?.replace("http://", "") || ""}
|
||||
onChange={(event) => handleChange(event, "url")}
|
||||
onChange={handleChange}
|
||||
error={errors.url}
|
||||
disabled={true}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">Check frequency</Typography>
|
||||
<Select
|
||||
id="monitor-frequency"
|
||||
items={frequencies}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Monitor display name"
|
||||
placeholder="Example monitor"
|
||||
isOptional={true}
|
||||
value={monitor?.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors.name}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">
|
||||
Incidents notifications{" "}
|
||||
<Typography component="span">(coming soon)</Typography>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
<Stack
|
||||
className="section-disabled"
|
||||
backgroundColor={theme.palette.background.fill}
|
||||
>
|
||||
<Typography mb={theme.spacing(4)}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Notify via email (to gorkem.cetin@bluewavelabs.ca)"
|
||||
isChecked={false}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-emails"
|
||||
label="Notify via email to following emails"
|
||||
isChecked={false}
|
||||
/>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("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={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor?.notifications?.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
id="notify-emails-list"
|
||||
placeholder="notifications@gmail.com"
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
error=""
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
id="monitor-frequency"
|
||||
label="Check frequency"
|
||||
items={frequencies}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
@@ -347,6 +446,7 @@ const PageSpeedConfigure = () => {
|
||||
id="modal-delete-pagespeed-monitor"
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
>
|
||||
Do you really want to delete this monitor?
|
||||
</Typography>
|
||||
|
||||
@@ -29,11 +29,13 @@ import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import PagespeedDetailsAreaChart from "./Charts/AreaChart";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import PieChart from "./Charts/PieChart";
|
||||
import useUtils from "../../Monitors/utils";
|
||||
import "./index.css";
|
||||
|
||||
const PageSpeedDetails = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [audits, setAudits] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
@@ -109,11 +111,7 @@ const PageSpeedDetails = () => {
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
monitor?.status
|
||||
? "Your pagespeed monitor is live."
|
||||
: "Your pagespeed monitor is down."
|
||||
}
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
@@ -129,13 +127,7 @@ const PageSpeedDetails = () => {
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot
|
||||
color={
|
||||
monitor?.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.main
|
||||
}
|
||||
/>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography component="h2" variant="h2">
|
||||
|
||||
@@ -1,32 +1,220 @@
|
||||
import PropTypes from "prop-types";
|
||||
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import { getLastChecked } from "../../Utils/monitorUtils";
|
||||
import { IconBox } from "./Details/styled";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
} from "recharts";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
formatDateWithTz,
|
||||
formatDurationRounded,
|
||||
formatDurationSplit,
|
||||
} from "../../Utils/timeUtils";
|
||||
import useUtils from "../Monitors/utils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* CustomToolTip displays a tooltip with formatted date and score information.
|
||||
* @param {Object} props
|
||||
* @param {Array} props.payload - Data to display in the tooltip
|
||||
* @returns {JSX.Element} The rendered tooltip component
|
||||
*/
|
||||
const CustomToolTip = ({ payload }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(
|
||||
payload[0]?.payload.createdAt,
|
||||
"ddd, MMMM D, YYYY, h:mm A",
|
||||
uiTimezone
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={payload[0]?.color}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Typography
|
||||
component="span"
|
||||
textTransform="capitalize"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{payload[0]?.name}
|
||||
</Typography>{" "}
|
||||
<Typography component="span">{payload[0]?.payload.score}</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
CustomToolTip.propTypes = {
|
||||
payload: PropTypes.array,
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes the raw data to include a score for each entry.
|
||||
* @param {Array<Object>} data - The raw data array.
|
||||
* @returns {Array<Object>} - The formatted data array with scores.
|
||||
*/
|
||||
const processData = (data) => {
|
||||
if (data.length === 0) return [];
|
||||
let formattedData = [];
|
||||
|
||||
const calculateScore = (entry) => {
|
||||
return (
|
||||
(entry.accessibility +
|
||||
entry.bestPractices +
|
||||
entry.performance +
|
||||
entry.seo) /
|
||||
4
|
||||
);
|
||||
};
|
||||
|
||||
data.forEach((entry) => {
|
||||
entry = { ...entry, score: calculateScore(entry) };
|
||||
formattedData.push(entry);
|
||||
});
|
||||
|
||||
return formattedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders an area chart displaying page speed scores.
|
||||
* @param {Object} props
|
||||
* @param {Array<Object>} props.data - The raw data to be displayed in the chart.
|
||||
* @param {string} props.status - The status of the page speed which determines the chart's color scheme.
|
||||
* @returns {JSX.Element} - The rendered area chart.
|
||||
*/
|
||||
const PagespeedAreaChart = ({ data, status }) => {
|
||||
const theme = useTheme();
|
||||
const { pagespeedStyles } = useUtils();
|
||||
|
||||
const formattedData = processData(data);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={25} height={85}>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={formattedData}
|
||||
margin={{ top: 10, bottom: -5 }}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={<CustomToolTip />}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`pagespeed-chart-${status}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={pagespeedStyles[status].stroke}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={pagespeedStyles[status].light}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
dataKey="score"
|
||||
stroke={pagespeedStyles[status].stroke}
|
||||
strokeWidth={1.5}
|
||||
fill={`url(#pagespeed-chart-${status})`}
|
||||
activeDot={{
|
||||
stroke: pagespeedStyles[status].light,
|
||||
fill: pagespeedStyles[status].stroke,
|
||||
r: 4.5,
|
||||
}}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
PagespeedAreaChart.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
accessibility: PropTypes.number.isRequired,
|
||||
bestPractices: PropTypes.number.isRequired,
|
||||
performance: PropTypes.number.isRequired,
|
||||
seo: PropTypes.number.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a card displaying monitor details and an area chart.
|
||||
* @param {Object} props
|
||||
* @param {Object} props.monitor - The monitor data to be displayed in the card.
|
||||
* @returns {JSX.Element} - The rendered card.
|
||||
*/
|
||||
const Card = ({ monitor }) => {
|
||||
const { determineState, pagespeedStatusMsg } = useUtils();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const monitorState = determineState(monitor);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
lg={6}
|
||||
flexGrow={1}
|
||||
sx={{
|
||||
"&:hover > .MuiStack-root": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
<Grid item lg={6} flexGrow={1}>
|
||||
<Box
|
||||
position="relative"
|
||||
p={theme.spacing(8)}
|
||||
onClick={() => navigate(`/pagespeed/${monitor._id}`)}
|
||||
border={1}
|
||||
@@ -34,41 +222,88 @@ const Card = ({ monitor }) => {
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "34px 2fr 1fr",
|
||||
columnGap: theme.spacing(5),
|
||||
gridTemplateRows: "34px 1fr 3fr",
|
||||
cursor: "pointer",
|
||||
"& svg path": { stroke: theme.palette.other.icon, strokeWidth: 0.8 },
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PageSpeedIcon
|
||||
style={{ width: theme.spacing(8), height: theme.spacing(8) }}
|
||||
<IconBox>
|
||||
<PageSpeedIcon />
|
||||
</IconBox>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
fontWeight={500}
|
||||
alignSelf="center"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<StatusLabel
|
||||
status={monitorState}
|
||||
text={pagespeedStatusMsg[monitorState] || "Pending..."}
|
||||
customStyles={{
|
||||
width: "max-content",
|
||||
textTransform: "capitalize",
|
||||
alignSelf: "flex-start",
|
||||
justifySelf: "flex-end",
|
||||
}}
|
||||
/>
|
||||
<Box flex={1}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(2)}
|
||||
fontSize={16}
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<StatusLabel
|
||||
status={monitorState}
|
||||
text={pagespeedStatusMsg[monitorState] || "Pending..."}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography>{monitor.url.replace(/^https?:\/\//, "")}</Typography>
|
||||
<Typography mt={theme.spacing(12)}>
|
||||
<Typography component="span" variant="body2" fontWeight={600}>
|
||||
Last checked:{" "}
|
||||
</Typography>
|
||||
{formatDate(getLastChecked(monitor.checks, false))}{" "}
|
||||
<Typography component="span" variant="body2" fontStyle="italic">
|
||||
({formatDurationRounded(getLastChecked(monitor.checks))} ago)
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
mt={theme.spacing(-2)}
|
||||
sx={{ gridColumnStart: 2 }}
|
||||
>
|
||||
{monitor.url}
|
||||
</Typography>
|
||||
<Box
|
||||
mx={theme.spacing(-8)}
|
||||
mt={theme.spacing(4)}
|
||||
mb={theme.spacing(-8)}
|
||||
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
|
||||
>
|
||||
<PagespeedAreaChart data={monitor.checks} status={monitorState} />
|
||||
</Box>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
py={theme.spacing(1)}
|
||||
px={theme.spacing(4)}
|
||||
borderTop={1}
|
||||
borderRight={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
userSelect: "none",
|
||||
borderTopRightRadius: 8,
|
||||
borderBottomLeftRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Typography fontSize={11} color={theme.palette.text.accent}>
|
||||
Checking every{" "}
|
||||
{(() => {
|
||||
const { time, format } = formatDurationSplit(monitor?.interval);
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={12}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{time}{" "}
|
||||
</Typography>
|
||||
{format}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
.page-speed h2.MuiTypography-root {
|
||||
line-height: 1.1;
|
||||
}
|
||||
.page-speed .label {
|
||||
padding: 7px;
|
||||
height: 22px;
|
||||
height: 24px;
|
||||
font-size: var(--env-var-font-size-small);
|
||||
}
|
||||
.page-speed:not(:has([class*="fallback__"])) button {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Button, Grid, Stack } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
@@ -11,24 +11,51 @@ import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import Greeting from "../../Utils/greeting";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Card from "./card";
|
||||
import { networkService } from "../../main";
|
||||
|
||||
const PageSpeed = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitorsSummary, isLoading } = useSelector(
|
||||
(state) => state.pageSpeedMonitors
|
||||
);
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
useEffect(() => {
|
||||
dispatch(getPageSpeedByTeamId(authToken));
|
||||
}, [authToken, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorsByTeamId(
|
||||
authToken,
|
||||
user.teamId,
|
||||
10,
|
||||
["pagespeed"],
|
||||
null,
|
||||
"desc",
|
||||
true,
|
||||
null,
|
||||
null
|
||||
);
|
||||
if (res?.data?.data?.monitors) {
|
||||
setMonitors(res.data.data.monitors);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMonitors();
|
||||
}, []);
|
||||
|
||||
// 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 && monitorsSummary?.monitors?.length === 0;
|
||||
|
||||
let isActuallyLoading = isLoading && monitors?.length === 0;
|
||||
return (
|
||||
<Box
|
||||
className="page-speed"
|
||||
@@ -46,14 +73,8 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
>
|
||||
{isActuallyLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : monitorsSummary?.monitors?.length !== 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
"& p": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
) : monitors?.length !== 0 ? (
|
||||
<Box>
|
||||
<Box mb={theme.spacing(12)}>
|
||||
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
|
||||
<Stack
|
||||
@@ -67,13 +88,14 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/pagespeed/create")}
|
||||
sx={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
Create new
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Grid container spacing={theme.spacing(12)}>
|
||||
{monitorsSummary?.monitors?.map((monitor) => (
|
||||
{monitors?.map((monitor) => (
|
||||
<Card monitor={monitor} key={monitor._id} />
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user