use heathmap
This commit is contained in:
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>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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,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} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user