diff --git a/client/src/Components/v2/common/charts/HistogramResponseTime.tsx b/client/src/Components/v2/common/charts/HistogramResponseTime.tsx index e3b82cd50..1ef400daf 100644 --- a/client/src/Components/v2/common/charts/HistogramResponseTime.tsx +++ b/client/src/Components/v2/common/charts/HistogramResponseTime.tsx @@ -1,102 +1,155 @@ import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import type { ResponsiveStyleValue } from "@mui/system"; import type { CheckSnapshot } from "@/Types/Check"; import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; interface HistogramResponseTimeProps { checks: CheckSnapshot[]; height?: ResponsiveStyleValue; gap?: ResponsiveStyleValue; + showStats?: boolean; + statsPosition?: "left" | "right"; +} + +interface ResponseTimeStats { + max: number | string; + avg: number | string; } const DEFAULT_HEIGHT = 50; +const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats => { + if (!Array.isArray(checks) || checks.length === 0) { + return { max: "-", avg: "-" }; + } + + const validChecks = checks.filter((check) => check.originalResponseTime != null); + if (validChecks.length === 0) { + return { max: "-", avg: "-" }; + } + + const responseTimes = validChecks.map((check) => check.originalResponseTime); + const max = Math.max(...responseTimes); + const avg = Math.round( + responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length + ); + + return { max, avg }; +}; + export const HistogramResponseTime = ({ checks, height = DEFAULT_HEIGHT, gap, + showStats = true, + statsPosition = "right", }: HistogramResponseTimeProps) => { const theme = useTheme(); + const { t } = useTranslation(); + + const stats = useMemo(() => calculateResponseTimeStats(checks), [checks]); if (!Array.isArray(checks) || checks.length === 0) return null; - let data = Array(); + const data = + checks.length !== 25 + ? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })] + : checks; - if (!checks || checks.length === 0) { - data = []; - } - if (checks.length !== 25) { - const placeholders = Array(25 - checks.length).fill({ - status: "placeholder", - }); - data = [...checks, ...placeholders]; - } else { - data = checks; - } const chartHeight = typeof height === "number" ? `${height}px` : height; const gridGap = gap ?? theme.spacing(0.5); - return ( - - - {data.map((check, index) => { - const isPlaceholder = (check as any).status === "placeholder"; - const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`; - const barColor = - check.status === true - ? theme.palette.success.light - : theme.palette.error.light; - const bar = ( - + + {t("common.charts.histogram.avg", { value: stats.avg })} + + + {t("common.charts.histogram.max", { value: stats.max })} + + + ); + + return ( + + + + {data.map((check, index) => { + const isPlaceholder = (check as any).status === "placeholder"; + const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`; + const barColor = + check.status === true + ? theme.palette.success.light + : theme.palette.error.light; + const bar = ( - - ); + > + + + ); - return isPlaceholder ? ( - - {bar} - - ) : ( - - {bar} - - ); - })} + return isPlaceholder ? ( + + {bar} + + ) : ( + + {bar} + + ); + })} + - + {statsContent} + ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d8a46fb7c..14081a445 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -47,6 +47,10 @@ "high": "High", "low": "Low", "uptime": "Uptime" + }, + "histogram": { + "avg": "Avg: {{value}} ms", + "max": "Max: {{value}} ms" } }, "dialogs": {