diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx index bd78f3289..17849fadb 100644 --- a/client/src/Components/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramDetails.tsx @@ -12,6 +12,7 @@ import { import type { TooltipProps } from "recharts"; import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"; import Typography from "@mui/material/Typography"; +import prettyMilliseconds from "pretty-ms"; import { useSelector } from "react-redux"; import { @@ -68,12 +69,14 @@ const ResponseTimeToolTip = ({ if (!active) return null; const format = tooltipDateFormatLookup(range); - console.log(payload); const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0); return ( {formatDateWithTz(String(label), format, uiTimezone)} - Response time: {responseTime} ms + + Response time:{" "} + {prettyMilliseconds(responseTime, { formatSubMilliseconds: true, compact: true })} + ); }; @@ -95,7 +98,7 @@ export const HistogramDetails = ({ strokeWidth={1.5} /> } - title="Response times" + title="Average response time (First byte)" > = { + avgDns: "DNS", + avgTcp: "TCP", + avgTls: "TLS", + avgRequest: "Request", + avgFirstByte: "First byte", + avgDownload: "Download", +}; + +type TimingPhaseToolTipProps = TooltipProps & { + range: string; + theme: Theme; + uiTimezone: string; +}; + +type XTickProps = { + x: number; + y: number; + payload: { value: string }; + range: string; +}; + +const TimingPhasesToolTip = ({ + active, + payload, + range, + theme, + uiTimezone, +}: TimingPhaseToolTipProps) => { + if (!active || !payload?.length) return null; + const bucket = payload[0].payload as GroupedUptimeCheck; + const total = PHASE_KEYS.reduce((sum, key) => sum + bucket[key], 0); + const format = tooltipDateFormatLookup(range); + return ( + + {formatDateWithTz(bucket.bucketDate, format, uiTimezone)} + Total: {prettyMilliseconds(total)} + {PHASE_KEYS.map((key) => ( + + + + {PHASE_LABELS[key]}:{" "} + {prettyMilliseconds(bucket[key], { + formatSubMilliseconds: true, + compact: true, + })} + + + ))} + + ); +}; + +export const XTick = ({ x, y, payload, range }: XTickProps) => { + const format = tickDateFormatLookup(range); + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + {formatDateWithTz(payload?.value, format, uiTimezone)} + + ); +}; + export const HistogramDetailsStacked = ({ checks = [], range, @@ -21,83 +135,103 @@ export const HistogramDetailsStacked = ({ range: string; }) => { const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); - // Cap the y-domain at the p95 of bucket totals so a single timeout bucket - // doesn't flatten every other bucket; spikes clip off the top instead. + // Cap the y domain at 2x the p95 of bucket totals, so typical stacks sit + // around mid-height with headroom above const yMax = useMemo(() => { const totals = checks .map((check) => PHASE_KEYS.reduce((sum, key) => sum + check[key], 0)) .sort((a, b) => a - b); if (totals.length < 2) return undefined; const p95 = totals[Math.min(totals.length - 1, Math.floor(totals.length * 0.95))]; - return p95 > 0 ? Math.ceil(p95) : undefined; + return p95 > 0 ? Math.ceil(p95 * 2) : undefined; }, [checks]); - console.log(JSON.stringify(checks, null, 2)); - return ( - + } + title="Timing phases" > - - - - - - - - - - - - - + + + + + {PHASE_KEYS.map((key) => ( + + + + + ))} + + + ( + + )} + /> + ( + + )} + /> + {PHASE_KEYS.map((key) => ( + + ))} + + + ); }; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 7aa0b1046..04d207d06 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -31,7 +31,10 @@ import type { RootState } from "@/Types/state"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { t } from "i18next"; import { Typography } from "@mui/material"; -import { HistogramDetailsStacked } from "@/Components/monitors/charts/HistogramDetailsStacked"; +import { + HistogramDetailsStacked, + PHASE_KEYS, +} from "@/Components/monitors/charts/HistogramDetailsStacked"; const certificateDateFormat = "MMM D, YYYY h A"; @@ -77,6 +80,14 @@ const UptimeDetailsPage = () => { const monitor = monitorData?.monitor; const monitorStats = monitorDetailsData?.monitorStats ?? null; + const hasTimingPhases = useMemo( + () => + (monitorData?.groupedChecks ?? []).some((check) => + PHASE_KEYS.some((key) => check[key] > 0) + ), + [monitorData] + ); + // Certificate fetch - only for HTTP monitors const certificateUrl = useMemo(() => { if (!monitorId || monitor?.type !== "http") { @@ -225,13 +236,15 @@ const UptimeDetailsPage = () => { /> - + {hasTimingPhases && ( + + )}