Merge pull request #1657 from bluewave-labs/fix/fe/pagespeed-details-refactor

fix: fe/pagespeed details refactor
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-01-29 07:22:53 -08:00
commit c55ca3f593
36 files changed
+687 -743

No files matched your search

@@ -0,0 +1,90 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox";
import PropTypes from "prop-types";
const ChartBox = ({
children,
icon,
header,
height = "300px",
justifyContent = "space-between",
Legend,
borderRadiusRight = 4,
}) => {
const theme = useTheme();
return (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: borderRadiusRight,
borderBottomRightRadius: borderRadiusRight,
}}
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
minWidth: 250,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& .MuiBox-root:not(.area-tooltip) p": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 13,
},
"& .MuiBox-root > span": {
color: theme.palette.primary.contrastText,
fontSize: 20,
"& span": {
opacity: 0.8,
marginLeft: 2,
fontSize: 15,
},
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
{Legend && Legend}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node.isRequired,
header: PropTypes.string.isRequired,
height: PropTypes.string,
};
@@ -0,0 +1,42 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox";
import PropTypes from "prop-types";
const LegendBox = ({ children, icon, header, sx }) => {
const theme = useTheme();
return (
<Stack
direction="column"
gap={theme.spacing(4)}
borderRadius={theme.spacing(8)}
sx={{
...sx,
"& label": { pl: theme.spacing(6) },
borderLeftStyle: "solid",
borderLeftWidth: 1,
borderLeftColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: theme.spacing(8),
background: `linear-gradient(325deg, ${theme.palette.tertiary.main} 20%, ${theme.palette.primary.main} 45%)`,
}}
>
<Stack
direction="row"
gap={theme.spacing(6)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
LegendBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node,
header: PropTypes.string,
};
export default LegendBox;
+5 -3
View File
@@ -32,12 +32,14 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) =>
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
borderStyle="dashed"
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
>
<Stack
className={`fallback__${title.trim().split(" ")[0]}`}
className={`fallback__${title?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
@@ -68,7 +70,7 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) =>
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks.map((check, index) => (
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
@@ -139,7 +139,9 @@ const Search = ({
}
return filtered;
}}
getOptionKey={(option) => option.id}
getOptionKey={(option) => {
return option._id;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
@@ -196,6 +198,7 @@ const Search = ({
};
Search.propTypes = {
label: PropTypes.string,
id: PropTypes.string,
multiple: PropTypes.bool,
options: PropTypes.array.isRequired,
@@ -1,7 +1,7 @@
import { Button, Box } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
const ConfigButton = ({ shouldRender, monitorId }) => {
const theme = useTheme();
@@ -1,17 +1,16 @@
import { Stack, Typography } from "@mui/material";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import Dot from "../../../../../Components/Dot";
import PulseDot from "../Animated/PulseDot";
import Dot from "../Dot";
import { useTheme } from "@emotion/react";
import useUtils from "../../../Monitors/Hooks/useUtils";
import { formatDurationRounded } from "../../../../../Utils/timeUtils";
import ConfigButton from "../ConfigButton";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
import { formatDurationRounded } from "../../Utils/timeUtils";
import ConfigButton from "./ConfigButton";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => {
const theme = useTheme();
const { statusColor, statusMsg, determineState } = useUtils();
console.log(shouldRender);
if (!shouldRender) {
return <SkeletonLayout />;
}
@@ -0,0 +1,28 @@
// Components
import { Stack } from "@mui/material";
import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender, children }) => {
const theme = useTheme();
if (!shouldRender) {
return <SkeletonLayout numBoxes={children?.length ?? 1} />;
}
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
{children}
</Stack>
);
};
StatusBoxes.propTypes = {
shouldRender: PropTypes.bool,
children: PropTypes.node,
};
export default StatusBoxes;
@@ -0,0 +1,31 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const SkeletonLayout = ({ numBoxes }) => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
{Array.from({ length: numBoxes }).map((_, index) => {
const width = `${100 / numBoxes}%`;
return (
<Skeleton
variant="rounded"
width={width}
height={50}
key={index}
/>
);
})}
</Stack>
);
};
SkeletonLayout.propTypes = {
numBoxes: PropTypes.number,
};
export default SkeletonLayout;
@@ -11,7 +11,7 @@ import {
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { Box, Stack, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../Utils/timeUtils";
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
import { useSelector } from "react-redux";
const config = {
@@ -211,10 +211,13 @@ CustomTick.propTypes = {
* @returns {JSX.Element} The area chart component.
*/
const PagespeedDetailsAreaChart = ({ data, interval, metrics }) => {
const PageSpeedAreaChart = ({ data, monitor, metrics }) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
const memoizedData = useMemo(() => processDataWithGaps(data, interval), [data[0]]);
const memoizedData = useMemo(
() => processDataWithGaps(data, monitor.interval),
[data[0]]
);
const filteredConfig = Object.keys(config).reduce((result, key) => {
if (metrics[key]) {
@@ -310,7 +313,7 @@ const PagespeedDetailsAreaChart = ({ data, interval, metrics }) => {
);
};
PagespeedDetailsAreaChart.propTypes = {
PageSpeedAreaChart.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
createdAt: PropTypes.string.isRequired,
@@ -320,8 +323,8 @@ PagespeedDetailsAreaChart.propTypes = {
seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})
).isRequired,
interval: PropTypes.number.isRequired,
monitor: PropTypes.object.isRequired,
metrics: PropTypes.object,
};
export default PagespeedDetailsAreaChart;
export default PageSpeedAreaChart;
@@ -0,0 +1,56 @@
import { Box, Typography, Divider } from "@mui/material";
import Checkbox from "../../../../../Components/Inputs/Checkbox";
import MetricsIcon from "../../../../../assets/icons/ruler-icon.svg?react";
import LegendBox from "../../../../../Components/Charts/LegendBox";
import { useTheme } from "@emotion/react";
const AreaChartLegend = ({ metrics, handleMetrics }) => {
const theme = useTheme();
return (
<LegendBox
icon={<MetricsIcon />}
header="Metrics"
>
<Box>
<Typography
fontSize={11}
fontWeight={500}
>
Shown
</Typography>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Checkbox
id="accessibility-toggle"
label="Accessibility"
isChecked={metrics.accessibility}
onChange={() => handleMetrics("accessibility")}
/>
<Divider />
<Checkbox
id="best-practices-toggle"
label="Best Practices"
isChecked={metrics.bestPractices}
onChange={() => handleMetrics("bestPractices")}
/>
<Divider />
<Checkbox
id="performance-toggle"
label="Performance"
isChecked={metrics.performance}
onChange={() => handleMetrics("performance")}
/>
<Divider />
<Checkbox
id="seo-toggle"
label="Search Engine Optimization"
isChecked={metrics.seo}
onChange={() => handleMetrics("seo")}
/>
<Divider />
</LegendBox>
);
};
export default AreaChartLegend;
@@ -115,6 +115,10 @@ const weights = {
const PieChart = ({ audits }) => {
const theme = useTheme();
const [highlightedItem, setHighLightedItem] = useState(null);
const [expand, setExpand] = useState(false);
if (typeof audits === "undefined") return null;
/**
* Retrieves color properties based on the performance value.
@@ -216,8 +220,6 @@ const PieChart = ({ audits }) => {
const pieData = getPieData(audits);
const colorMap = getColors(performance);
const [highlightedItem, setHighLightedItem] = useState(null);
const [expand, setExpand] = useState(false);
return (
<Box onMouseLeave={() => setExpand(false)}>
{expand ? (
@@ -0,0 +1,96 @@
import { Stack, Box, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import LegendBox from "../../../../../Components/Charts/LegendBox";
import SpeedometerIcon from "../../../../../assets/icons/speedometer-icon.svg?react";
import PropTypes from "prop-types";
const PieChartLegend = ({ audits }) => {
const theme = useTheme();
if (typeof audits === "undefined") return null;
return (
<LegendBox
icon={<SpeedometerIcon />}
header="Performance metrics"
sx={{ flex: 1 }}
>
{Object.keys(audits).map((key) => {
if (key === "_id") return;
let audit = audits[key];
let score = audit.score * 100;
let bg =
score >= 90
? theme.palette.success.main
: score >= 50
? theme.palette.warning.main
: score >= 0
? theme.palette.error.main
: theme.palette.tertiary.main;
// 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];
match[2] === "s" ? (unit = "seconds") : (unit = match[2]);
} else {
value = audit.displayValue;
}
return (
<Stack
flex={1}
key={`${key}-box`}
justifyContent="space-between"
direction="row"
gap={theme.spacing(4)}
p={theme.spacing(3)}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={4}
>
<Box>
<Typography
fontSize={12}
fontWeight={500}
lineHeight={1}
mb={1}
textTransform="uppercase"
>
{audit.title}
</Typography>
<Typography
component="span"
fontSize={14}
fontWeight={500}
color={theme.palette.primary.contrastText}
>
{value}
<Typography
component="span"
variant="body2"
ml={2}
>
{unit}
</Typography>
</Typography>
</Box>
<Box
width={4}
backgroundColor={bg}
borderRadius={4}
/>
</Stack>
);
})}
</LegendBox>
);
};
PieChartLegend.propTypes = {
audits: PropTypes.object,
};
export default PieChartLegend;
Whitespace-only changes.
@@ -0,0 +1,59 @@
import ChartBox from "../../../../../Components/Charts/ChartBox";
import AreaChart from "../Charts/AreaChart";
import AreaChartLegend from "../Charts/AreaChartLegend";
import SkeletonLayout from "./skeleton";
import ScoreIcon from "../../../../../assets/icons/monitor-graph-line.svg?react";
import { Stack } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) => {
const theme = useTheme();
if (!shouldRender) {
return <SkeletonLayout />;
}
if (typeof monitor === "undefined") {
return null;
}
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
return (
<Stack
direction="row"
gap={theme.spacing(10)}
>
<ChartBox
justifyContent="flex-start"
icon={<ScoreIcon />}
header="Score history"
height="100%"
borderRadiusRight={16}
Legend={
<AreaChartLegend
metrics={metrics}
handleMetrics={handleMetrics}
/>
}
>
<AreaChart
data={data}
monitor={monitor}
metrics={metrics}
/>
</ChartBox>
</Stack>
);
};
PageSpeedAreaChart.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
metrics: PropTypes.object,
handleMetrics: PropTypes.func,
};
export default PageSpeedAreaChart;
@@ -0,0 +1,17 @@
import { Box, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Box
height={"100%"}
width={"100%"}
>
<Skeleton
height={"100%"}
width={"100%"}
/>
</Box>
);
};
export default SkeletonLayout;
@@ -0,0 +1,41 @@
import StatusBoxes from "../../../../../Components/StatusBoxes";
import StatBox from "../../../../../Components/StatBox";
import { Typography } from "@mui/material";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
const { time: uptimeDuration, units: uptimeUnits } = getHumanReadableDuration(
monitor?.uptimeDuration
);
const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
monitor?.lastChecked
);
return (
<StatusBoxes shouldRender={shouldRender}>
<StatBox
heading="checks since"
subHeading={
<>
{uptimeDuration}
<Typography component="span">{uptimeUnits}</Typography>
<Typography component="span">ago</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{lastCheckTime}
<Typography component="span">{lastCheckUnits}</Typography>
<Typography component="span">ago</Typography>
</>
}
/>
</StatusBoxes>
);
};
export default PageSpeedStatusBoxes;
@@ -0,0 +1,55 @@
import ChartBox from "../../../../../Components/Charts/ChartBox";
import PerformanceIcon from "../../../../../assets/icons/performance-report.svg?react";
import PieChart from "../Charts/PieChart";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import PieChartLegend from "../Charts/PieChartLegend";
import SkeletonLayout from "./skeleton";
const PerformanceReport = ({ shouldRender, audits }) => {
console.log("shouldRender", shouldRender);
const theme = useTheme();
if (!shouldRender) {
return <SkeletonLayout />;
}
if (typeof audits === "undefined") {
return null;
}
return (
<ChartBox
icon={<PerformanceIcon />}
header="Performance report"
Legend={<PieChartLegend audits={audits} />}
borderRadiusRight={16}
>
<PieChart audits={audits} />
<Typography
variant="body1"
mt="auto"
>
Values are estimated and may vary.{" "}
<Typography
component="span"
fontSize="inherit"
sx={{
color: theme.palette.primary.main,
fontWeight: 500,
textDecoration: "underline",
textUnderlineOffset: 2,
transition: "all 200ms",
cursor: "pointer",
"&:hover": {
textUnderlineOffset: 4,
},
}}
>
See calculator
</Typography>
</Typography>
</ChartBox>
);
};
export default PerformanceReport;
@@ -0,0 +1,17 @@
import { Box, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Box
height={"100%"}
width={"100%"}
>
<Skeleton
height={"100%"}
width={"100%"}
/>
</Box>
);
};
export default SkeletonLayout;
@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { logger } from "../../../../Utils/Logger";
import { useNavigate } from "react-router-dom";
const useMonitorFetch = ({ authToken, monitorId }) => {
const navigate = useNavigate();
const [monitor, setMonitor] = useState(undefined);
const [audits, setAudits] = useState(undefined);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchMonitor = async () => {
try {
setIsLoading(true);
const res = await networkService.getStatsByMonitorId({
authToken: authToken,
monitorId: monitorId,
sortOrder: "desc",
limit: 50,
dateRange: "day",
numToDisplay: null,
normalize: null,
});
setMonitor(res?.data?.data ?? undefined);
setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined);
} catch (error) {
logger.error(logger);
navigate("/not-found", { replace: true });
} finally {
setIsLoading(false);
}
};
fetchMonitor();
}, [authToken, monitorId, navigate]);
return { monitor, audits, isLoading };
};
export { useMonitorFetch };
@@ -1,18 +0,0 @@
.page-speed-details button.MuiButtonBase-root {
min-height: 34px;
}
.page-speed-details .MuiPieArcLabel-root {
font-size: var(--env-var-font-size-small-plus);
transition: fill 300ms ease;
}
.page-speed-details .MuiPieArcLabel-faded {
fill: rgba(0, 0, 0, 0.3);
}
.page-speed-details .pie-label,
.page-speed-details .pie-value-label {
transition: all 400ms ease;
}
.page-speed-details .MuiPieArc-root {
stroke: inherit;
}
+52 -420
View File
@@ -1,75 +1,37 @@
import PropTypes from "prop-types";
import { Box, Button, Divider, Stack, Tooltip, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
import { ChartBox } from "./styled";
import { logger } from "../../../Utils/Logger";
import { networkService } from "../../../main";
import SkeletonLayout from "./skeleton";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import SpedometerIcon from "../../../assets/icons/spedometer-icon.svg?react";
import MetricsIcon from "../../../assets/icons/ruler-icon.svg?react";
import ScoreIcon from "../../../assets/icons/monitor-graph-line.svg?react";
import PerformanceIcon from "../../../assets/icons/performance-report.svg?react";
// Components
import { Stack, Typography } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
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 "../../Uptime/Monitors/Hooks/useUtils";
import "./index.css";
import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes";
import PageSpeedAreaChart from "./Components/PageSpeedAreaChart";
import PerformanceReport from "./Components/PerformanceReport";
import Fallback from "../../../Components/Fallback";
// Utils
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import StatBox from "../../../Components/StatBox";
import IconBox from "../../../Components/IconBox";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useMonitorFetch } from "./Hooks/useMonitorFetch";
import { useState } from "react";
// Constants
const BREADCRUMBS = [
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `` },
// { name: "details", path: `/pagespeed/${monitorId}` }, // Not needed?
];
const PageSpeedDetails = () => {
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
const [monitor, setMonitor] = useState({});
const [audits, setAudits] = useState({});
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
useEffect(() => {
const fetchMonitor = async () => {
try {
const res = await networkService.getStatsByMonitorId({
authToken: authToken,
monitorId: monitorId,
sortOrder: "desc",
limit: 50,
dateRange: "day",
numToDisplay: null,
normalize: null,
});
setMonitor(res?.data?.data ?? {});
setAudits(res?.data?.data?.checks?.[0]?.audits ?? {});
} catch (error) {
logger.error(logger);
navigate("/not-found", { replace: true });
}
};
const { monitor, audits, isLoading } = useMonitorFetch({
authToken,
monitorId,
});
fetchMonitor();
}, [monitorId, authToken, navigate]);
let loading = Object.keys(monitor).length === 0;
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
const splitDuration = (duration) => {
const { time, format } = formatDurationSplit(duration);
return (
<>
{time}
<Typography component="span">{format}</Typography>
</>
);
};
console.log(monitor, audits, isLoading);
const [metrics, setMetrics] = useState({
accessibility: true,
@@ -77,371 +39,41 @@ const PageSpeedDetails = () => {
performance: true,
seo: true,
});
const handleMetrics = (id) => {
setMetrics((prev) => ({ ...prev, [id]: !prev[id] }));
};
return (
<Stack
className="page-speed-details"
gap={theme.spacing(10)}
>
{loading ? (
<SkeletonLayout />
) : (
<>
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `/pagespeed/${monitorId}` },
]}
/>
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<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}
</Typography>
<Typography
position="relative"
variant="body2"
mt={theme.spacing(1)}
ml={theme.spacing(6)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: 4,
height: 4,
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: -9,
top: "50%",
transform: "translateY(-50%)",
},
}}
>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Box>
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
},
}}
>
<SettingsIcon />
Configure
</Button>
)}
</Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
heading="checks since"
subHeading={
<>
{splitDuration(monitor?.uptimeDuration)}
<Typography component="span">ago</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</>
}
/>
</Stack>
<Box>
<Typography
variant="body2"
my={theme.spacing(8)}
>
Showing statistics for past 24 hours.
</Typography>
<ChartBox sx={{ gridTemplateColumns: "75% 25%" }}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<ScoreIcon />
</IconBox>
<Typography component="h2">Score history</Typography>
</Stack>
<Box>
<PagespeedDetailsAreaChart
data={data}
interval={monitor?.interval}
metrics={metrics}
/>
</Box>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<MetricsIcon />
</IconBox>
<Typography component="h2">Metrics</Typography>
</Stack>
<Stack
gap={theme.spacing(4)}
mt={theme.spacing(16)}
sx={{
"& label": { pl: theme.spacing(6) },
}}
>
<Box>
<Typography
fontSize={11}
fontWeight={500}
>
Shown
</Typography>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Checkbox
id="accessibility-toggle"
label="Accessibility"
isChecked={metrics.accessibility}
onChange={() => handleMetrics("accessibility")}
/>
<Divider />
<Checkbox
id="best-practices-toggle"
label="Best Practices"
isChecked={metrics.bestPractices}
onChange={() => handleMetrics("bestPractices")}
/>
<Divider />
<Checkbox
id="performance-toggle"
label="Performance"
isChecked={metrics.performance}
onChange={() => handleMetrics("performance")}
/>
<Divider />
<Checkbox
id="seo-toggle"
label="Search Engine Optimization"
isChecked={metrics.seo}
onChange={() => handleMetrics("seo")}
/>
<Divider />
</Stack>
</Box>
</ChartBox>
</Box>
<ChartBox
flex={1}
/* TODO apply 1fr 1fr for columns, and auto 1fr for Rows */
sx={{ gridTemplateColumns: "50% 50%", gridTemplateRows: "15% 85%" }}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<PerformanceIcon />
</IconBox>
<Typography component="h2">Performance report</Typography>
</Stack>
<Stack
alignItems="center"
textAlign="center"
minWidth="300px"
flex={1}
pt={theme.spacing(6)}
pb={theme.spacing(12)}
>
<PieChart audits={audits} />
<Typography
variant="body1"
mt="auto"
>
Values are estimated and may vary.{" "}
<Typography
component="span"
fontSize="inherit"
sx={{
color: theme.palette.primary.main,
fontWeight: 500,
textDecoration: "underline",
textUnderlineOffset: 2,
transition: "all 200ms",
cursor: "pointer",
"&:hover": {
textUnderlineOffset: 4,
},
}}
>
See calculator
</Typography>
</Typography>
</Stack>
<Box
px={theme.spacing(20)}
py={theme.spacing(8)}
height="100%"
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<SpedometerIcon />
</IconBox>
<Typography component="h2">Performance Metrics</Typography>
</Stack>
<Stack
flexWrap="wrap"
mt={theme.spacing(4)}
gap={theme.spacing(4)}
>
{Object.keys(audits).map((key) => {
if (key === "_id") return;
let audit = audits[key];
let score = audit.score * 100;
let bg =
score >= 90
? theme.palette.success.main
: score >= 50
? theme.palette.warning.main
: score >= 0
? theme.palette.error.contrastText
: theme.palette.tertiary.main;
// 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];
match[2] === "s" ? (unit = "seconds") : (unit = match[2]);
} else {
value = audit.displayValue;
}
return (
<Stack
key={`${key}-box`}
justifyContent="space-between"
direction="row"
gap={theme.spacing(4)}
p={theme.spacing(3)}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={4}
>
<Box>
<Typography
fontSize={12}
fontWeight={500}
lineHeight={1}
mb={1}
textTransform="uppercase"
>
{audit.title}
</Typography>
<Typography
component="span"
fontSize={14}
fontWeight={500}
color={theme.palette.primary.contrastText}
>
{value}
<Typography
component="span"
variant="body2"
ml={2}
>
{unit}
</Typography>
</Typography>
</Box>
<Box
width={4}
backgroundColor={bg}
borderRadius={4}
/>
</Stack>
);
})}
</Stack>
</Box>
</ChartBox>
</>
)}
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorStatusHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
monitor={monitor}
/>
<PageSpeedStatusBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
<Typography
variant="body2"
my={theme.spacing(8)}
>
Showing statistics for past 24 hours.
</Typography>
<PageSpeedAreaChart
shouldRender={!isLoading}
monitor={monitor}
metrics={metrics}
handleMetrics={handleMetrics}
/>
<PerformanceReport
shouldRender={!isLoading}
audits={audits}
/>
</Stack>
);
};
PageSpeedDetails.propTypes = {
isAdmin: PropTypes.bool,
push: PropTypes.func,
};
export default PageSpeedDetails;
@@ -1,93 +0,0 @@
import { Box, Skeleton, Stack } from "@mui/material";
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
return (
<>
<Skeleton
variant="rounded"
width="15%"
height={34}
/>
<Stack
direction="row"
gap={4}
>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="85%">
<Skeleton
variant="rounded"
width="50%"
height={24}
/>
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: 4 }}
/>
</Box>
<Skeleton
variant="rounded"
width="15%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={20}
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}
/>
</>
);
};
export default SkeletonLayout;
@@ -1,42 +0,0 @@
import { Stack, styled } from "@mui/material";
export const ChartBox = styled(Stack)(({ theme }) => ({
display: "grid",
minHeight: 300,
minWidth: 250,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 4,
borderTopRightRadius: 16,
borderBottomRightRadius: 16,
backgroundColor: theme.palette.primary.main,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& p": { color: theme.palette.primary.contrastTextTertiary },
"& > :nth-of-type(1)": {
gridColumn: 1,
gridRow: 1,
height: "fit-content",
paddingTop: theme.spacing(8),
paddingLeft: theme.spacing(8),
},
"& > :nth-of-type(2)": { gridColumn: 1, gridRow: 2 },
"& > :nth-of-type(3)": {
gridColumn: 2,
gridRow: "span 2",
padding: theme.spacing(8),
borderLeft: 1,
borderLeftStyle: "solid",
borderLeftColor: theme.palette.primary.lowContrast,
borderRadius: 16,
backgroundColor: theme.palette.primary.main,
background: `linear-gradient(325deg, ${theme.palette.tertiary.main} 20%, ${theme.palette.primary.main} 45%)`,
},
"& path": {
transition: "stroke-width 400ms ease",
},
}));
@@ -1,6 +1,6 @@
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Box } from "@mui/material";
import { Stack } from "@mui/material";
import CreateMonitorHeader from "../../../Components/CreateMonitorHeader";
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorGrid from "./Components/MonitorGrid";
@@ -1,6 +1,6 @@
// Components
import { Stack, Typography, Box } from "@mui/material";
import ChartBox from "../Charts/ChartBox";
import ChartBox from "../../../../../Components/Charts/ChartBox";
import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react";
import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react";
import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react";
@@ -40,7 +40,10 @@ const ChartBoxes = ({
icon={<UptimeIcon />}
header="Uptime"
>
<Stack justifyContent="space-between">
<Stack
justifyContent="space-between"
direction="row"
>
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
@@ -129,6 +132,7 @@ const ChartBoxes = ({
export default ChartBoxes;
ChartBoxes.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object.isRequired,
dateRange: PropTypes.string.isRequired,
uiTimezone: PropTypes.string.isRequired,
@@ -1,75 +0,0 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../../../../Components/IconBox";
import PropTypes from "prop-types";
const ChartBox = ({ children, icon, header, height = "300px" }) => {
const theme = useTheme();
return (
<Stack
sx={{
justifyContent: "space-between",
flex: "1 30%",
gap: theme.spacing(8),
height,
minWidth: 250,
padding: theme.spacing(8),
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 4,
backgroundColor: theme.palette.primary.main,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& .MuiBox-root:not(.area-tooltip) p": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 13,
},
"& .MuiBox-root > span": {
color: theme.palette.primary.contrastText,
fontSize: 20,
"& span": {
opacity: 0.8,
marginLeft: 2,
fontSize: 15,
},
},
"& .MuiStack-root": {
flexDirection: "row",
gap: theme.spacing(6),
},
"& .MuiStack-root:first-of-type": {
alignItems: "center",
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node.isRequired,
header: PropTypes.string.isRequired,
height: PropTypes.string,
};
@@ -1,4 +1,4 @@
import ChartBox from "./ChartBox";
import ChartBox from "../../../../../Components/Charts/ChartBox";
import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
import SkeletonLayout from "./ResponseTimeChartSkeleton";
@@ -1,4 +1,4 @@
import ChartBox from "../Charts/ChartBox";
import ChartBox from "../../../../../Components/Charts/ChartBox";
import PropTypes from "prop-types";
import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react";
import Table from "../../../../../Components/Table";
@@ -1,30 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,20 +1,13 @@
// Components
import { Stack, Typography } from "@mui/material";
import StatusBoxes from "../../../../../Components/StatusBoxes";
import StatBox from "../../../../../Components/StatBox";
import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import useUtils from "../../../Monitors/Hooks/useUtils";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
// Utils
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
import useUtils from "../../../Monitors/Hooks/useUtils";
const UptimeStatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
const theme = useTheme();
const { determineState } = useUtils();
if (!shouldRender) {
return <SkeletonLayout />;
}
const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
monitor?.uptimeStreak
);
@@ -22,12 +15,8 @@ const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
monitor?.timeSinceLastCheck
);
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatusBoxes shouldRender={shouldRender}>
<StatBox
gradient={true}
status={determineState(monitor)}
@@ -70,14 +59,8 @@ const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
</Typography>
}
/>
</Stack>
</StatusBoxes>
);
};
StatusBoxes.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
certificateExpiry: PropTypes.string,
};
export default StatusBoxes;
export default UptimeStatusBoxes;
+4 -4
View File
@@ -1,11 +1,11 @@
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import MonitorHeader from "./Components/MonitorHeader";
import StatusBoxes from "./Components/StatusBoxes";
import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
import TimeFramePicker from "./Components/TimeFramePicker";
import ChartBoxes from "./Components/ChartBoxes";
import ResponseTimeChart from "./Components/Charts/ResponseTimeChart";
import ResponseTable from "./Components/ResponseTable";
import UptimeStatusBoxes from "./Components/UptimeStatusBoxes";
// MUI Components
import { Stack } from "@mui/material";
@@ -80,12 +80,12 @@ const UptimeDetails = () => {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorHeader
<MonitorStatusHeader
isAdmin={isAdmin}
shouldRender={!monitorIsLoading}
monitor={monitor}
/>
<StatusBoxes
<UptimeStatusBoxes
shouldRender={!monitorIsLoading}
monitor={monitor}
certificateExpiry={certificateExpiry}
@@ -1,6 +1,5 @@
// Components
import { Box, Stack, CircularProgress } from "@mui/material";
import { Heading } from "../../../../../Components/Heading";
import { Box, Stack } from "@mui/material";
import DataTable from "../../../../../Components/Table";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
@@ -12,8 +11,8 @@ import { useState } from "react";
import SearchComponent from "../SearchComponent";
import CircularCount from "../../../../../Components/CircularCount";
import LoadingSpinner from "../LoadingSpinner";
import MonitorCountHeader from "../../../../../Components/MonitorCountHeader";
import UptimeDataTableSkeleton from "./skeleton";
import { Heading } from "../../../../../Components/Heading";
// Utils
import { useTheme } from "@emotion/react";
@@ -216,19 +215,21 @@ const UptimeDataTable = (props) => {
flex={1}
py={theme.spacing(8)}
>
<MonitorCountHeader
shouldRender={!monitorsAreLoading}
monitorCount={monitorCount}
heading="Uptime monitors"
sx={{ mb: theme.spacing(8) }}
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
gap={theme.spacing(2)}
>
<Heading component="h2">Uptime monitors</Heading>
<CircularCount count={monitorCount} />
<SearchComponent
monitorsAreLoading={monitorsAreLoading}
monitors={monitors}
onSearchChange={setSearch}
setIsSearching={setIsSearching}
/>
</MonitorCountHeader>
</Stack>
<MonitorDataTable
shouldRender={!monitorsAreLoading}
@@ -255,6 +256,7 @@ UptimeDataTable.propTypes = {
monitors: PropTypes.array,
filteredMonitors: PropTypes.array,
monitorCount: PropTypes.number,
monitorsAreLoading: PropTypes.bool,
sort: PropTypes.shape({
field: PropTypes.string,
order: PropTypes.oneOf(["asc", "desc"]),
+1 -1
View File
@@ -100,7 +100,7 @@ const UptimeMonitors = () => {
});
const totalMonitors = monitorsSummary?.totalMonitors ?? 0;
if (!isLoading && monitors?.length === 0) {
if (!isLoading && !monitorsAreLoading && monitors?.length === 0) {
return (
<Fallback
vowelStart={true}
+1 -1
View File
@@ -12,7 +12,7 @@ import Alert from "../Components/Alert";
*/
export const createToast = ({
variant = "secondary",
variant = "info",
title,
body,
hasIcon = false,