remove unused

This commit is contained in:
Alex Holliday committed 2026-01-28 20:20:18 +00:00
1 parent bb8cc8b464
commit 396d5b369b
22 files changed
-2694

No files matched your search

@@ -1,330 +0,0 @@
import PropTypes from "prop-types";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { Box, Stack, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
const config = {
seo: {
id: "seo",
text: "SEO",
color: "secondary",
},
performance: {
id: "performance",
text: "performance",
color: "success",
},
bestPractices: {
id: "bestPractices",
text: "best practices",
color: "warning",
},
accessibility: {
id: "accessibility",
text: "accessibility",
color: "accent",
},
};
/**
* Custom tooltip for the area chart.
* @param {Object} props
* @param {boolean} props.active - Whether the tooltip is active.
* @param {Array} props.payload - The payload data for the tooltip.
* @param {string} props.label - The label for the tooltip.
* @returns {JSX.Element|null} The tooltip element or null if not active.
*/
const CustomToolTip = ({ active, payload, label, config }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
if (active && payload && payload.length) {
return (
<Box
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
</Typography>
{Object.keys(config)
.reverse()
.map((key) => {
const { color } = config[key];
const dotColor = theme.palette[color].main;
return (
<Stack
key={`${key}-tooltip`}
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mt={theme.spacing(1)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Box
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
textTransform="capitalize"
sx={{ opacity: 0.8 }}
>
{config[key].text}
</Typography>{" "}
<Typography component="span">{payload[0].payload[key]}</Typography>
</Stack>
);
})}
</Box>
);
}
return null;
};
CustomToolTip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.array,
label: PropTypes.string,
config: PropTypes.object,
};
/**
* Processes data to insert gaps with null values based on the interval.
* @param {Array} data
* @param {number} interval - The interval in milliseconds for gaps.
* @returns {Array} The formatted data with gaps.
*/
const processDataWithGaps = (data, interval) => {
if (data.length === 0) return [];
let formattedData = [];
let last = new Date(data[0].createdAt).getTime();
// Helper function to add a null entry
const addNullEntry = (timestamp) => {
formattedData.push({
accessibility: "N/A",
bestPractices: "N/A",
performance: "N/A",
seo: "N/A",
createdAt: timestamp,
});
};
data.forEach((entry) => {
const current = new Date(entry.createdAt).getTime();
if (current - last > interval * 2) {
// Insert null entries for each interval
let temp = last + interval;
while (temp < current) {
addNullEntry(new Date(temp).toISOString());
temp += interval;
}
}
formattedData.push(entry);
last = current;
});
return formattedData;
};
/**
* Custom tick component to render ticks on the XAxis.
*
* @param {Object} props
* @param {number} props.x - The x coordinate for the tick.
* @param {number} props.y - The y coordinate for the tick.
* @param {Object} props.payload - The data object containing the tick value.
* @param {number} props.index - The index of the tick in the array of ticks.
*
* @returns {JSX.Element|null} The tick element or null if the tick should be hidden.
*/
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
// Render nothing for the first tick
if (index === 0) return null;
return (
<Text
x={x}
y={y + 8}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.shape({
value: PropTypes.string.isRequired,
}),
index: PropTypes.number,
};
/**
* A chart displaying pagespeed details over time.
* @param {Object} props
* @param {Array} props.data - The data to display in the chart.
* @param {number} props.interval - The interval in milliseconds for processing gaps.
* @returns {JSX.Element} The area chart component.
*/
const PageSpeedAreaChart = ({ data, monitor, metrics }) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
const memoizedData = useMemo(
() => processDataWithGaps(data, monitor.interval),
[data[0]]
);
const filteredConfig = Object.keys(config).reduce((result, key) => {
if (metrics[key]) {
result[key] = config[key];
}
return result;
}, {});
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={215}
>
<AreaChart
width="100%"
height="100%"
data={memoizedData}
margin={{ top: 10 }}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="createdAt"
tick={<CustomTick />}
axisLine={false}
tickLine={false}
height={18}
minTickGap={0}
interval="equidistantPreserveStart"
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip config={filteredConfig} />}
/>
<defs>
{Object.values(filteredConfig).map(({ id, color }) => {
/* TODO not working? */
const startColor = theme.palette[color].main;
const endColor = theme.palette[color].lowContrast;
return (
<linearGradient
id={id}
x1="0"
y1="0"
x2="0"
y2="1"
key={id}
>
<stop
offset="0%"
stopColor={startColor}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={endColor}
stopOpacity={0}
/>
</linearGradient>
);
})}
</defs>
{Object.keys(filteredConfig).map((key) => {
const { color } = filteredConfig[key];
const strokeColor = theme.palette[color].main;
const bgColor = theme.palette.primary.main;
return (
<Area
connectNulls
key={key}
dataKey={key}
stackId={1}
stroke={strokeColor}
strokeWidth={isHovered ? 2.5 : 1.5}
fill={`url(#${filteredConfig[key].id})`}
activeDot={{ stroke: bgColor, fill: strokeColor, r: 4.5 }}
/>
);
})}
</AreaChart>
</ResponsiveContainer>
);
};
PageSpeedAreaChart.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
createdAt: PropTypes.string.isRequired,
accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})
).isRequired,
monitor: PropTypes.object.isRequired,
metrics: PropTypes.object,
};
export default PageSpeedAreaChart;
@@ -1,62 +0,0 @@
import { Box, Typography, Divider } from "@mui/material";
import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
import Icon from "@/Components/v1/Icon";
import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const AreaChartLegend = ({ metrics, handleMetrics }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<LegendBox
icon={
<Icon
name="Ruler"
size={20}
/>
}
header="Metrics"
>
<Box>
<Typography
fontSize={11}
fontWeight={500}
>
{t("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;
@@ -1,327 +0,0 @@
import PropTypes from "prop-types";
import { PieChart as MuiPieChart } from "@mui/x-charts/PieChart";
import { useDrawingArea } from "@mui/x-charts";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { Box } from "@mui/material";
/**
* Renders a centered label within a pie chart.
*
* @param {Object} props
* @param {string | number} props.value - The value to display in the label.
* @param {string} props.color - The color of the text.
* @returns {JSX.Element}
*/
const PieCenterLabel = ({ value, color, setExpand }) => {
const { width, height } = useDrawingArea();
return (
<g
transform={`translate(${width / 2}, ${height / 2})`}
onMouseEnter={() => setExpand(true)}
>
<circle
cx={0}
cy={0}
r={width / 4}
fill="transparent"
/>
<text
className="pie-label"
style={{
fill: color,
fontSize: 48,
textAnchor: "middle",
dominantBaseline: "central",
userSelect: "none",
}}
>
{value}
</text>
</g>
);
};
PieCenterLabel.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
color: PropTypes.string,
setExpand: PropTypes.func,
};
/**
* A component that renders a label on a pie chart slice.
* The label is positioned relative to the center of the pie chart and is optionally highlighted.
*
* @param {Object} props
* @param {number} props.value - The value to display inside the pie slice.
* @param {number} props.startAngle - The starting angle of the pie slice in degrees.
* @param {number} props.endAngle - The ending angle of the pie slice in degrees.
* @param {string} props.color - The color of the label text when highlighted.
* @param {boolean} props.highlighted - Determines if the label should be highlighted or not.
* @returns {JSX.Element}
*/
const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
const { width, height } = useDrawingArea();
// Compute the midpoint angle in radians
const angle = (((startAngle + endAngle) / 2) * Math.PI) / 180;
const radius = height / 4; // length from center of the circle to where the text is positioned
// Calculate x and y positions
const x = Math.sin(angle) * radius;
const y = -Math.cos(angle) * radius;
return (
<g transform={`translate(${width / 2}, ${height / 2})`}>
<text
className="pie-value-label"
x={x}
y={y}
style={{
fill: highlighted ? color : "rgba(0,0,0,0)",
fontSize: 12,
textAnchor: "middle",
dominantBaseline: "central",
userSelect: "none",
pointerEvents: "none",
}}
>
+{value}
</text>
</g>
);
};
// Validate props using PropTypes
PieValueLabel.propTypes = {
value: PropTypes.number.isRequired,
startAngle: PropTypes.number.isRequired,
endAngle: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
highlighted: PropTypes.bool.isRequired,
};
/**
* Weight constants for different performance metrics.
* @type {Object}
*/
const weights = {
fcp: 10,
si: 10,
lcp: 25,
tbt: 30,
cls: 25,
};
const PieChart = ({ audits }) => {
const theme = useTheme();
const [highlightedItem, setHighLightedItem] = useState(null);
const [expand, setExpand] = useState(false);
/**
* Retrieves color properties based on the performance value.
*
* @param {number} value - The performance score used to determine the color properties.
* @returns {{stroke: string, strokeBg: string, text: string, bg: string}} The color properties for the given performance value.
*/
const getColors = (value) => {
if (value >= 90 && value <= 100)
return {
stroke: theme.palette.success.main,
strokeBg: theme.palette.success.lowContrast,
text: theme.palette.success.contrastText,
bg: theme.palette.success.lowContrast,
};
else if (value >= 50 && value < 90)
return {
stroke: theme.palette.warning.main,
strokeBg: theme.palette.warning.lowContrast,
text: theme.palette.warning.contrastText,
bg: theme.palette.warning.lowContrast,
};
else if (value >= 0 && value < 50)
return {
stroke: theme.palette.error.contrastText,
strokeBg: theme.palette.error.lowContrast,
text: theme.palette.error.contrastText,
bg: theme.palette.error.lowContrast,
};
return {
stroke: theme.palette.tertiary.contrastText,
strokeBg: theme.palette.tertiary.contrastText,
text: theme.palette.tertiary.contrastText,
bg: theme.palette.tertiary.main,
};
};
/**
* Calculates and formats the data needed for rendering a pie chart based on audit scores and weights.
* This function generates properties for each pie slice, including angles, radii, and colors.
* It also calculates performance based on the weighted values.
*
* @returns {Array<Object>} An array of objects, each representing the properties for a slice of the pie chart.
* @returns {number} performance - A variable updated with the rounded sum of weighted values.
*/
let performance = 0;
const getPieData = (audits) => {
if (typeof audits === "undefined") return undefined;
let data = [];
let startAngle = 0;
const padding = 3; // padding between arcs
const max = 360 - padding * (Object.keys(audits).length - 1); // _id is a child of audits
Object.keys(audits).forEach((key) => {
if (audits[key].score) {
let value = audits[key].score * weights[key];
let endAngle = startAngle + (weights[key] * max) / 100;
let theme = getColors(audits[key].score * 100);
data.push({
id: key,
data: [
{
value: value,
color: theme.stroke,
label: key.toUpperCase(),
},
{
value: weights[key] - value,
color: theme.strokeBg,
label: "",
},
],
arcLabel: (item) => `${item.label}`,
arcLabelRadius: 95,
startAngle: startAngle,
endAngle: endAngle,
innerRadius: 73,
outerRadius: 80,
cornerRadius: 2,
highlightScope: { faded: "global", highlighted: "series" },
faded: {
innerRadius: 73,
outerRadius: 80,
additionalRadius: -20,
arcLabelRadius: 5,
},
cx: pieSize.width / 2,
});
performance += Math.floor(value);
startAngle = endAngle + padding;
}
});
return data;
};
const pieSize = { width: 230, height: 230 };
const pieData = getPieData(audits);
const colorMap = getColors(performance);
return (
<Box
onMouseLeave={() => setExpand(false)}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
}}
>
{expand ? (
<MuiPieChart
series={[
{
data: [
{
value: 100,
color: colorMap.bg,
},
],
outerRadius: 77,
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}
/>
))}
</MuiPieChart>
) : (
<MuiPieChart
series={[
{
data: [
{
value: 100,
color: colorMap.bg,
},
],
outerRadius: 77,
cx: pieSize.width / 2,
},
{
data: [
{
value: performance,
color: colorMap.stroke,
},
],
innerRadius: 73,
outerRadius: 80,
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}
/>
</MuiPieChart>
)}
</Box>
);
};
PieChart.propTypes = {
audits: PropTypes.object,
};
export default PieChart;
@@ -1,101 +0,0 @@
import { Stack, Box, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import PropTypes from "prop-types";
const PieChartLegend = ({ audits }) => {
const theme = useTheme();
return (
<LegendBox
icon={
<Icon
name="Gauge"
size={20}
/>
}
header="Performance metrics"
sx={{ flex: 1 }}
>
{typeof audits !== "undefined" &&
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;
@@ -1,60 +0,0 @@
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import AreaChart from "../Charts/AreaChart.jsx";
import AreaChartLegend from "../Charts/AreaChartLegend.jsx";
import SkeletonLayout from "./skeleton.jsx";
import Icon from "@/Components/v1/Icon";
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 />;
}
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
return (
<Stack
direction="row"
gap={theme.spacing(10)}
>
<ChartBox
justifyContent="flex-start"
icon={
<Icon
name="TrendingUp"
size={20}
/>
}
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;
@@ -1,17 +0,0 @@
import { Box, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Box
height={"100%"}
width={"100%"}
>
<Skeleton
height={"100%"}
width={"100%"}
/>
</Box>
);
};
export default SkeletonLayout;
@@ -1,49 +0,0 @@
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
import StatBox from "@/Components/v1/StatBox/index.jsx";
import { Typography } from "@mui/material";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js";
import { useTranslation } from "react-i18next";
const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => {
const lastChecked = monitorStats?.lastCheckTimestamp
? Date.now() - monitorStats.lastCheckTimestamp
: 0;
// Determine time since last failure
const timeOfLastFailure = monitorStats?.timeOfLastFailure;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt);
const streakTime = getHumanReadableDuration(timeSinceLastFailure);
const time = getHumanReadableDuration(lastChecked);
const { t } = useTranslation();
return (
<StatusBoxes shouldRender={shouldRender}>
<StatBox
heading="checks since"
subHeading={
<>
{streakTime}
<Typography component="span">{t("ago")}</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{time}
<Typography component="span">{t("ago")}</Typography>
</>
}
/>
</StatusBoxes>
);
};
export default PageSpeedStatusBoxes;
@@ -1,61 +0,0 @@
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import PieChart from "../Charts/PieChart.jsx";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import PieChartLegend from "../Charts/PieChartLegend.jsx";
import SkeletonLayout from "./skeleton.jsx";
import { useTranslation } from "react-i18next";
const PerformanceReport = ({ shouldRender, audits }) => {
const theme = useTheme();
const { t } = useTranslation();
if (!shouldRender) {
return <SkeletonLayout />;
}
return (
<ChartBox
icon={
<Icon
name="Layers"
size={20}
/>
}
header="Performance report"
Legend={<PieChartLegend audits={audits} />}
borderRadiusRight={16}
>
<PieChart audits={audits} />
<Typography
variant="body1"
mt="auto"
sx={{
textAlign: "center",
}}
>
{t("pageSpeedDetailsPerformanceReport")}{" "}
<Typography
component="span"
fontSize="inherit"
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontWeight: 500,
textDecoration: "underline",
textUnderlineOffset: 2,
transition: "all 200ms",
cursor: "pointer",
"&:hover": {
textUnderlineOffset: 4,
},
}}
>
{t("pageSpeedDetailsPerformanceReportCalculator")}
</Typography>
</Typography>
</ChartBox>
);
};
export default PerformanceReport;
@@ -1,17 +0,0 @@
import { Box, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Box
height={"100%"}
width={"100%"}
>
<Skeleton
height={"100%"}
width={"100%"}
/>
</Box>
);
};
export default SkeletonLayout;
-120
View File
@@ -1,120 +0,0 @@
// Components
import { Stack, Typography } from "@mui/material";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes/index.jsx";
import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
import PageSpeedAreaChart from "./Components/PageSpeedAreaChart/index.jsx";
import PerformanceReport from "./Components/PerformanceReport/index.jsx";
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import { useParams } from "react-router-dom";
import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
// Constants
const BREADCRUMBS = [
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `` },
];
const PageSpeedDetails = () => {
const theme = useTheme();
const { t } = useTranslation();
const isAdmin = useIsAdmin();
const { monitorId } = useParams();
// Local state
const [metrics, setMetrics] = useState({
accessibility: true,
bestPractices: true,
performance: true,
seo: true,
});
const [trigger, setTrigger] = useState(false);
// Network
const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({
monitorId,
dateRange: "day",
updateTrigger: trigger,
});
// Handlers
const handleMetrics = (id) => {
setMetrics((prev) => ({ ...prev, [id]: !prev[id] }));
};
const triggerUpdate = () => {
setTrigger(!trigger);
};
if (networkError === true) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
// Empty view, displayed when loading is complete and there are no checks
if (!isLoading && monitor?.checks?.length === 0) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorDetailsControlHeader
path={"pagespeed"}
isLoading={isLoading}
isAdmin={isAdmin}
monitor={monitor}
triggerUpdate={triggerUpdate}
/>
<GenericFallback>
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
</GenericFallback>
</Stack>
);
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorDetailsControlHeader
path={"pagespeed"}
isLoading={isLoading}
isAdmin={isAdmin}
monitor={monitor}
triggerUpdate={triggerUpdate}
/>
<PageSpeedStatusBoxes
shouldRender={!isLoading}
monitorStats={monitorStats}
/>
<MonitorTimeFrameHeader
shouldRender={!isLoading}
dateRange={"day"}
hasDateRange={false}
/>
<PageSpeedAreaChart
shouldRender={!isLoading}
monitor={monitor}
metrics={metrics}
handleMetrics={handleMetrics}
/>
<PerformanceReport
shouldRender={!isLoading}
audits={monitor?.checks[0]?.audits}
/>
</Stack>
);
};
export default PageSpeedDetails;
@@ -1,331 +0,0 @@
import PropTypes from "prop-types";
import Icon from "@/Components/v1/Icon";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtilsLegacy.js";
import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import IconBox from "@/Components/v1/IconBox/index.jsx";
/**
* CustomToolTip displays a tooltip with formatted date and score information.
* @param {Object} props
* @param {Array} props.payload - Data to display in the tooltip
* @returns {JSX.Element} The rendered tooltip component
*/
const CustomToolTip = ({ payload }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Box
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(
payload[0]?.payload.createdAt,
"ddd, MMMM D, YYYY, h:mm A",
uiTimezone
)}
</Typography>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mt={theme.spacing(1)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Box
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={payload[0]?.color}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
textTransform="capitalize"
sx={{ opacity: 0.8 }}
>
{payload[0]?.name}
</Typography>{" "}
<Typography component="span">{payload[0]?.payload.score}</Typography>
</Stack>
</Box>
);
};
CustomToolTip.propTypes = {
payload: PropTypes.array,
};
/* TODO separate utils in folder*/
/**
* Processes the raw data to include a score for each entry.
* @param {Array<Object>} data - The raw data array.
* @returns {Array<Object>} - The formatted data array with scores.
*/
const processData = (data) => {
if (data.length === 0) return [];
let formattedData = [];
const calculateScore = (entry) => {
return (
(entry.accessibility + entry.bestPractices + entry.performance + entry.seo) / 4
);
};
data.forEach((entry) => {
entry = { ...entry, score: calculateScore(entry) };
formattedData.push(entry);
});
return formattedData;
};
/* TODO separate component*/
/**
* Renders an area chart displaying page speed scores.
* @param {Object} props
* @param {Array<Object>} props.data - The raw data to be displayed in the chart.
* @param {string} props.status - The status of the page speed which determines the chart's color scheme.
* @returns {JSX.Element} - The rendered area chart.
*/
const PagespeedAreaChart = ({ data, status }) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
const formattedData = processData(data);
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={85}
>
<AreaChart
width="100%"
height="100%"
data={formattedData}
margin={{ top: 10, bottom: -5 }}
style={{ cursor: "pointer" }}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
/>
<defs>
<linearGradient
id={`pagespeed-chart-${status}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette[themeColor].lowContrast}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette[themeColor].main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
dataKey="score"
stroke={theme.palette[themeColor].lowContrast}
strokeWidth={isHovered ? 2.5 : 1.5}
fill={`url(#pagespeed-chart-${status})`}
activeDot={{
stroke: theme.palette[themeColor].main,
fill: theme.palette[themeColor].lowContrast,
r: 4.5,
}}
/>
</AreaChart>
</ResponsiveContainer>
);
};
PagespeedAreaChart.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
accessibility: PropTypes.number.isRequired,
bestPractices: PropTypes.number.isRequired,
performance: PropTypes.number.isRequired,
seo: PropTypes.number.isRequired,
})
).isRequired,
status: PropTypes.string.isRequired,
};
/* TODO separate component */
/**
* Renders a card displaying monitor details and an area chart.
* @param {Object} props
* @param {Object} props.monitor - The monitor data to be displayed in the card.
* @returns {JSX.Element} - The rendered card.
*/
const Card = ({ monitor }) => {
const { determineState, pagespeedStatusMsg } = useMonitorUtils();
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
const monitorState = determineState(monitor);
return (
<Grid
item
lg={6}
flexGrow={1}
>
<Box
position="relative"
p={theme.spacing(8)}
onClick={() => navigate(`/pagespeed/${monitor.id}`)}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
sx={{
display: "grid",
gridTemplateColumns: "34px 2fr 1fr",
columnGap: theme.spacing(5),
gridTemplateRows: "34px 1fr 3fr",
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.tertiary.main,
},
"& path": {
transition: "stroke-width 400ms ease",
},
}}
>
<IconBox>
<Icon
name="Gauge"
size={20}
/>
</IconBox>
<Typography
component="h2"
variant="monitorUrl"
fontWeight={500}
alignSelf="center"
>
{monitor.name}
</Typography>
<StatusLabel
status={monitorState}
text={pagespeedStatusMsg[monitorState] || "Pending..."}
customStyles={{
width: "max-content",
textTransform: "capitalize",
alignSelf: "flex-start",
justifySelf: "flex-end",
}}
/>
<Typography
variant="body2"
mt={theme.spacing(-2)}
sx={{ gridColumnStart: 2 }}
>
{monitor.url}
</Typography>
<Box
mx={theme.spacing(-8)}
mt={theme.spacing(4)}
mb={theme.spacing(-8)}
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
>
<PagespeedAreaChart
data={monitor?.checks?.slice().reverse()}
status={monitorState}
/>
</Box>
<Box
position="absolute"
bottom={0}
py={theme.spacing(1)}
px={theme.spacing(4)}
borderTop={1}
borderRight={1}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.tertiary.main}
sx={{
pointerEvents: "none",
userSelect: "none",
borderTopRightRadius: 8,
borderBottomLeftRadius: 4,
}}
>
<Typography
fontSize={11}
color={theme.palette.primary.contrastTextSecondary}
>
{t("checkingEvery")}{" "}
{(() => {
const { time, format } = formatDurationSplit(monitor?.interval);
return (
<>
<Typography
component="span"
fontSize={12}
color={theme.palette.primary.contrastText}
>
{time}{" "}
</Typography>
{format}
</>
);
})()}
</Typography>
</Box>
</Box>
</Grid>
);
};
Card.propTypes = {
monitor: PropTypes.object.isRequired,
};
export default Card;
@@ -1,22 +0,0 @@
import { Grid } from "@mui/material";
import Card from "../Card/index.jsx";
const MonitorGrid = ({ monitors, shouldRender = true }) => {
if (!shouldRender) return null;
return (
<Grid
container
spacing={12}
>
{monitors?.map((monitor) => (
<Card
monitor={monitor}
key={monitor._id || monitor.id}
/>
))}
</Grid>
);
};
export default MonitorGrid;
@@ -1,86 +0,0 @@
// Components
import { useState } from "react";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import { Stack } from "@mui/material";
import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
import MonitorGrid from "./Components/MonitorGrid/index.jsx";
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeedWarning.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js";
import { useFetchSettings } from "@/Hooks/settingsHooks.js";
// Constants
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
const TYPES = ["pagespeed"];
const PageSpeed = () => {
const theme = useTheme();
const isAdmin = useIsAdmin();
const [
summary,
monitorsWithChecks,
monitorsWithChecksCount,
monitorsWithChecksIsLoading,
monitorsWithChecksNetworkError,
] = useFetchMonitorsWithChecks({
types: TYPES,
limit: 10,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
monitorUpdateTrigger: null,
});
const [settingsData, setSettingsData] = useState(undefined);
const [isSettingsLoading, settingsError] = useFetchSettings({
setSettingsData,
setIsApiKeySet: () => {},
setIsEmailPasswordSet: () => {},
});
return (
<>
<PageStateWrapper
networkError={monitorsWithChecksNetworkError}
isLoading={monitorsWithChecksIsLoading}
items={monitorsWithChecks}
type="pageSpeed"
fallbackLink="/pagespeed/create"
fallbackChildren={
isAdmin &&
settingsData &&
!settingsData.pagespeedApiKey && (
<FallbackPageSpeedWarning settingsData={settingsData} />
)
}
>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
isLoading={monitorsWithChecksIsLoading}
path="/pagespeed/create"
/>
<MonitorCountHeader
isLoading={monitorsWithChecksIsLoading}
monitorCount={monitorsWithChecksCount}
sx={{ mb: theme.spacing(8) }}
/>
<MonitorGrid
size={6}
shouldRender={!monitorsWithChecksIsLoading}
monitors={monitorsWithChecks}
/>
</Stack>
</PageStateWrapper>
</>
);
};
export default PageSpeed;
@@ -1,44 +0,0 @@
import { logger } from "../../../../Utils/Logger.js";
import { useEffect, useState } from "react";
import { networkService } from "../../../../main.jsx";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
const useCertificateFetch = ({
monitor,
monitorId,
certificateDateFormat,
uiTimezone,
}) => {
const [certificateExpiry, setCertificateExpiry] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchCertificate = async () => {
if (monitor?.type !== "http") {
return;
}
try {
setIsLoading(true);
const res = await networkService.getCertificateExpiry({
monitorId: monitorId,
});
if (res?.data?.data?.certificateDate) {
const date = res.data.data.certificateDate;
setCertificateExpiry(
formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A"
);
}
} catch (error) {
setCertificateExpiry("N/A");
logger.error(error);
} finally {
setIsLoading(false);
}
};
fetchCertificate();
}, [monitorId, certificateDateFormat, uiTimezone, monitor]);
return [certificateExpiry, isLoading];
};
export default useCertificateFetch;
-190
View File
@@ -1,190 +0,0 @@
// Components
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
import ChartBoxes from "./Components/ChartBoxes/index.jsx";
import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx";
import ResponseTable from "./Components/ResponseTable/index.jsx";
import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx";
import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
// Utils
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks.js";
import useCertificateFetch from "./Hooks/useCertificateFetch.jsx";
import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks.js";
import { useTranslation } from "react-i18next";
// Constants
const BREADCRUMBS = [
{ name: "uptime", path: "/uptime" },
{ name: "details", path: "" },
// { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy
];
const certificateDateFormat = "MMM D, YYYY h A";
const UptimeDetails = () => {
// Redux state
const uiTimezone = useSelector((state) => state.ui.timezone);
// Local state
const [dateRange, setDateRange] = useState("recent");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [trigger, setTrigger] = useState(false);
// Utils
const dateFormat =
dateRange === "day" || dateRange === "recent" ? "MMM D, h A" : "MMM D";
const { monitorId } = useParams();
const theme = useTheme();
const isAdmin = useIsAdmin();
const { t } = useTranslation();
const [monitorData, monitorStats, monitorIsLoading, monitorNetworkError] =
useFetchUptimeMonitorById({
monitorId,
dateRange,
trigger,
});
const monitor = monitorData?.monitor;
const [certificateExpiry, certificateIsLoading] = useCertificateFetch({
monitor,
monitorId,
certificateDateFormat,
uiTimezone,
});
const monitorType = monitor?.type;
const [checks, checksCount, checksAreLoading, checksNetworkError] =
useFetchChecksByMonitor({
monitorId,
type: monitorType,
sortOrder: "desc",
limit: null,
dateRange,
filter: null,
page,
rowsPerPage,
});
// Handlers
const triggerUpdate = () => {
setTrigger(!trigger);
};
const handlePageChange = (_, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(event.target.value);
};
if (monitorNetworkError || checksNetworkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
// Empty view, displayed when loading is complete and there are no checks
if (!monitorIsLoading && !checksAreLoading && checksCount === 0) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorDetailsControlHeader
path={"uptime"}
isAdmin={isAdmin}
isLoading={monitorIsLoading}
monitor={monitor}
triggerUpdate={triggerUpdate}
/>
<UptimeStatusBoxes
isLoading={monitorIsLoading}
monitor={monitor}
monitorStats={monitorStats}
certificateExpiry={certificateExpiry}
/>
<MonitorTimeFrameHeader
isLoading={monitorIsLoading}
hasDateRange={true}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<GenericFallback>
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
</GenericFallback>
</Stack>
);
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorDetailsControlHeader
path={"uptime"}
isAdmin={isAdmin}
isLoading={monitorIsLoading}
monitor={monitor}
triggerUpdate={triggerUpdate}
/>
<UptimeStatusBoxes
isLoading={monitorIsLoading}
monitor={monitor}
monitorStats={monitorStats}
certificateExpiry={certificateExpiry}
/>
<MonitorTimeFrameHeader
isLoading={monitorIsLoading}
hasDateRange={true}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<ChartBoxes
isLoading={monitorIsLoading}
monitorData={monitorData}
uiTimezone={uiTimezone}
dateRange={dateRange}
dateFormat={dateFormat}
/>
<ResponseTimeChart
isLoading={monitorIsLoading}
groupedChecks={monitorData?.groupedChecks}
dateRange={dateRange}
/>
<ResponseTable
isLoading={checksAreLoading}
checks={checks}
uiTimezone={uiTimezone}
page={page}
setPage={handlePageChange}
rowsPerPage={rowsPerPage}
setRowsPerPage={handleChangeRowsPerPage}
checksCount={checksCount}
/>
</Stack>
);
};
export default UptimeDetails;
@@ -1,162 +0,0 @@
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
import { useMemo } from "react";
import { Box, Button } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import { useTranslation } from "react-i18next";
/**
* Filter Component
*
* A high-level component that provides filtering options for type, status, and state.
* It allows users to select multiple options for each filter and reset the filters.
*
* @component
* @param {Object} props - The component props.
* @param {string[]} props.selectedTypes - An array of selected type values.
* @param {function} props.setSelectedTypes - A function to set the selected type values.
* @param {string[]} props.selectedStatus - An array of selected status values.
* @param {function} props.setSelectedStatus - A function to set the selected status values.
* @param {string[]} props.selectedState - An array of selected state values.
* @param {function} props.setSelectedState - A function to set the selected state values.
* @param {function} props.setToFilterStatus - A function to set the filter status based on selected status values.
* @param {function} props.setToFilterActive - A function to set the filter active state based on selected state values.
* @param {function} props.handleReset - A function to reset all filters.
*
* @returns {JSX.Element} The rendered Filter component.
*/
const getTypeOptions = () => [
{ value: "http", label: "HTTP(S)" },
{ value: "ping", label: "Ping" },
{ value: "docker", label: "Docker" },
{ value: "port", label: "Port" },
{ value: "game", label: "Game" },
];
// These functions were moved inline to ensure translations are applied correctly
const Filter = ({
selectedTypes,
setSelectedTypes,
selectedStatus,
setSelectedStatus,
selectedState,
setSelectedState,
setToFilterStatus,
setToFilterActive,
handleReset,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const typeOptions = getTypeOptions();
// Create status options with translations
const statusOptions = [
{ value: "Up", label: t("monitorStatus.up") },
{ value: "Down", label: t("monitorStatus.down") },
];
// Create state options with translations
const stateOptions = [
{ value: "Active", label: t("monitorState.active") },
{ value: "Paused", label: t("monitorState.paused") },
];
const handleTypeChange = (event) => {
const selectedValues = event.target.value;
setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined);
};
const handleStatusChange = (event) => {
const selectedValues = event.target.value;
setSelectedStatus(selectedValues.length > 0 ? selectedValues : undefined);
if (selectedValues.length === 0 || selectedValues.length === 2) {
setToFilterStatus(undefined);
} else {
setToFilterStatus(selectedValues[0] === "Up" ? "true" : "false");
}
};
const handleStateChange = (event) => {
const selectedValues = event.target.value;
setSelectedState(selectedValues);
if (selectedValues.length === 0 || selectedValues.length === 2) {
setToFilterActive(undefined);
} else {
setToFilterActive(selectedValues[0] === "Active" ? "true" : "false");
}
};
const isFilterActive = useMemo(() => {
return (
(selectedTypes?.length ?? 0) > 0 ||
(selectedState?.length ?? 0) > 0 ||
(selectedStatus?.length ?? 0) > 0
);
}, [selectedState, selectedTypes, selectedStatus]);
return (
<Box
sx={{
display: "flex",
flexDirection: "row",
alignItems: "center",
m: theme.spacing(2),
ml: theme.spacing(4),
gap: theme.spacing(2),
}}
>
<FilterHeader
header={t("type")}
options={typeOptions}
value={selectedTypes}
onChange={handleTypeChange}
/>
<FilterHeader
header={t("status")}
options={statusOptions}
value={selectedStatus}
onChange={handleStatusChange}
/>
<FilterHeader
header={t("state")}
options={stateOptions}
value={selectedState}
onChange={handleStateChange}
/>
<Button
color={theme.palette.primary.contrastText}
onClick={handleReset}
variant="contained"
endIcon={
<Icon
name="X"
size={18}
/>
}
sx={{
visibility: isFilterActive ? "visible" : "hidden",
}}
>
{t("reset")}
</Button>
</Box>
);
};
Filter.propTypes = {
selectedTypes: PropTypes.arrayOf(PropTypes.string),
setSelectedTypes: PropTypes.func.isRequired,
selectedStatus: PropTypes.arrayOf(PropTypes.string),
setSelectedStatus: PropTypes.func.isRequired,
selectedState: PropTypes.arrayOf(PropTypes.string),
setSelectedState: PropTypes.func.isRequired,
setToFilterStatus: PropTypes.func.isRequired,
setToFilterActive: PropTypes.func.isRequired,
handleReset: PropTypes.func.isRequired,
};
export default Filter;
@@ -1,61 +0,0 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
/**
* 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.spacing(12)}
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>
<Skeleton
variant="rounded"
width="100%"
height="100%"
flex={1}
/>
</>
);
};
export default SkeletonLayout;
@@ -1,42 +0,0 @@
import PropTypes from "prop-types";
import { Stack } from "@mui/material";
import StatusBox from "./statusBox.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import SkeletonLayout from "./skeleton.jsx";
const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
const theme = useTheme();
const { t } = useTranslation();
if (!shouldRender) return <SkeletonLayout shouldRender={shouldRender} />;
return (
<Stack
gap={theme.spacing(8)}
direction="row"
justifyContent="space-between"
>
<StatusBox
title={t("monitorStatus.up")}
status="up"
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title={t("monitorStatus.down")}
status="down"
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title={t("monitorStatus.paused")}
status="paused"
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
);
};
StatusBoxes.propTypes = {
monitorsSummary: PropTypes.object,
shouldRender: PropTypes.bool,
};
export default StatusBoxes;
@@ -1,31 +0,0 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(12)}
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>
);
};
export default SkeletonLayout;
@@ -1,115 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Icon from "@/Components/v1/Icon";
import Background from "@/assets/Images/background-grid.svg?react";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
let sharedStyles = {
position: "absolute",
right: 8,
opacity: 0.5,
"& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
};
let color;
let icon;
if (status === "up") {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(4) }}>
<Icon
name="ArrowUpRight"
size={20}
/>
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: theme.spacing(2) }}>
<Icon
name="ArrowUpRight"
size={20}
/>
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<Icon
name="Clock"
size={20}
/>
</Box>
);
}
return (
<Box
position="relative"
flex={1}
border={1}
backgroundColor={theme.palette.primary.main}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
p={theme.spacing(8)}
overflow="hidden"
>
<Box
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
<Stack direction="column">
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
{icon}
</Stack>
<Stack
direction="row"
alignItems="flex-start"
fontSize={theme.typography.h1.fontSize}
fontWeight={600}
color={color}
gap={theme.spacing(1)}
>
{value}
<Typography
fontSize={theme.typography.label.fontSize}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{
opacity: 0.3,
}}
>
#
</Typography>
</Stack>
</Stack>
</Box>
);
};
StatusBox.propTypes = {
title: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
status: PropTypes.string,
};
export default StatusBox;
@@ -1,241 +0,0 @@
// Components
import { Box, Stack } from "@mui/material";
import DataTable from "@/Components/v1/Table/index.jsx";
import Icon from "@/Components/v1/Icon";
import Host from "@/Components/v1/Host/index.jsx";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import BarChart from "@/Components/v1/Charts/BarChart/index.jsx";
import ActionsMenu from "@/Components/v1/ActionsMenu/index.jsx";
import LoadingSpinner from "../LoadingSpinner/index.jsx";
import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
/**
* UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities
* @param {Object} props - Component props
* @param {boolean} props.isAdmin - Whether the current user has admin privileges
* @param {boolean} props.isLoading - Loading state of the table
* @param {Array<{
* id: string,
* url: string,
* title: string,
* percentage: number,
* percentageColor: string,
* monitor: {
* id: string,
* type: string,
* checks: Array
* }
* }>} props.monitors - Array of monitor objects to display
* @param {number} props.monitorCount - Total count of monitors
* @param {Object} props.sort - Current sort configuration
* @param {string} props.sort.field - Field to sort by
* @param {'asc'|'desc'} props.sort.order - Sort direction
* @param {Function} props.setSort - Callback to update sort configuration
* @param {string} props.search - Current search query
* @param {Function} props.setSearch - Callback to update search query
* @param {boolean} props.isSearching - Whether a search is in progress
* @param {Function} props.setIsLoading - Callback to update loading state
* @param {Function} props.triggerUpdate - Callback to trigger a data refresh
* @returns {JSX.Element} Rendered component
*/
const UptimeDataTable = ({
isAdmin,
isSearching,
filteredMonitors,
sort,
setSort,
triggerUpdate,
monitorsAreLoading,
}) => {
// Utils
const navigate = useNavigate();
const { determineState } = useMonitorUtils();
const theme = useTheme();
const { t } = useTranslation();
// Local state
// Handlers
const handleSort = (field) => {
let order = "";
if (sort?.field !== field) {
order = "desc";
} else {
order = sort?.order === "asc" ? "desc" : "asc";
}
setSort({ field, order });
};
const headers = [
{
id: "name",
content: (
<Stack
gap={theme.spacing(4)}
alignItems="center"
direction="row"
onClick={() => handleSort("name")}
>
{t("host")}
<Stack
justifyContent="center"
style={{
visibility: sort?.field === "name" ? "visible" : "hidden",
}}
>
{sort?.order === "asc" ? (
<Icon
name="ArrowUp"
size={18}
/>
) : (
<Icon
name="ArrowDown"
size={18}
/>
)}
</Stack>
</Stack>
),
render: (row) => (
<Host
key={row.id}
url={row.url}
title={row.name}
percentageColor={row.percentageColor}
percentage={row.percentage}
/>
),
},
{
id: "status",
content: (
<Stack
direction="row"
gap={theme.spacing(4)}
alignItems="center"
display={"inline-flex"}
onClick={() => handleSort("status")}
>
{" "}
{t("status")}
<Stack
justifyContent="center"
style={{
visibility: sort?.field === "status" ? "visible" : "hidden",
}}
>
{sort?.order === "asc" ? (
<Icon
name="ArrowUp"
size={18}
/>
) : (
<Icon
name="ArrowDown"
size={18}
/>
)}
</Stack>
</Stack>
),
render: (row) => {
const status = determineState(row.monitor);
return (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
id: "responseTime",
content: t("responseTime"),
render: (row) => (
<Box
display="flex"
justifyContent="center"
>
<BarChart checks={row.monitor.checks.slice().reverse()} />
</Box>
),
},
{
id: "type",
content: t("type"),
render: (row) => (
<span style={{ textTransform: "uppercase" }}>
{row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type}
</span>
),
},
{
id: "actions",
content: t("actions"),
render: (row) => (
<ActionsMenu
monitor={row.monitor}
isAdmin={isAdmin}
updateRowCallback={triggerUpdate}
pauseCallback={triggerUpdate}
/>
),
},
];
if (monitorsAreLoading) {
return <TableSkeleton />;
}
return (
<Box position="relative">
<LoadingSpinner shouldRender={isSearching} />
<DataTable
headers={headers}
data={filteredMonitors}
config={{
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => {
navigate(`/uptime/${row.id}`);
},
emptyView: "No monitors found",
}}
/>
</Box>
);
};
UptimeDataTable.propTypes = {
isSearching: PropTypes.bool,
setSort: PropTypes.func,
setSearch: PropTypes.func,
triggerUpdate: PropTypes.func,
debouncedSearch: PropTypes.string,
onSearchChange: PropTypes.func,
isAdmin: PropTypes.bool,
monitors: PropTypes.array,
filteredMonitors: PropTypes.array,
monitorCount: PropTypes.number,
monitorsAreLoading: PropTypes.bool,
sort: PropTypes.shape({
field: PropTypes.string,
order: PropTypes.oneOf(["asc", "desc"]),
}),
};
export default UptimeDataTable;
-225
View File
@@ -1,225 +0,0 @@
// Required Data
// 1. Monitor summary
// 2. List of monitors filtered by search term with 25 checks each
// 2a.List of monitors must have the total number of monitors that match.
// Components
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import Greeting from "../../../Utils/greeting.jsx";
import StatusBoxes from "./Components/StatusBoxes/index.jsx";
import UptimeDataTable from "./Components/UptimeDataTable/index.jsx";
import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
import SearchComponent from "./Components/SearchComponent/index.jsx";
import Filter from "./Components/Filter/index.jsx";
import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
// MUI Components
import { Stack, Box, Button } from "@mui/material";
// Utils
import { useState, useCallback, useEffect } from "react";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "../../../Features/UI/uiSlice.js";
import PropTypes from "prop-types";
import {
useFetchMonitorsWithChecks,
useFetchMonitorsByTeamId,
} from "@/Hooks/monitorHooks.js";
import { useTranslation } from "react-i18next";
const TYPES = ["http", "ping", "docker", "port", "game"];
const CreateMonitorButton = ({ shouldRender }) => {
// Utils
const navigate = useNavigate();
const { t } = useTranslation();
if (shouldRender === false) {
return;
}
return (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="accent"
onClick={() => {
navigate("/uptime/create");
}}
>
{t("createNew")}
</Button>
</Box>
);
};
CreateMonitorButton.propTypes = {
shouldRender: PropTypes.bool,
};
const UptimeMonitors = () => {
// Redux state
const rowsPerPage = useSelector((state) => state.ui?.monitors?.rowsPerPage ?? 10);
// Local state
const [search, setSearch] = useState(undefined);
const [page, setPage] = useState(undefined);
const [sort, setSort] = useState(undefined);
const [isSearching, setIsSearching] = useState(false);
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
const [selectedTypes, setSelectedTypes] = useState(undefined);
const [selectedState, setSelectedState] = useState(undefined);
const [selectedStatus, setSelectedStatus] = useState(undefined);
const [toFilterStatus, setToFilterStatus] = useState(undefined);
const [toFilterActive, setToFilterActive] = useState(undefined);
// Utils
const theme = useTheme();
const isAdmin = useIsAdmin();
const dispatch = useDispatch();
const { t } = useTranslation();
const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }];
// Handlers
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
dispatch(
setRowsPerPage({
value: parseInt(event.target.value, 10),
table: "monitors",
})
);
setPage(0);
};
const triggerUpdate = useCallback(() => {
setMonitorUpdateTrigger((prev) => !prev);
}, []);
const handleReset = () => {
setSelectedState(undefined);
setSelectedTypes(undefined);
setSelectedStatus(undefined);
setToFilterStatus(undefined);
setToFilterActive(undefined);
};
const filterLookup = new Map([
[toFilterStatus, "status"],
[toFilterActive, "isActive"],
]);
const activeFilter = [...filterLookup].find(([key]) => key !== undefined);
const field = activeFilter?.[1] || (search ? "name" : sort?.field);
const filter = activeFilter?.[0] || search;
const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES;
const [
summary,
monitorsWithChecks,
monitorsWithChecksCount,
monitorsWithChecksIsLoading,
networkError,
] = useFetchMonitorsWithChecks({
types: effectiveTypes,
limit: 25,
page: page,
rowsPerPage: rowsPerPage,
filter: filter,
field: field,
order: sort?.order,
monitorUpdateTrigger,
});
const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({
type: ["http", "ping", "docker", "port", "game"],
});
useEffect(() => {
if (isSearching) {
setPage(undefined);
}
}, [isSearching]);
const isLoading = monitorsWithChecksIsLoading;
return (
<>
<PageStateWrapper
networkError={networkError}
isLoading={isLoading}
items={monitors}
type="uptimeMonitor"
fallbackLink="/uptime/create"
>
<Stack
className="monitors"
gap={theme.spacing(10)}
>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
isLoading={isLoading}
path="/uptime/create"
bulkPath="/uptime/bulk-import"
/>
<Greeting type="uptime" />
<StatusBoxes
monitorsSummary={summary}
shouldRender={!monitorsWithChecksIsLoading}
/>
<Stack direction={"row"}>
<MonitorCountHeader
isLoading={monitorsWithChecksIsLoading}
monitorCount={summary?.totalMonitors}
/>
<Filter
selectedTypes={selectedTypes}
setSelectedTypes={setSelectedTypes}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
setToFilterStatus={setToFilterStatus}
setToFilterActive={setToFilterActive}
handleReset={handleReset}
/>
<SearchComponent
monitors={monitors}
onSearchChange={setSearch}
setIsSearching={setIsSearching}
/>
</Stack>
<UptimeDataTable
isAdmin={isAdmin}
isSearching={isSearching}
filteredMonitors={monitorsWithChecks}
sort={sort}
setSort={setSort}
monitorsAreLoading={monitorsWithChecksIsLoading}
triggerUpdate={triggerUpdate}
/>
<Pagination
itemCount={monitorsWithChecksCount}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
</PageStateWrapper>
</>
);
};
export default UptimeMonitors;