milestone 30: overview as a dashboard, explicit health contract, period aggregations
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s

This commit is contained in:
2026-08-22 16:45:15 +02:00
parent 17422fac21
commit 648d9b4496
89 changed files with 7222 additions and 4239 deletions
+209
View File
@@ -0,0 +1,209 @@
/**
* A breakdown as a ring, a legend and a table.
*
* The ring is decoration: it carries `aria-hidden` and `focusable="false"`,
* because a non-focusable SVG is still in the accessibility tree and would
* announce a pile of unlabelled paths. Everything the ring says is said again in
* the legend — visibly, with the share and the count — and once more in a
* visually hidden table, which is the surface a screen reader reads.
*
* Labels can collide: two rows can both be "Unknown", and one upstream name can
* appear under two route kinds. Identity is therefore the caller's `key`, and an
* entry that needs disambiguating carries `secondary` text saying which it is.
*/
import * as stylex from "@stylexjs/stylex";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
import { layoutDonut, type DonutSlice } from "./donutLayout";
const SIZE = 180;
const THICKNESS = 36;
const numberFormat = new Intl.NumberFormat();
/** The width at which the page puts the two donuts side by side, and the page's
* own grid switches on the same query. StyleX will not take it from an import,
* so it is written out in both modules and must be changed in both. */
const TWO_COLUMN = "@media (min-width: 1280px)";
const styles = stylex.create({
empty: {
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: SIZE,
borderRadius: "0.25rem",
borderWidth: 1,
borderStyle: "dashed",
borderColor: colors.borderStrong,
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
/**
* Centred while the panels are stacked, left-anchored once they are side by
* side. Stacked, the panel is as wide as the page and a ring pinned to the
* left edge reads as a mistake; in a column it is one of a pair and lines up
* with everything above it.
*/
body: {
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: { default: "center", [TWO_COLUMN]: "flex-start" },
gap: "1.25rem",
},
ring: {
flexShrink: 0,
},
/**
* Capped and left-anchored. Without the cap the row justifies across whatever
* the panel is given — most of a metre of whitespace on a wide monitor — and a
* label stops reading as belonging to the count opposite it.
*/
legend: {
flex: 1,
minWidth: "12rem",
maxWidth: "24rem",
display: "flex",
flexDirection: "column",
gap: "0.25rem",
listStyleType: "none",
padding: 0,
margin: 0,
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
legendItem: {
display: "flex",
alignItems: "baseline",
gap: "0.5rem",
},
swatch: {
flexShrink: 0,
alignSelf: "center",
display: "inline-block",
width: "0.625rem",
height: "0.625rem",
borderRadius: "0.125rem",
},
/** Dynamic: the swatch takes the colour the ring is drawn in. */
swatchColor: (color: string) => ({ backgroundColor: color }),
label: {
flex: 1,
minWidth: 0,
overflowWrap: "anywhere",
},
secondary: {
marginLeft: "0.375rem",
fontSize: "0.75rem",
lineHeight: "1rem",
color: colors.textMuted,
},
count: {
color: colors.textSecondary,
},
share: {
minWidth: "3rem",
textAlign: "right",
color: colors.textMuted,
},
});
function sharePercent(share: number): string {
return `${(share * 100).toFixed(1)}%`;
}
export default function Donut({
slices,
caption,
unit,
}: {
slices: DonutSlice[];
/** Names the hidden table, so a screen reader knows which breakdown it is in. */
caption: string;
/** The column header for the counted thing, e.g. "Queries". */
unit: string;
}) {
const layout = layoutDonut(slices, SIZE, THICKNESS);
if (layout.total === 0) {
return <div {...stylex.props(styles.empty)}>No queries in this period.</div>;
}
return (
<div {...stylex.props(styles.body)}>
<svg
aria-hidden="true"
focusable="false"
width={SIZE}
height={SIZE}
viewBox={`0 0 ${SIZE} ${SIZE}`}
{...stylex.props(styles.ring)}
>
{layout.arcs.map((arc) => (
// The stroke is what keeps a shared hue from lying. Colour is a pure
// function of identity, so two neighbouring slices can come out the
// same; outlined in the panel's own colour they still read as two
// shapes rather than merging into one. Attributes rather than a
// class, as the client chart's segments are, so the separation is
// visible to a test and not only to a stylesheet.
<path
key={arc.slice.key}
d={arc.d}
fill={arc.slice.color}
fillRule="evenodd"
stroke={colors.surfaceRaised}
strokeWidth={1}
/>
))}
</svg>
<ul {...stylex.props(styles.legend)}>
{layout.arcs.map((arc) => (
<li key={arc.slice.key} {...stylex.props(styles.legendItem)}>
<span
aria-hidden="true"
{...stylex.props(styles.swatch, styles.swatchColor(arc.slice.color))}
/>
<span {...stylex.props(styles.label)}>
{arc.slice.label}
{arc.slice.secondary !== undefined && (
<span {...stylex.props(styles.secondary)}>{arc.slice.secondary}</span>
)}
</span>
<span {...stylex.props(styles.count, shared.tabularNums)}>
{numberFormat.format(arc.slice.value)}
</span>
<span {...stylex.props(styles.share, shared.tabularNums)}>{sharePercent(arc.share)}</span>
</li>
))}
</ul>
<div {...stylex.props(shared.srOnly)}>
<table>
<caption>{caption}</caption>
<thead>
<tr>
<th scope="col">Entry</th>
<th scope="col">{unit}</th>
<th scope="col">Share</th>
</tr>
</thead>
<tbody>
{layout.arcs.map((arc) => (
<tr key={arc.slice.key}>
<th scope="row">
{arc.slice.secondary === undefined
? arc.slice.label
: `${arc.slice.label} (${arc.slice.secondary})`}
</th>
<td>{arc.slice.value}</td>
<td>{sharePercent(arc.share)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}