conditionally render charts
This commit is contained in:
1 parent
4e495eed33
commit
81aab35961
3 files changed
+227
-77
No files matched your search
@@ -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 (
|
||||
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
||||
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
|
||||
<Typography>Response time: {responseTime} ms</Typography>
|
||||
<Typography>
|
||||
Response time:{" "}
|
||||
{prettyMilliseconds(responseTime, { formatSubMilliseconds: true, compact: true })}
|
||||
</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -95,7 +98,7 @@ export const HistogramDetails = ({
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
title="Response times"
|
||||
title="Average response time (First byte)"
|
||||
>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
import type { GroupedUptimeCheck } from "@/Types/Check";
|
||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, YAxis } from "recharts";
|
||||
import type { TooltipProps } from "recharts";
|
||||
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { Clock } from "lucide-react";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
YAxis,
|
||||
XAxis,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "recharts";
|
||||
import { BaseBox, BaseChart, Dot } from "@/Components/design-elements";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import {
|
||||
formatDateWithTz,
|
||||
tickDateFormatLookup,
|
||||
tooltipDateFormatLookup,
|
||||
} from "@/Utils/TimeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useMemo } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
const PHASE_KEYS = [
|
||||
export const PHASE_KEYS = [
|
||||
"avgDns",
|
||||
"avgTcp",
|
||||
"avgTls",
|
||||
@@ -13,6 +39,94 @@ const PHASE_KEYS = [
|
||||
"avgDownload",
|
||||
] as const;
|
||||
|
||||
const PHASE_COLOR_KEYS = {
|
||||
avgDns: "dns",
|
||||
avgTcp: "tcp",
|
||||
avgTls: "tls",
|
||||
avgRequest: "request",
|
||||
avgFirstByte: "firstByte",
|
||||
avgDownload: "download",
|
||||
} as const;
|
||||
|
||||
const PHASE_LABELS: Record<(typeof PHASE_KEYS)[number], string> = {
|
||||
avgDns: "DNS",
|
||||
avgTcp: "TCP",
|
||||
avgTls: "TLS",
|
||||
avgRequest: "Request",
|
||||
avgFirstByte: "First byte",
|
||||
avgDownload: "Download",
|
||||
};
|
||||
|
||||
type TimingPhaseToolTipProps = TooltipProps<ValueType, NameType> & {
|
||||
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 (
|
||||
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
||||
<Typography>{formatDateWithTz(bucket.bucketDate, format, uiTimezone)}</Typography>
|
||||
<Typography>Total: {prettyMilliseconds(total)}</Typography>
|
||||
{PHASE_KEYS.map((key) => (
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
direction="row"
|
||||
key={key}
|
||||
gap={SPACING.LG}
|
||||
>
|
||||
<Dot
|
||||
size={"12px"}
|
||||
color={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
|
||||
/>
|
||||
<Typography>
|
||||
{PHASE_LABELS[key]}:{" "}
|
||||
{prettyMilliseconds(bucket[key], {
|
||||
formatSubMilliseconds: true,
|
||||
compact: true,
|
||||
})}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const XTick = ({ x, y, payload, range }: XTickProps) => {
|
||||
const format = tickDateFormatLookup(range);
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
return (
|
||||
<Text
|
||||
x={x}
|
||||
y={y + 10}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={300}
|
||||
<BaseChart
|
||||
icon={
|
||||
<Clock
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
title="Timing phases"
|
||||
>
|
||||
<AreaChart data={checks.slice()}>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.divider}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<YAxis
|
||||
hide
|
||||
domain={[0, yMax ?? "auto"]}
|
||||
allowDataOverflow
|
||||
/>
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
stackId={1}
|
||||
dataKey="avgDns"
|
||||
stroke={"blue"}
|
||||
fill="blue"
|
||||
/>
|
||||
<Area
|
||||
stackId={1}
|
||||
type="monotone"
|
||||
dataKey="avgTcp"
|
||||
stroke={"red"}
|
||||
fill="red"
|
||||
/>
|
||||
<Area
|
||||
stackId={1}
|
||||
type="monotone"
|
||||
dataKey="avgTls"
|
||||
stroke={"green"}
|
||||
fill="green"
|
||||
/>
|
||||
<Area
|
||||
stackId={1}
|
||||
type="monotone"
|
||||
dataKey="avgRequest"
|
||||
stroke={"orange"}
|
||||
fill="orange"
|
||||
/>
|
||||
<Area
|
||||
stackId={1}
|
||||
type="monotone"
|
||||
dataKey="avgFirstByte"
|
||||
stroke={"purple"}
|
||||
fill="purple"
|
||||
/>
|
||||
|
||||
<Area
|
||||
stackId={1}
|
||||
type="monotone"
|
||||
dataKey="avgDownload"
|
||||
stroke={"brown"}
|
||||
fill="brown"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height={300}
|
||||
>
|
||||
<AreaChart data={checks.slice()}>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.divider}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
{PHASE_KEYS.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`phaseGradient-${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
|
||||
stopOpacity={0.2}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<YAxis
|
||||
hide
|
||||
// scale="sqrt"
|
||||
domain={[0, yMax ?? "auto"]}
|
||||
allowDataOverflow
|
||||
/>
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
dataKey="bucketDate"
|
||||
tick={(props) => (
|
||||
<XTick
|
||||
{...props}
|
||||
range={range}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Tooltip
|
||||
content={(props) => (
|
||||
<TimingPhasesToolTip
|
||||
{...props}
|
||||
range={range}
|
||||
theme={theme}
|
||||
uiTimezone={uiTimezone}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{PHASE_KEYS.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
stackId={1}
|
||||
dataKey={key}
|
||||
stroke={theme.palette.chart.phases[PHASE_COLOR_KEYS[key]]}
|
||||
fill={`url(#phaseGradient-${key})`}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</BaseChart>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
/>
|
||||
</Stack>
|
||||
<HistogramDetails
|
||||
checks={monitorData?.groupedChecks || []}
|
||||
range={dateRange}
|
||||
/>
|
||||
<HistogramDetailsStacked
|
||||
checks={monitorData.groupedChecks}
|
||||
range={dateRange}
|
||||
/>
|
||||
{hasTimingPhases && (
|
||||
<HistogramDetailsStacked
|
||||
checks={monitorData.groupedChecks}
|
||||
range={dateRange}
|
||||
/>
|
||||
)}
|
||||
<ChecksTable
|
||||
checks={checks}
|
||||
count={checksCount}
|
||||
|
||||
Reference in new issue
Block a user