use heathmap

This commit is contained in:
Alex Holliday committed 2025-11-25 19:03:48 +00:00
1 parent 68ab185f84
commit c2a1d6d97e
8 files changed
+154 -106

No files matched your search

@@ -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<any>();
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 (
<Box sx={{ width: "100%" }}>
<Box
sx={{
width: "100%",
display: "grid",
gridTemplateColumns: "repeat(25, 1fr)",
gap: theme.spacing(0.5),
alignItems: "stretch",
}}
>
{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 (
<HistogramResponseTimeTooltip
key={`${check}-${index}`}
check={check}
>
<Box
sx={{
display: "grid",
gridTemplateRows: "auto auto",
gap: theme.spacing(0.5),
}}
>
<Box
sx={{
width: "100%",
height: theme.spacing(0.75),
borderRadius: theme.spacing(0.5),
bgcolor: statusBg,
}}
/>
<Box
sx={{
width: "100%",
aspectRatio: "1 / 1",
borderRadius: theme.spacing(0.5),
bgcolor: respBg,
}}
/>
</Box>
</HistogramResponseTimeTooltip>
);
})}
</Box>
</Box>
);
};
@@ -51,7 +51,7 @@ export function DataTable<
id?: string | number;
_id?: string | number;
onRowClick?: (row: T) => void;
}
},
>({
headers,
data,
@@ -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<any>();
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 (
<Stack
direction="row"
flexWrap="nowrap"
gap={theme.spacing(1.5)}
height="50px"
width="fit-content"
onClick={(event) => event.stopPropagation()}
sx={{
cursor: "default",
}}
>
{data.map((check, index) => {
if (check === "placeholder") {
return (
<Box
key={`${check}-${index}`}
position="relative"
width={BAR_WIDTH}
height="100%"
bgcolor={theme.palette.primary.veryLowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
);
} else {
return (
<HistogramResponseTimeTooltip
key={`${check}-${index}`}
check={check}
>
<Box
position="relative"
height="100%"
bgcolor={theme.palette.primary.veryLowContrast}
width={BAR_WIDTH}
sx={{
borderRadius: theme.spacing(1.5),
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${check.normalResponseTime}%`}
bgcolor={
check.status === "up"
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</HistogramResponseTimeTooltip>
);
}
})}
</Stack>
);
};
@@ -47,6 +47,9 @@ export const HistogramResponseTimeTooltip: React.FC<{
{check?.responseTime && (
<Typography>Response Time: {check.responseTime} ms</Typography>
)}
<Typography color="error" textTransform={"capitalize"}>
Status: {check?.status}
</Typography>
</Stack>
}
>
-1
View File
@@ -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";
@@ -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 (
<Grid container>
<Grid size={12}>
<Grid size={6} display={"flex"} alignItems={"center"}>
<Typography>{monitor?.name}</Typography>
</Grid>
<Grid size={{ xs: 8, sm: 10 }}>
<HistogramStatusPage checks={monitor?.latestChecks} />
</Grid>
<Grid
size={{ xs: 4, sm: 2 }}
paddingLeft={theme.spacing(10)}
size={6}
display={"flex"}
alignItems={"center"}
justifyContent={"flex-start"}
justifyContent={"flex-end"}
>
<StatusLabel status={monitor?.status} />
</Grid>
<Grid size={12}>
<HeatmapResponseTime checks={monitor?.latestChecks} />
</Grid>
</Grid>
);
};
+2 -6
View File
@@ -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 (
<Stack alignItems={{ xs: "flex-start", md: "center" }}>
<HistogramResponseTime checks={row.latestChecks} />
</Stack>
);
return <HeatmapResponseTime checks={row.latestChecks} />;
},
},
{
+51 -1
View File
@@ -3,7 +3,7 @@ const MAX_OUT = 100;
export const normalizeResponseTimes = <
T extends Record<K, number>,
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;
};