/** * Overview: what the resolver did over a period the reader chooses, for the * household or for one device, in the layout the decision record settled * (ui-visual-redesign.md) — four totals, two full-width charts, then the cache * rate and the two breakdown rings in a row of cards. 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 scope is URL state, so a view is a link: `/overview?period=1h&client=…` * 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 CoverageNotice from "@/lib/CoverageNotice"; import InlineError from "@/lib/InlineError"; import { qtypeName } from "@/features/provenance/qtype"; import type { Overview, OverviewRouteRow, OverviewTypeRow } from "@/lib/types"; import Card from "@/ui/Card"; import CacheCard from "./CacheCard"; import ClientChart from "./ClientChart"; import Donut from "./Donut"; import StatTiles from "./StatTiles"; import TimeseriesChart from "./TimeseriesChart"; import type { DonutSlice } from "./Donut"; import { OverviewFrame, OverviewLoading, useOverviewScope } from "./OverviewFrame"; import { useOverviewWindow, type Panel } from "./overviewWindow"; import { qtypeKey, routeKey, seriesColor, typeRampColor } from "./seriesColors"; const ROUTE_LABELS = { blocked: "Blocked", cache: "Cache", local: "Local", rejected: "Rejected", upstream: "Upstream", forward_zone: "Forward zone", } as const; const styles = stylex.create({ /** As many cards per row as fit at 20rem each: three on a desktop, one on a phone. */ cards: { display: "grid", gap: "1.25rem", gridTemplateColumns: "repeat(auto-fit, minmax(20rem, 1fr))", }, }); /** * The page's three states. The heading and the toolbar 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)}; } /** The API ranks the types by count, so the ramp's darkest step is the busiest type. */ function typeSlices(types: OverviewTypeRow[]): DonutSlice[] { return types.map((row, rank) => ({ key: qtypeKey(row.qtype), label: row.qtype === null ? "Unknown" : qtypeName(row.qtype), value: row.count, color: typeRampColor(rank), })); } /** * 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)), }; }); } /** What the cache card reads: hits are the buckets' cached counts, forwarded is every upstream or zone answer. */ function cacheOf(data: Overview) { return { queries: data.totals.queries, hits: data.buckets.reduce((sum, bucket) => sum + bucket.cached, 0), forwarded: data.routes .filter((row) => row.route === "upstream" || row.route === "forward_zone") .reduce((sum, row) => sum + row.count, 0), avg_response_time_us: data.totals.avg_response_time_us, }; } export default function OverviewPage() { const [scope, setScope] = useOverviewScope(); const overview = useOverviewWindow(scope.period, scope.client); return ( {(data) => ( <> {/* One notice for the page: every panel came out of this one response, so a second copy would only repeat this sentence. */}
)}
); }