/** * 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. * * One request feeds every panel (`overviewWindow.ts`), so the page has one * loading state and one error state rather than six: there is no longer a * partial answer to render, and nothing left for a panel to disagree about. */ 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/provenance/qtype"; import type { Overview, OverviewRouteRow, OverviewTypeRow } from "@/lib/types"; 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 "./Donut"; import { OverviewFrame, OverviewLoading } from "./OverviewFrame"; import { useOverviewWindow, type Panel } from "./overviewWindow"; import { DEFAULT_PERIOD } 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({ 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))" }, }, }); /** * The page's three states. The heading and the period picker stay put through * all three, so the reader can rescope or retry without waiting for anything. */ function PageBody({ panel, children }: { panel: Panel; children: (data: Overview) => React.ReactNode }) { if (panel.status === "error") return ; if (panel.status === "loading") return ; return <>{children(panel.data)}; } function typeSlices(types: OverviewTypeRow[]): DonutSlice[] { return 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(routes: OverviewRouteRow[]): DonutSlice[] { return 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 ( void navigate({ search: (prev) => ({ ...prev, period: next }) })} > {(data) => ( <> {/* One notice for the page: every panel came out of this one response, so a second copy would only repeat this sentence. */}

Queries over time

Client activity over time

Query types

Upstream servers

)}
); }