From 6323f70bd22fe99ef43b50d9f2a5a832a0197e83 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 22 Dec 2025 06:39:00 +0000 Subject: [PATCH] check details --- .../components/checks/CheckDetailsCard.tsx | 111 ++++++++++++++++++ client/src/components/checks/TimingsChart.tsx | 97 +++++++++++++++ client/src/components/checks/index.tsx | 2 + client/src/pages/checks/CheckDetails.tsx | 20 +++- 4 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 client/src/components/checks/CheckDetailsCard.tsx create mode 100644 client/src/components/checks/TimingsChart.tsx create mode 100644 client/src/components/checks/index.tsx diff --git a/client/src/components/checks/CheckDetailsCard.tsx b/client/src/components/checks/CheckDetailsCard.tsx new file mode 100644 index 000000000..79e624cdf --- /dev/null +++ b/client/src/components/checks/CheckDetailsCard.tsx @@ -0,0 +1,111 @@ +import Stack from "@mui/material/Stack"; +import Grid from "@mui/material/Grid"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { StatusLabel, ValueLabel } from "@/components/design-elements"; +import type { ICheck } from "@/types/check"; +import type { MonitorStatus } from "@/types/monitor"; +import { useAppSelector } from "@/hooks/AppHooks"; +import { formatDateWithTz } from "@/utils/TimeUtils"; + +type CheckDetailsCardProps = { + check: ICheck; +}; + +export const CheckDetailsCard = ({ check }: CheckDetailsCardProps) => { + const uiTimezone = useAppSelector((state: any) => state.ui.timezone); + const phases = check?.timings?.phases; + const phaseKeys = [ + "wait", + "dns", + "tcp", + "tls", + "request", + "firstByte", + "download", + ] as const; + const totalMs = phases + ? phaseKeys.reduce( + (sum, key) => sum + Math.max(0, Number((phases as any)[key] ?? 0)), + 0 + ) + : Number(check.responseTime ?? 0); + + return ( + + Check details + + + + + Status + + + + + + + + + + + + HTTP Code + + + + + + + + + + Type + + + + {check.metadata?.type || "-"} + + + + Total request time + + + + {Math.round(totalMs)} ms + + + + Checked At + + + + + {formatDateWithTz( + check.createdAt, + "ddd, MMM D, YYYY, HH:mm A", + uiTimezone + )} + + + + + Message + + + + {check.message || "-"} + + + + ); +}; diff --git a/client/src/components/checks/TimingsChart.tsx b/client/src/components/checks/TimingsChart.tsx new file mode 100644 index 000000000..935c2abf3 --- /dev/null +++ b/client/src/components/checks/TimingsChart.tsx @@ -0,0 +1,97 @@ +import type { ICheck, CheckTimingPhases } from "@/types/check"; +import { + PieChart, + Pie, + Cell, + ResponsiveContainer, + Tooltip, + Legend, +} from "recharts"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { alpha } from "@mui/material/styles"; + +type TimingsChartProps = { check: ICheck | null }; + +export type TimingSegment = { + key: keyof CheckTimingPhases; + label: string; + value: number; // milliseconds + percent: number; // 0..100 +}; + +const PHASE_ORDER: (keyof CheckTimingPhases)[] = [ + "wait", + "dns", + "tcp", + "tls", + "request", + "firstByte", + "download", +]; + +const buildTimingSegments = (phases?: CheckTimingPhases): TimingSegment[] => { + if (!phases) return []; + const raw = PHASE_ORDER.map((key) => ({ + key, + value: Math.max(0, Number((phases as any)[key] ?? 0)), + })); + const total = raw.reduce((sum, p) => sum + p.value, 0); + if (total <= 0) { + return raw.map(({ key, value }) => ({ + key, + label: key, + value, + percent: 0, + })); + } + return raw.map(({ key, value }) => ({ + key, + label: key, + value, + percent: (value / total) * 100, + })); +}; + +export const TimingsChart = ({ check }: TimingsChartProps) => { + console.log(check); + const segments = buildTimingSegments(check?.timings?.phases); + if (!segments.length) return null; + if (!check) return null; + + const colors = [ + alpha("#1DE9B6", 0.8), + alpha("#7C4DFF", 0.8), + alpha("#FFB200", 0.8), + alpha("#00AFFE", 0.8), + alpha("#FF4181", 0.8), + alpha("#59A14F", 0.8), + alpha("#E15759", 0.8), + ]; + + return ( + + Request timings + + {" "} + {`Total request time: ${check.timings.phases.total} ms`} + + + + + {segments.map((entry, index) => ( + + ))} + + `${Math.round(value as number)} ms`} + /> + + + + + ); +}; diff --git a/client/src/components/checks/index.tsx b/client/src/components/checks/index.tsx new file mode 100644 index 000000000..6f62eddbc --- /dev/null +++ b/client/src/components/checks/index.tsx @@ -0,0 +1,2 @@ +export { TimingsChart } from "./TimingsChart"; +export { CheckDetailsCard } from "./CheckDetailsCard"; diff --git a/client/src/pages/checks/CheckDetails.tsx b/client/src/pages/checks/CheckDetails.tsx index ea3630c9c..21b089830 100644 --- a/client/src/pages/checks/CheckDetails.tsx +++ b/client/src/pages/checks/CheckDetails.tsx @@ -1,19 +1,33 @@ -import { BasePage } from "@/components/design-elements"; +import { BasePage, BaseBox } from "@/components/design-elements"; +import { TimingsChart, CheckDetailsCard } from "@/components/checks/"; +import Stack from "@mui/material/Stack"; import { useParams } from "react-router"; import { useGet } from "@/hooks/UseApi"; import type { ICheck } from "@/types/check"; +import useMediaQuery from "@mui/material/useMediaQuery"; import type { ApiResponse } from "@/types/api"; +import { useTheme } from "@mui/material/styles"; const CheckDetails = () => { const { id } = useParams(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const { response, loading, error } = useGet>( `/checks/${id}` ); - const check = response?.data; + const check = response?.data || null; return ( - {JSON.stringify(check)} + + + + + + + + ); };