/** * Overview: what the resolver did over a period the reader chooses, in the * layout Pi-hole's dashboard established — four totals, two full-width charts, * two breakdown donuts. Nothing on this page is a current-state readout; the * five health conditions live on Diagnostics, and protection lives in the * sidebar beside its control. * * The period is URL state, so a view is a link: `/overview?period=1h` opens * exactly what the sender was reading. * * Every panel reads the same window (`overviewWindow.ts`) and renders on its * own. A donut whose request failed shows its own error while the charts keep * their data, and no two panels ever describe different spans. */ import * as stylex from "@stylexjs/stylex"; import { useNavigate, useSearch } from "@tanstack/react-router"; import CoverageNotice from "@/lib/CoverageNotice"; import InlineError from "@/lib/InlineError"; import { qtypeName } from "@/features/queries/qtype"; import type { Period, StatsRoutes, StatsTypes } from "@/lib/types"; import { styles as shared } from "@/ui/styles"; import { colors } from "@/ui/tokens.stylex"; import ClientChart from "./ClientChart"; import Donut from "./Donut"; import StatTiles from "./StatTiles"; import TimeseriesChart from "./TimeseriesChart"; import type { DonutSlice } from "./donutLayout"; import { useOverviewWindow, type Panel } from "./overviewWindow"; import { DEFAULT_PERIOD, PERIODS } from "./period"; import { qtypeKey, routeKey, seriesColor } from "./seriesColors"; /** * Where the two donuts stop competing for width and sit side by side. `Donut` * carries the same query for the alignment it switches at that width; StyleX * requires the string to be a literal in the module that uses it, so the two * agree by inspection rather than by sharing a constant. */ const TWO_COLUMN = "@media (min-width: 1280px)"; const ROUTE_LABELS = { blocked: "Blocked", cache: "Cache", local: "Local", rejected: "Rejected", upstream: "Upstream", forward_zone: "Forward zone", } as const; const styles = stylex.create({ page: { display: "flex", flexDirection: "column", gap: "1rem", }, headingRow: { display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "space-between", gap: "0.75rem", }, heading: { fontSize: "1.5rem", lineHeight: "2rem", fontWeight: 600, }, periodGroup: { display: "flex", gap: "0.25rem", }, period: { borderStyle: "none", borderRadius: "0.25rem", paddingInline: "0.625rem", paddingBlock: "0.25rem", fontSize: "0.875rem", lineHeight: "1.25rem", }, /** The pressed fill is heavier than `surfaceHover`, so a hover cannot mimic it. */ periodSelected: { backgroundColor: { default: "oklch(92% 0.004 286.32)", "@media (prefers-color-scheme: dark)": "oklch(37% 0.013 285.805)", }, color: colors.text, fontWeight: 500, }, periodIdle: { backgroundColor: { default: "transparent", ":hover": colors.surfaceHover }, color: colors.textSecondary, }, panel: { borderRadius: "0.25rem", borderWidth: 1, borderStyle: "solid", borderColor: colors.border, backgroundColor: colors.surfaceRaised, paddingInline: "1rem", paddingBlock: "0.75rem", }, panelHeading: { marginBottom: "0.75rem", fontSize: "0.875rem", lineHeight: "1.25rem", fontWeight: 600, }, donutRow: { display: "grid", gap: "1rem", gridTemplateColumns: { default: "minmax(0, 1fr)", [TWO_COLUMN]: "repeat(2, minmax(0, 1fr))" }, }, loading: { fontSize: "0.875rem", lineHeight: "1.25rem", color: colors.textMuted, }, }); function PeriodPicker({ period, onChange }: { period: Period; onChange: (period: Period) => void }) { return (
Loading…
); } return <>{children(panel.data)}>; } function typeSlices(data: StatsTypes): DonutSlice[] { return data.types.map((row) => ({ key: qtypeKey(row.qtype), label: row.qtype === null ? "Unknown" : qtypeName(row.qtype), value: row.count, color: seriesColor(qtypeKey(row.qtype)), })); } /** * Route slices. A row that names a resolver or a zone is labelled by that name * with the route kind as secondary text, because one name can legitimately * appear under two kinds and two rows can both be "Unknown". The four * source-less kinds are their own label and need no qualifier. */ function routeSlices(data: StatsRoutes): DonutSlice[] { return data.routes.map((row) => { const named = row.route === "upstream" || row.route === "forward_zone"; return { key: routeKey(row.route, row.source), label: named ? (row.source ?? "Unknown") : ROUTE_LABELS[row.route], ...(named ? { secondary: ROUTE_LABELS[row.route] } : {}), value: row.count, color: seriesColor(routeKey(row.route, row.source)), }; }); } export default function OverviewPage() { const period = useSearch({ from: "/shell/overview" }).period ?? DEFAULT_PERIOD; const navigate = useNavigate({ from: "/overview" }); const overview = useOverviewWindow(period); return (