diff --git a/client/src/components/common/HeatmapResponseTime.tsx b/client/src/components/common/HeatmapResponseTime.tsx new file mode 100644 index 000000000..ac44e630d --- /dev/null +++ b/client/src/components/common/HeatmapResponseTime.tsx @@ -0,0 +1,90 @@ +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import type { ICheck } from "@/types/check"; +import { getResponseColor } from "@/utils/DataUtils"; +import { HistogramResponseTimeTooltip } from "@/components/monitors/HistogramResponseTimeTooltip"; + +interface HeatmapResponseTimeProps { + checks: ICheck[]; +} + +export const HeatmapResponseTime = ({ checks }: HeatmapResponseTimeProps) => { + const theme = useTheme(); + + if (!checks || checks.length === 0) return null; + let data = Array(); + + if (!checks || checks.length === 0) { + checks = []; + } + if (checks.length !== 25) { + const placeholders = Array(25 - checks.length).fill({ + status: "placeholder", + }); + data = [...checks, ...placeholders]; + } else { + data = checks; + } + return ( + + + {data.map((check, index) => { + const statusBg = + check.status === "placeholder" + ? theme.palette.primary.lowContrast + : check.status === "up" + ? theme.palette.success.lowContrast + : theme.palette.error.lowContrast; + const respBg = + check.status === "placeholder" + ? theme.palette.primary.lowContrast + : getResponseColor(check.responseTime, { + start: theme.palette.success.lowContrast, + mid: theme.palette.warning.lowContrast, + end: theme.palette.error.lowContrast, + }); + + return ( + + + + + + + ); + })} + + + ); +}; diff --git a/client/src/components/design-elements/Table.tsx b/client/src/components/design-elements/Table.tsx index ddd5a8bac..a216d17cd 100644 --- a/client/src/components/design-elements/Table.tsx +++ b/client/src/components/design-elements/Table.tsx @@ -51,7 +51,7 @@ export function DataTable< id?: string | number; _id?: string | number; onRowClick?: (row: T) => void; - } + }, >({ headers, data, diff --git a/client/src/components/monitors/HistogramResponseTime.tsx b/client/src/components/monitors/HistogramResponseTime.tsx deleted file mode 100644 index e7c765d15..000000000 --- a/client/src/components/monitors/HistogramResponseTime.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; -import { useTheme } from "@mui/material/styles"; -import type { ICheck } from "@/types/check"; -import { HistogramResponseTimeTooltip } from "@/components/monitors/HistogramResponseTimeTooltip"; -import { normalizeResponseTimes } from "@/utils/DataUtils"; - -export const HistogramResponseTime = ({ checks }: { checks: ICheck[] }) => { - const theme = useTheme(); - const normalChecks = normalizeResponseTimes(checks, "responseTime"); - const BAR_WIDTH = { xs: theme.spacing(2.5), md: theme.spacing(4.5) }; - let data = Array(); - - if (!normalChecks || normalChecks.length === 0) { - return null; - } - if (normalChecks.length !== 25) { - const placeholders = Array(25 - normalChecks.length).fill("placeholder"); - data = [...normalChecks, ...placeholders]; - } else { - data = normalChecks; - } - - return ( - event.stopPropagation()} - sx={{ - cursor: "default", - }} - > - {data.map((check, index) => { - if (check === "placeholder") { - return ( - - ); - } else { - return ( - - - - - - ); - } - })} - - ); -}; diff --git a/client/src/components/monitors/HistogramResponseTimeTooltip.tsx b/client/src/components/monitors/HistogramResponseTimeTooltip.tsx index 42deefe76..b18841926 100644 --- a/client/src/components/monitors/HistogramResponseTimeTooltip.tsx +++ b/client/src/components/monitors/HistogramResponseTimeTooltip.tsx @@ -47,6 +47,9 @@ export const HistogramResponseTimeTooltip: React.FC<{ {check?.responseTime && ( Response Time: {check.responseTime} ms )} + + Status: {check?.status} + } > diff --git a/client/src/components/monitors/index.tsx b/client/src/components/monitors/index.tsx index 30355f7f2..69627fd3a 100644 --- a/client/src/components/monitors/index.tsx +++ b/client/src/components/monitors/index.tsx @@ -4,7 +4,6 @@ export { ChartAvgResponse } from "./ChartAvgResponse"; export { HeaderRange } from "../common/HeaderRange"; export { HeaderControls } from "./HeaderControls"; export { ChartResponseTime } from "./ChartResponseTime"; -export { HistogramResponseTime } from "./HistogramResponseTime"; export { HistogramPageSpeed } from "./HistogramPageSpeed"; export { HistogramInfrastructure } from "./HistogramInfrastructure"; diff --git a/client/src/components/status-pages/StatusPageRow.tsx b/client/src/components/status-pages/StatusPageRow.tsx index ec3c38af5..f6bdd0ad9 100644 --- a/client/src/components/status-pages/StatusPageRow.tsx +++ b/client/src/components/status-pages/StatusPageRow.tsx @@ -1,30 +1,27 @@ import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; -import { HistogramStatusPage } from "./HistogramStatusPage"; +import { HeatmapResponseTime } from "../common/HeatmapResponseTime"; import { StatusLabel } from "@/components/design-elements"; import type { IMonitor } from "@/types/monitor"; -import { useTheme } from "@mui/material/styles"; export const StatusPageRow = ({ monitor }: { monitor: IMonitor }) => { - const theme = useTheme(); return ( - + {monitor?.name} - - - + + + ); }; diff --git a/client/src/pages/uptime/MonitorTable.tsx b/client/src/pages/uptime/MonitorTable.tsx index bfe863963..d45e0a3c2 100644 --- a/client/src/pages/uptime/MonitorTable.tsx +++ b/client/src/pages/uptime/MonitorTable.tsx @@ -2,7 +2,7 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { Table, Pagination, StatusLabel } from "@/components/design-elements"; -import { HistogramResponseTime } from "@/components/monitors"; +import { HeatmapResponseTime } from "@/components/common/HeatmapResponseTime"; import type { Header } from "@/components/design-elements/Table"; import { ActionsMenu } from "@/components/actions-menu"; import { ArrowDown, ArrowUp } from "lucide-react"; @@ -192,11 +192,7 @@ export const MonitorTable = ({ id: "histogram", content: t("responseTime"), render: (row) => { - return ( - - - - ); + return ; }, }, { diff --git a/client/src/utils/DataUtils.ts b/client/src/utils/DataUtils.ts index 4a86b3a9b..ebea3d506 100644 --- a/client/src/utils/DataUtils.ts +++ b/client/src/utils/DataUtils.ts @@ -3,7 +3,7 @@ const MAX_OUT = 100; export const normalizeResponseTimes = < T extends Record, - K extends keyof T + K extends keyof T, >( checks: T[], key: K @@ -39,3 +39,53 @@ export const normalizeResponseTimes = < MIN_OUT + ((check[key] - min) * (MAX_OUT - MIN_OUT)) / range, })); }; + +// Interpolate color between three theme colors over 0-100 range. +// 0-50 => start (success.lowContrast), 50-75 => mid (warning.lowContrast), 75-100 => end (error.lowContrast) +export const getResponseColor = ( + ms: number, + colors: { + start: string | undefined; + mid: string | undefined; + end: string | undefined; + } +): string => { + // New ranges with open high end: + // 0–300ms: interpolate start (good) -> mid (warning) + // 300–600ms: interpolate mid (warning) -> end (error) + // >600ms: solid end (error) + const safe = { ...colors }; + if (!safe.start) safe.start = "#4caf50"; // fallback green + if (!safe.mid) safe.mid = "#ff9800"; // fallback orange + if (!safe.end) safe.end = "#f44336"; // fallback red + + const parseHex = (hex: string) => { + const h = hex.replace("#", ""); + const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h; + const n = parseInt(full, 16); + return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; + }; + const toHex = (c: number) => c.toString(16).padStart(2, "0"); + const mix = (a: number, b: number, t: number) => Math.round(a + (b - a) * t); + + const v = Math.max(0, ms); + if (v <= 300) { + const t = v / 300; // 0..1 from start->mid + const s = parseHex(safe.start); + const m = parseHex(safe.mid); + const r = mix(s.r, m.r, t); + const g = mix(s.g, m.g, t); + const b = mix(s.b, m.b, t); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + if (v <= 600) { + const t = (v - 300) / 300; // 0..1 from mid->end + const m = parseHex(safe.mid); + const e = parseHex(safe.end); + const r = mix(m.r, e.r, t); + const g = mix(m.g, e.g, t); + const b = mix(m.b, e.b, t); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + return safe.end; +};