Merge pull request #564 from bluewave-labs/feat/loading-skeletons

Loading skeletons
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-08-07 17:43:57 -07:00
commit 288a24e314
8 files changed
+1094 -721

No files matched your search

+48 -8
View File
@@ -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>
</>
+265 -203
View File
@@ -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"
@@ -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);
}
+202 -112
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } 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";
@@ -30,6 +30,81 @@ 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
@@ -158,135 +233,150 @@ 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>
<BasicTable data={data} paginated={true} reversed={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;
+85 -39
View File
@@ -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();
@@ -204,47 +237,60 @@ 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>
<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}
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}
+418 -353
View File
@@ -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>
);
};
+72 -3
View File
@@ -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"
+4
View File
@@ -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;