refactor details

This commit is contained in:
Alex Holliday
2025-01-27 13:18:05 -08:00
parent 674c12af5c
commit 671d297c2d
14 changed files with 814 additions and 5 deletions
@@ -0,0 +1,134 @@
// Components
import { Stack, Typography, Box } from "@mui/material";
import ChartBox from "../Charts/ChartBox";
import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react";
import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react";
import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react";
import UpBarChart from "../Charts/UpBarChart";
import DownBarChart from "../Charts/DownBarChart";
import ResponseGaugeChart from "../Charts/ResponseGaugeChart";
// Utils
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const ChartBoxes = ({
monitor,
dateRange,
uiTimezone,
dateFormat,
hoveredUptimeData,
setHoveredUptimeData,
hoveredIncidentsData,
setHoveredIncidentsData,
}) => {
const theme = useTheme();
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<ChartBox
icon={<UptimeIcon />}
header="Uptime"
>
<Stack justifyContent="space-between">
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
: (monitor?.groupedUpChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<Box>
<Typography>
{hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"}
</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
((monitor?.upChecks?.totalChecks ?? 0) /
(monitor?.totalChecks ?? 1)) *
100
)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredUptimeData}
/>
</ChartBox>
<ChartBox
icon={<IncidentsIcon />}
header="Incidents"
>
<Box position="relative">
<Typography component="span">
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalChecks
: (monitor?.groupedDownChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<DownBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox
icon={<AverageResponseIcon />}
header="Average Response Time"
>
<ResponseGaugeChart avgResponseTime={monitor.avgResponseTime ?? 0} />
</ChartBox>
</Stack>
);
};
export default ChartBoxes;
ChartBoxes.propTypes = {
monitor: PropTypes.object.isRequired,
dateRange: PropTypes.string.isRequired,
uiTimezone: PropTypes.string.isRequired,
dateFormat: PropTypes.string.isRequired,
hoveredUptimeData: PropTypes.object,
setHoveredUptimeData: PropTypes.func,
hoveredIncidentsData: PropTypes.object,
setHoveredIncidentsData: PropTypes.func,
};
@@ -0,0 +1,74 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../../../../Components/IconBox";
import PropTypes from "prop-types";
const ChartBox = ({ children, icon, header }) => {
const theme = useTheme();
return (
<Stack
sx={{
justifyContent: "space-between",
flex: "1 30%",
gap: theme.spacing(8),
height: 300,
minWidth: 250,
padding: theme.spacing(8),
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 4,
backgroundColor: theme.palette.primary.main,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& .MuiBox-root:not(.area-tooltip) p": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 13,
},
"& .MuiBox-root > span": {
color: theme.palette.primary.contrastText,
fontSize: 20,
"& span": {
opacity: 0.8,
marginLeft: 2,
fontSize: 15,
},
},
"& .MuiStack-root": {
flexDirection: "row",
gap: theme.spacing(6),
},
"& .MuiStack-root:first-of-type": {
alignItems: "center",
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node.isRequired,
header: PropTypes.string.isRequired,
};
@@ -0,0 +1,42 @@
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
return (
<>
<text
x={x}
y={height}
dy={-3}
textAnchor="start"
fontSize={11}
>
{formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)}
</text>
<text
x={width}
y={height}
dy={-3}
textAnchor="end"
fontSize={11}
>
{formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)}
</text>
</>
);
};
CustomLabels.propTypes = {
x: PropTypes.number.isRequired,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
firstDataPoint: PropTypes.object,
lastDataPoint: PropTypes.object,
type: PropTypes.string.isRequired,
};
export default CustomLabels;
@@ -0,0 +1,92 @@
import { memo, useState } from "react";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
import PropTypes from "prop-types";
import CustomLabels from "./CustomLabels";
const DownBarChart = memo(({ monitor, type, onBarHover }) => {
const theme = useTheme();
const [chartHovered, setChartHovered] = useState(false);
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
return (
<ResponsiveContainer
width="100%"
minWidth={250}
height={155}
>
<BarChart
width="100%"
height="100%"
data={monitor.groupedDownChecks}
onMouseEnter={() => {
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0 });
}}
onMouseLeave={() => {
setChartHovered(false);
setHoveredBarIndex(null);
onBarHover(null);
}}
>
<XAxis
stroke={theme.palette.primary.lowContrast}
height={15}
tick={false}
label={
<CustomLabels
x={0}
y={0}
width="100%"
height="100%"
firstDataPoint={monitor?.groupedDownChecks?.[0] ?? {}}
lastDataPoint={
monitor?.groupedDownChecks?.[monitor?.groupedDownChecks?.length - 1] ?? {}
}
type={type}
/>
}
/>
<Bar
dataKey="avgResponseTime"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{monitor?.groupedDownChecks?.map((entry, index) => {
return (
<Cell
key={`cell-${entry.time}`}
fill={
hoveredBarIndex === index
? theme.palette.error.main
: chartHovered
? theme.palette.error.light // CAIO_REVIEW
: theme.palette.error.main
}
onMouseEnter={() => {
setHoveredBarIndex(index);
onBarHover(entry);
}}
onMouseLeave={() => {
setHoveredBarIndex(null);
onBarHover({ time: null, totalChecks: 0 });
}}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
);
});
DownBarChart.displayName = "DownBarChart";
DownBarChart.propTypes = {
monitor: PropTypes.shape({
groupedDownChecks: PropTypes.arrayOf(PropTypes.object),
}),
type: PropTypes.string,
onBarHover: PropTypes.func,
};
export default DownBarChart;
@@ -0,0 +1,117 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts";
const ResponseGaugeChart = ({ avgResponseTime }) => {
const theme = useTheme();
let max = 1000; // max ms
const data = [
{ response: max, fill: "transparent", background: false },
{ response: avgResponseTime, background: true },
];
let responseTime = Math.floor(avgResponseTime);
let responseProps =
responseTime <= 200
? {
category: "Excellent",
main: theme.palette.success.main,
bg: theme.palette.success.contrastText,
}
: responseTime <= 500
? {
category: "Fair",
main: theme.palette.success.main,
bg: theme.palette.success.contrastText,
}
: responseTime <= 600
? {
category: "Acceptable",
main: theme.palette.warning.main,
bg: theme.palette.warning.lowContrast,
}
: {
category: "Poor",
main: theme.palette.error.main,
bg: theme.palette.error.contrastText,
};
return (
<ResponsiveContainer
width="100%"
minWidth={210}
height={155}
>
<RadialBarChart
width="100%"
height="100%"
cy="89%"
data={data}
startAngle={180}
endAngle={0}
innerRadius={100}
outerRadius={150}
>
<text
x={0}
y="100%"
dx="5%"
dy={-2}
textAnchor="start"
fontSize={11}
>
low
</text>
<text
x="100%"
y="100%"
dx="-3%"
dy={-2}
textAnchor="end"
fontSize={11}
>
high
</text>
<text
x="50%"
y="45%"
textAnchor="middle"
dominantBaseline="middle"
fontSize={18}
fontWeight={400}
>
{responseProps.category}
</text>
<text
x="50%"
y="55%"
textAnchor="middle"
dominantBaseline="hanging"
fontSize={25}
>
<tspan fontWeight={600}>{responseTime}</tspan> <tspan opacity={0.8}>ms</tspan>
</text>
<RadialBar
background={{ fill: responseProps.bg }}
clockWise
dataKey="response"
stroke="none"
>
<Cell
fill="transparent"
background={false}
barSize={0}
/>
<Cell fill={responseProps.main} />
</RadialBar>
</RadialBarChart>
</ResponsiveContainer>
);
};
ResponseGaugeChart.propTypes = {
avgResponseTime: PropTypes.number.isRequired,
};
export default ResponseGaugeChart;
@@ -0,0 +1,18 @@
import ChartBox from "./ChartBox";
import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
const ResponseTImeChart = ({ monitor, dateRange }) => {
return (
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
>
<MonitorDetailsAreaChart
checks={monitor.groupedChecks ?? []}
dateRange={dateRange}
/>
</ChartBox>
);
};
export default ResponseTImeChart;
@@ -0,0 +1,109 @@
import { memo, useState } from "react";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
import PropTypes from "prop-types";
import CustomLabels from "./CustomLabels";
const getThemeColor = (responseTime) => {
if (responseTime < 200) {
return "success";
} else if (responseTime < 300) {
return "warning";
} else {
return "error";
}
};
const UpBarChart = memo(({ monitor, type, onBarHover }) => {
const theme = useTheme();
const [chartHovered, setChartHovered] = useState(false);
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
return (
<ResponsiveContainer
width="100%"
minWidth={210}
height={155}
>
<BarChart
width="100%"
height="100%"
data={monitor?.groupedUpChecks}
onMouseEnter={() => {
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
}}
onMouseLeave={() => {
setChartHovered(false);
setHoveredBarIndex(null);
onBarHover(null);
}}
>
<XAxis
stroke={theme.palette.primary.lowContrast}
height={15}
tick={false}
label={
<CustomLabels
x={0}
y={0}
width="100%"
height="100%"
firstDataPoint={monitor?.groupedUpChecks?.[0]}
lastDataPoint={
monitor?.groupedUpChecks?.[monitor?.groupedUpChecks?.length - 1]
}
type={type}
/>
}
/>
<Bar
dataKey="avgResponseTime"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{monitor?.groupedUpChecks?.map((entry, index) => {
const themeColor = getThemeColor(entry.avgResponseTime);
return (
<Cell
key={`cell-${entry.time}`}
fill={
hoveredBarIndex === index
? theme.palette[themeColor].main
: chartHovered
? theme.palette[themeColor].light // CAIO_REVIEW
: theme.palette[themeColor].main
}
onMouseEnter={() => {
setHoveredBarIndex(index);
onBarHover(entry);
}}
onMouseLeave={() => {
setHoveredBarIndex(null);
onBarHover({
time: null,
totalChecks: 0,
groupUptimePercentage: 0,
});
}}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
);
});
// Add display name for the component
UpBarChart.displayName = "UpBarChart";
// Validate props using PropTypes
UpBarChart.propTypes = {
monitor: PropTypes.shape({
groupedUpChecks: PropTypes.array,
}),
type: PropTypes.string,
onBarHover: PropTypes.func,
};
export default UpBarChart;
@@ -0,0 +1,35 @@
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";
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>
);
};
export default ConfigButton;
@@ -0,0 +1,43 @@
import { Stack, Typography } from "@mui/material";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import Dot from "../../../../../Components/Dot";
import { useTheme } from "@emotion/react";
import useUtils from "../../../Home/Hooks/useUtils";
import { formatDurationRounded } from "../../../../../Utils/timeUtils";
import ConfigButton from "../ConfigButton";
const MonitorHeader = ({ monitor }) => {
const theme = useTheme();
const { statusColor, statusMsg, determineState } = useUtils();
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={true}
monitorId={monitor._id}
/>
</Stack>
);
};
export default MonitorHeader;
@@ -0,0 +1,72 @@
// Components
import { Stack, Typography } from "@mui/material";
import StatBox from "../../../../../Components/StatBox";
// Utils
import { useTheme } from "@mui/material/styles";
import useUtils from "../../../Home/Hooks/useUtils";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
const StatusBoxes = ({ monitor, certificateExpiry }) => {
const theme = useTheme();
const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
monitor?.uptimeStreak
);
const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
monitor?.timeSinceLastCheck
);
const { determineState } = useUtils();
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
gradient={true}
status={determineState(monitor)}
heading={"active for"}
subHeading={
<>
{streakTime}
<Typography component="span">{streakUnits}</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{lastCheckTime}
<Typography component="span">{lastCheckUnits}</Typography>
<Typography component="span">{"ago"}</Typography>
</>
}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
</Stack>
);
};
export default StatusBoxes;
@@ -0,0 +1,44 @@
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { useTheme } from "@emotion/react";
const TimeFramePicker = ({ dateRange, setDateRange }) => {
const theme = useTheme();
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}.
</Typography>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
</Stack>
);
};
export default TimeFramePicker;
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => {
const [monitorIsLoading, setMonitorsIsLoading] = useState(false);
const [monitor, setMonitor] = useState([]);
const [monitor, setMonitor] = useState({});
const navigate = useNavigate();
useEffect(() => {
+33 -4
View File
@@ -1,6 +1,10 @@
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import MonitorHeader from "./Components/MonitorHeader";
import StatusBoxes from "./Components/StatusBoxes";
import TimeFramePicker from "./Components/TimeFramePicker";
import ChartBoxes from "./Components/ChartBoxes";
import ResponseTimeChart from "./Components/Charts/ResponseTimeChart";
// MUI Components
import { Stack } from "@mui/material";
@@ -8,6 +12,7 @@ import { Stack } from "@mui/material";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import useMonitorFetch from "./Hooks/useMonitorFetch";
import useCertificateFetch from "./Hooks/useCertificateFetch";
// Constants
@@ -26,10 +31,13 @@ const UptimeDetails = () => {
// Local state
const [dateRange, setDateRange] = useState("day");
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
// Utils
const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
const { monitorId } = useParams();
const theme = useTheme();
const { monitor, monitorIsLoading } = useMonitorFetch({
authToken,
@@ -45,11 +53,32 @@ const UptimeDetails = () => {
uiTimezone,
});
console.log(monitor);
console.log(certificateExpiry);
return (
<Stack>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorHeader monitor={monitor} />
<StatusBoxes
monitor={monitor}
certificateExpiry={certificateExpiry}
/>
<TimeFramePicker
dateRange={dateRange}
setDateRange={setDateRange}
/>
<ChartBoxes
monitor={monitor}
uiTimezone={uiTimezone}
dateRange={dateRange}
dateFormat={dateFormat}
hoveredUptimeData={hoveredUptimeData}
setHoveredUptimeData={setHoveredUptimeData}
hoveredIncidentsData={hoveredIncidentsData}
setHoveredIncidentsData={setHoveredIncidentsData}
/>
<ResponseTimeChart
monitor={monitor}
dateRange={dateRange}
/>
</Stack>
);
};