overview: one endpoint, live projections and a response cache (m36)

This commit is contained in:
2026-08-27 17:48:20 +02:00
parent 7cdb74ec17
commit c8470724ce
40 changed files with 3298 additions and 2242 deletions
+59 -140
View File
@@ -8,9 +8,9 @@
* 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.
* 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";
@@ -18,16 +18,16 @@ 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 { Period, StatsRoutes, StatsTypes } from "@/lib/types";
import { styles as shared } from "@/ui/styles";
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, PERIODS } from "./period";
import { DEFAULT_PERIOD } from "./period";
import { qtypeKey, routeKey, seriesColor } from "./seriesColors";
/**
@@ -48,48 +48,6 @@ const ROUTE_LABELS = {
} 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,
@@ -110,54 +68,20 @@ const styles = stylex.create({
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 (
<div role="group" aria-label="Period" {...stylex.props(styles.periodGroup)}>
{PERIODS.map((option) => (
<button
key={option}
type="button"
aria-pressed={option === period}
onClick={() => onChange(option)}
{...stylex.props(
styles.period,
option === period ? styles.periodSelected : styles.periodIdle,
shared.focusRing,
)}
>
{option}
</button>
))}
</div>
);
}
/**
* One panel's three states. Loading and error are the panel's own: a failure
* here never reaches past this box, which is what keeps a failed donut from
* blanking the charts beside it.
* 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 PanelBody<T>({ panel, children }: { panel: Panel<T>; children: (data: T) => React.ReactNode }) {
function PageBody({ panel, children }: { panel: Panel<Overview>; children: (data: Overview) => React.ReactNode }) {
if (panel.status === "error") return <InlineError error={panel.error} onRetry={panel.retry} />;
if (panel.status === "loading") {
return (
<p role="status" {...stylex.props(styles.loading, shared.pulse)}>
Loading
</p>
);
}
if (panel.status === "loading") return <OverviewLoading />;
return <>{children(panel.data)}</>;
}
function typeSlices(data: StatsTypes): DonutSlice[] {
return data.types.map((row) => ({
function typeSlices(types: OverviewTypeRow[]): DonutSlice[] {
return types.map((row) => ({
key: qtypeKey(row.qtype),
label: row.qtype === null ? "Unknown" : qtypeName(row.qtype),
value: row.count,
@@ -171,8 +95,8 @@ function typeSlices(data: StatsTypes): DonutSlice[] {
* 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) => {
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),
@@ -190,59 +114,54 @@ export default function OverviewPage() {
const overview = useOverviewWindow(period);
return (
<div {...stylex.props(styles.page)}>
<div {...stylex.props(styles.headingRow)}>
<h1 {...stylex.props(styles.heading)}>Overview</h1>
<PeriodPicker
period={period}
onChange={(next) => void navigate({ search: (prev) => ({ ...prev, period: next }) })}
/>
</div>
<OverviewFrame
period={period}
onChange={(next) => void navigate({ search: (prev) => ({ ...prev, period: next }) })}
>
<PageBody panel={overview}>
{(data) => (
<>
<StatTiles stats={{ since: data.since, until: data.until, ...data.totals }} />
<PanelBody panel={overview.totals}>{(totals) => <StatTiles stats={totals} />}</PanelBody>
{/* One notice for the page: every panel came out of this one
response, so a second copy would only repeat this sentence. */}
<CoverageNotice coverage={data.coverage} />
{/* One notice for the page: every panel is judged against the same window,
so a second copy would only repeat this sentence. */}
{overview.coverage !== null && <CoverageNotice coverage={overview.coverage} />}
<section aria-labelledby="overview-queries" {...stylex.props(styles.panel)}>
<h2 id="overview-queries" {...stylex.props(styles.panelHeading)}>
Queries over time
</h2>
<TimeseriesChart data={data} />
</section>
<section aria-labelledby="overview-queries" {...stylex.props(styles.panel)}>
<h2 id="overview-queries" {...stylex.props(styles.panelHeading)}>
Queries over time
</h2>
<PanelBody panel={overview.timeseries}>{(data) => <TimeseriesChart data={data} />}</PanelBody>
</section>
<section aria-labelledby="overview-clients" {...stylex.props(styles.panel)}>
<h2 id="overview-clients" {...stylex.props(styles.panelHeading)}>
Client activity over time
</h2>
<ClientChart data={data} />
</section>
<section aria-labelledby="overview-clients" {...stylex.props(styles.panel)}>
<h2 id="overview-clients" {...stylex.props(styles.panelHeading)}>
Client activity over time
</h2>
<PanelBody panel={overview.clients}>{(data) => <ClientChart data={data} />}</PanelBody>
</section>
<div {...stylex.props(styles.donutRow)}>
<section aria-labelledby="overview-types" {...stylex.props(styles.panel)}>
<h2 id="overview-types" {...stylex.props(styles.panelHeading)}>
Query types
</h2>
<PanelBody panel={overview.types}>
{(data) => <Donut slices={typeSlices(data)} caption="Queries by DNS type" unit="Queries" />}
</PanelBody>
</section>
<section aria-labelledby="overview-routes" {...stylex.props(styles.panel)}>
<h2 id="overview-routes" {...stylex.props(styles.panelHeading)}>
Upstream servers
</h2>
<PanelBody panel={overview.routes}>
{(data) => (
<Donut
slices={routeSlices(data)}
caption="Queries by how they were answered"
unit="Queries"
/>
)}
</PanelBody>
</section>
</div>
</div>
<div {...stylex.props(styles.donutRow)}>
<section aria-labelledby="overview-types" {...stylex.props(styles.panel)}>
<h2 id="overview-types" {...stylex.props(styles.panelHeading)}>
Query types
</h2>
<Donut slices={typeSlices(data.types)} caption="Queries by DNS type" unit="Queries" />
</section>
<section aria-labelledby="overview-routes" {...stylex.props(styles.panel)}>
<h2 id="overview-routes" {...stylex.props(styles.panelHeading)}>
Upstream servers
</h2>
<Donut
slices={routeSlices(data.routes)}
caption="Queries by how they were answered"
unit="Queries"
/>
</section>
</div>
</>
)}
</PageBody>
</OverviewFrame>
);
}