mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-18 23:48:43 -05:00
Refactor pagespeed details
This commit is contained in:
@@ -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;
|
||||
@@ -0,0 +1,40 @@
|
||||
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 PropTypes from "prop-types";
|
||||
const ConfigButton = ({ shouldRender, monitorId }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return (
|
||||
<Box alignSelf="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/uptime/configure/${monitorId}`)}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
/* Should always be contrastText for the button color */
|
||||
stroke: theme.palette.secondary.contrastText,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsIcon /> Configure
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigButton.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
monitorId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ConfigButton;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import PulseDot from "../Animated/PulseDot";
|
||||
import Dot from "../Dot";
|
||||
import { useTheme } from "@emotion/react";
|
||||
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();
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack>
|
||||
<Typography variant="h1">{monitor.name}</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Typography variant="h2">
|
||||
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Dot />
|
||||
<Typography>
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ConfigButton
|
||||
shouldRender={isAdmin}
|
||||
monitorId={monitor._id}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorHeader.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MonitorHeader;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton
|
||||
height={40}
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
/>
|
||||
<Skeleton
|
||||
height={40}
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
+9
-6
@@ -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;
|
||||
+4
-2
@@ -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;
|
||||
@@ -0,0 +1,45 @@
|
||||
import ChartBox from "../../../../../Components/Charts/ChartBox";
|
||||
import AreaChart from "../Charts/AreaChart";
|
||||
import AreaChartLegend from "../Charts/AreaChartLegend";
|
||||
import ScoreIcon from "../../../../../assets/icons/monitor-graph-line.svg?react";
|
||||
import { Stack } from "@mui/material";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) => {
|
||||
const theme = useTheme();
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeedAreaChart;
|
||||
@@ -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,45 @@
|
||||
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";
|
||||
|
||||
const PerformanceReport = ({ audits }) => {
|
||||
const theme = useTheme();
|
||||
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,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;
|
||||
}
|
||||
@@ -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,46 @@ const PageSpeedDetails = () => {
|
||||
performance: true,
|
||||
seo: true,
|
||||
});
|
||||
|
||||
const handleMetrics = (id) => {
|
||||
setMetrics((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return "funk";
|
||||
}
|
||||
|
||||
if (typeof audits === "undefined") {
|
||||
return "No Data";
|
||||
}
|
||||
|
||||
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 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";
|
||||
|
||||
Reference in New Issue
Block a user