Public: anyone can read this; the crew can change it.

Design: /fleet/graph is built from the design system, and its fleet-* classes leave §5d #3220

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/3197-design-fleet-graph-is-built-from-the-des → main 2 commits fork

  • Merged. mason/3197-design-fleet-graph-is-built-from-the-des was merged into main as c4f2e44.

Review

Sign in to review this change.
7 files changed +74 −49
docs/DESIGN.md +4 −3

Diff not shown — a line 2,234 characters wide (limit 2,000). +4 −3

View file
src/web/fleet-graph.ts +16 −14
@@ -25,6 +25,7 @@ import type {
2525 FleetWindow,
2626 } from "../actions/fleet-graph.ts";
2727 import { html, type Html } from "./html.ts";
28 import { seriesDot } from "./ui.ts";
2829
2930 export const SVG_W = 600;
3031 const LEFT = 48;
@@ -119,8 +120,10 @@ export function windowSvg(graph: FleetGraph, w: FleetWindow): Html {
119120 });
120121
121122 return html`<svg
122 class="fleet-svg"
123 class="plot"
123124 viewBox="0 0 ${SVG_W} ${SVG_H}"
125 width="${SVG_W}"
126 height="${SVG_H}"
124127 preserveAspectRatio="xMinYMin meet"
125128 font-size="10"
126129 role="img"
@@ -129,29 +132,29 @@ export function windowSvg(graph: FleetGraph, w: FleetWindow): Html {
129132 data-spend-top="${spendTop}"
130133 data-runs-top="${runsTopWhole}"
131134 >
132 <g class="fleet-grid">
135 <g>
133136 <line x1="${LEFT}" y1="${TOP}" x2="${SVG_W - RIGHT}" y2="${TOP}" />
134137 <line x1="${LEFT}" y1="${TOP + SPEND_H / 2}" x2="${SVG_W - RIGHT}" y2="${TOP + SPEND_H / 2}" />
135138 <line x1="${LEFT}" y1="${spendY0}" x2="${SVG_W - RIGHT}" y2="${spendY0}" />
136139 <line x1="${LEFT}" y1="${runsY0 - RUNS_H}" x2="${SVG_W - RIGHT}" y2="${runsY0 - RUNS_H}" />
137140 <line x1="${LEFT}" y1="${runsY0}" x2="${SVG_W - RIGHT}" y2="${runsY0}" />
138141 </g>
139 <g class="fleet-ticks">
142 <g text-anchor="end">
140143 <text x="${LEFT - 6}" y="${TOP + 4}">${moneyTick(spendTop)}</text>
141144 <text x="${LEFT - 6}" y="${TOP + SPEND_H / 2 + 4}">${moneyTick(spendTop / 2)}</text>
142145 <text x="${LEFT - 6}" y="${spendY0 + 4}">$0</text>
143146 <text x="${LEFT - 6}" y="${runsY0 - RUNS_H + 4}">${runsTopWhole}</text>
144147 <text x="${LEFT - 6}" y="${runsY0 + 4}">0</text>
145148 </g>
146 <g class="fleet-panel-name">
149 <g class="plot-name">
147150 <text x="${LEFT + 4}" y="${TOP + 12}">spend, USD</text>
148151 <text x="${LEFT + 4}" y="${runsY0 - RUNS_H + 12}">runs ended</text>
149152 </g>
150 <g class="fleet-bars">
153 <g>
151154 ${segments("costUsd", spendTop, SPEND_H, spendY0)}
152155 ${segments("runs", runsTopWhole, RUNS_H, runsY0)}
153156 </g>
154 <g class="fleet-axis">
157 <g text-anchor="middle">
155158 ${w.buckets.map((b, i) =>
156159 (n - 1 - i) % every === 0
157160 ? html`<text x="${r2(LEFT + i * slot + slot / 2)}" y="${runsY0 + 14}">${bucketWords(w, b.start)}</text>`
@@ -164,11 +167,11 @@ export function windowSvg(graph: FleetGraph, w: FleetWindow): Html {
164167 /** The legend: one swatch per series, with its 14-day spend. */
165168 function legend(graph: FleetGraph): Html {
166169 const fortnight = graph.windows[1];
167 return html`<ul class="fleet-legend">
170 return html`<ul class="plot-legend">
168171 ${graph.series.map(
169172 (s, i) =>
170173 html`<li title="${s.repos.join(", ")}">
171 <span class="fleet-swatch fg-s${i}" aria-hidden="true"></span>
174 ${seriesDot(i)}
172175 ${s.repos.length === 1
173176 ? html`<a href="/${s.label}">${s.label}</a>`
174177 : s.label}
@@ -181,7 +184,7 @@ function legend(graph: FleetGraph): Html {
181184 /** Every repository's own figures, the chart's numbers as text. */
182185 function table(graph: FleetGraph): Html {
183186 return html`<div class="table-scroll">
184 <table class="table fleet-table" data-dense>
187 <table class="table" data-dense>
185188 <caption class="sr-only">
186189 Spend and runs per repository, last 24 hours and last 14 days
187190 </caption>
@@ -199,7 +202,7 @@ function table(graph: FleetGraph): Html {
199202 ${graph.repos.map(
200203 (r) => html`<tr data-repo="${r.repo}">
201204 <th scope="row">
202 <span class="fleet-swatch fg-s${r.series}" aria-hidden="true"></span>
205 ${seriesDot(r.series)}
203206 <a href="/${r.repo}">${r.repo}</a>
204207 </th>
205208 <td class="num">${money(r.day.costUsd)}</td>
@@ -232,7 +235,6 @@ export function selectionQuery(graph: FleetGraph): string {
232235 export function fleetGraphSection(graph: FleetGraph): Html {
233236 const q = selectionQuery(graph);
234237 return html`<section
235 class="fleet-graph"
236238 id="fleet-graph"
237239 data-src="/fleet/graph/live${q === "" ? "" : `?${q}`}"
238240 data-at="${graph.at}"
@@ -244,9 +246,9 @@ export function fleetGraphSection(graph: FleetGraph): Html {
244246 : "None of these repositories is yours to see spend for."}
245247 </p>`
246248 : html`${legend(graph)}
247 <div class="fleet-windows">
249 <div class="plots">
248250 ${graph.windows.map(
249 (w) => html`<figure class="fleet-window">
251 (w) => html`<figure>
250252 <figcaption>
251253 ${w.name === "24h" ? "Last 24 hours" : "Last 14 days"}
252254 <span class="muted"
@@ -262,7 +264,7 @@ export function fleetGraphSection(graph: FleetGraph): Html {
262264 ${graph.hidden > 0
263265 ? html`<p class="muted small">${hiddenWords(graph.hidden)}</p>`
264266 : null}
265 <p class="muted small fleet-assume">
267 <p class="muted small mt">
266268 Read from the usage ledger at
267269 <time datetime="${graph.at}">${graph.at.slice(11, 16)} UTC</time>. A
268270 run counts in the UTC hour or day it ended; spend is the provider's
src/web/pages/fleet-graph.ts +13 −11
@@ -23,7 +23,7 @@ import {
2323 } from "../fleet-graph.ts";
2424 import { html } from "../html.ts";
2525 import { fragment, page } from "../layout.ts";
26 import { btn } from "../ui.ts";
26 import { btn, field } from "../ui.ts";
2727
2828 /** The selection from the URL, or the refusal's words. */
2929 function selection(ctx: Ctx) {
@@ -65,16 +65,18 @@ export function fleetGraphPage(ctx: Ctx): Response {
6565 hour by hour and day by day.
6666 </p>
6767 </div>
68 <form method="get" action="/fleet/graph" class="fleet-pick">
69 <label for="fleet-repos">Repositories</label>
70 <input
71 id="fleet-repos"
72 name="repo"
73 value="${names}"
74 placeholder="owner/name owner/name — empty is everything you can write that spent"
75 autocomplete="off"
76 spellcheck="false"
77 />
68 <form method="get" action="/fleet/graph" class="field-row mb">
69 ${field({
70 label: "Repositories",
71 name: "repo",
72 id: "fleet-repos",
73 value: names,
74 placeholder:
75 "owner/name owner/name — empty is everything you can write that spent",
76 autocomplete: "off",
77 mono: true,
78 spellcheck: false,
79 })}
7880 ${btn("Draw", { type: "submit", variant: "secondary" })}
7981 </form>
8082 ${graph === undefined
src/web/style/components.ts +19
@@ -220,6 +220,24 @@ progress:not(.tests-bar)::-moz-progress-bar { background: var(--success); }
220220 .dot[data-tone="danger"] { background: var(--danger); }
221221 .dot[data-tone="attn"] { background: var(--attn); }
222222 .dot[data-tone="merged"] { background: var(--merged); }
223 .dot[data-series="0"] { background: var(--series-0); }
224 .dot[data-series="1"] { background: var(--series-1); }
225 .dot[data-series="2"] { background: var(--series-2); }
226 .dot[data-series="3"] { background: var(--series-3); }
227 .dot[data-series="4"] { background: var(--series-4); }
228 .dot[data-series="5"] { background: var(--series-5); }
229 .dot[data-series="6"] { background: var(--series-6); }
230 .dot[data-series="7"] { background: var(--series-7); }
231
232 /* ---- stacked bar chart: series stacked per bucket, drawn as SVG on the server ---- */
233 .plots { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); gap: 16px; margin: 0 0 16px; }
234 .plots figcaption { font-size: 13px; font-weight: 600; margin: 0 0 4px; display: flex; gap: 8px; align-items: baseline; }
235 .plot { display: block; width: 100%; height: auto; }
236 .plot line { stroke: var(--border); stroke-width: 1; }
237 .plot text { fill: var(--muted-fg); font-family: var(--mono); }
238 .plot .plot-name text { fill: var(--faint); font-family: inherit; }
239 .plot-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0 0 12px; font-size: 13px; }
240 .plot-legend li { display: inline-flex; align-items: center; gap: 6px; }
223241
224242 /* ---- meter: a share as a bar — spend against a ceiling, a window used, how sure ---- */
225243 meter {
@@ -421,6 +439,7 @@ a.row:hover .row-title { color: var(--accent); }
421439 }
422440 .table tbody tr:first-child td { border-top: none; }
423441 .table[data-dense] td, .table[data-dense] th { padding: 5px 10px; }
442 .table .num { text-align: right; }
424443 .table-scroll { overflow-x: auto; scrollbar-width: thin; }
425444 /* The row's name stays in view while a narrow screen scrolls the figures. */
426445 .table[data-sticky-lead] th:first-child,
src/web/style/content.ts +3 −20
@@ -361,17 +361,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
361361 }
362362 .backups-now { margin-top: 12px; }
363363 .backups-drill { overflow-wrap: anywhere; }
364 /* The fleet graph: a selection's spend and runs, hour by hour and day by day.
365 Series colours are Okabe–Ito (told apart with common colour-vision
366 deficiencies); the eighth, the folded "others", is grey. */
367 .fleet-pick { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 16px; }
368 .fleet-pick input { flex: 1 1 24em; min-width: 0; font-family: var(--mono); font-size: 13px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--fg); }
369 .fleet-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0 0 12px; padding: 0; list-style: none; font-size: 13px; }
370 .fleet-legend li { display: inline-flex; align-items: center; gap: 6px; }
371 .fleet-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; background: currentColor; vertical-align: middle; flex: none; }
372 .fleet-windows { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); gap: 16px; margin: 0 0 16px; }
373 .fleet-window { margin: 0; }
374 .fleet-window figcaption { font-size: 13px; font-weight: 600; margin: 0 0 4px; display: flex; gap: 8px; align-items: baseline; }
375364 .chart-box { color: var(--accent); aspect-ratio: 720 / 260; max-width: 720px; }
376365 .chart-box .chart { display: block; width: 100%; height: auto; }
377366 .chart-box text { fill: var(--muted-fg); font-family: var(--mono); }
@@ -413,14 +402,9 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
413402 .bgrid h2 { font-size: 14px; }
414403 .bmanage { display: grid; gap: 8px; }
415404 @media (max-width: 720px) { .bgrid { grid-template-columns: minmax(0, 1fr); } .bp-w2, .bp-w3 { grid-column: auto; } }
416 .fleet-svg { display: block; width: 100%; height: auto; aspect-ratio: 600 / 230; }
417 .fleet-grid line { stroke: var(--border); stroke-width: 1; }
418 .fleet-ticks text { fill: var(--muted-fg); text-anchor: end; font-family: var(--mono); }
419 .fleet-axis text { fill: var(--muted-fg); text-anchor: middle; font-family: var(--mono); }
420 .fleet-panel-name text { fill: var(--faint); }
421 .fleet-table .num { text-align: right; font-variant-numeric: tabular-nums; }
422 .fleet-table th[scope="row"] { font-weight: 500; white-space: nowrap; text-align: left; }
423 .fleet-assume { margin-top: 8px; }
405 /* The stacked bar chart's series colours, for its bars (fleet-graph.ts).
406 Okabe–Ito, told apart with common colour-vision deficiencies; the eighth,
407 the folded "others", is grey. */
424408 .fg-s0 { fill: var(--series-0); color: var(--series-0); }
425409 .fg-s1 { fill: var(--series-1); color: var(--series-1); }
426410 .fg-s2 { fill: var(--series-2); color: var(--series-2); }
@@ -429,7 +413,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
429413 .fg-s5 { fill: var(--series-5); color: var(--series-5); }
430414 .fg-s6 { fill: var(--series-6); color: var(--series-6); }
431415 .fg-s7 { fill: var(--series-7); color: var(--series-7); }
432 @media print { .fleet-pick { display: none; } }
433416 .backups-error { display: flex; align-items: center; gap: 8px; color: var(--danger); }
434417 .backups-restore { margin: 8px 0 0; padding: 8px 10px; font-size: 12.5px; white-space: pre-wrap; word-break: break-all; background: var(--muted); border-radius: var(--radius); }
435418 /* The records a domain waits on: type, name, value — as a zone file reads. */
src/web/ui.ts +9 −1
@@ -667,6 +667,11 @@ export function dot(tone: Tone = "default"): Html {
667667 ></span>`;
668668 }
669669
670 /** A chart series' swatch: the dot in the `--series-N` colour its bars wear. */
671 export function seriesDot(series: number): Html {
672 return html`<span class="dot" data-series="${series}" aria-hidden="true"></span>`;
673 }
674
670675 /**
671676 * A share as a bar: a whole percentage, 0–100, and what it is a share of in
672677 * words for a screen reader. `attn` when the share is a limit reached.
@@ -1028,6 +1033,8 @@ export interface FieldOptions {
10281033 maxlength?: number;
10291034 rows?: number;
10301035 mono?: boolean;
1036 /** `false` for names and paths, which a spelling checker only underlines. */
1037 spellcheck?: boolean;
10311038 id?: string;
10321039 }
10331040
@@ -1044,7 +1051,8 @@ export function field(o: FieldOptions): Html {
10441051 ${o.required ? raw("required") : null}
10451052 ${o.autofocus ? raw("autofocus") : null}
10461053 ${o.readonly ? raw("readonly") : null}
1047 ${o.placeholder ? html`placeholder="${o.placeholder}"` : null}`;
1054 ${o.placeholder ? html`placeholder="${o.placeholder}"` : null}
1055 ${o.spellcheck === false ? raw('spellcheck="false"') : null}`;
10481056 const control =
10491057 o.type === "textarea"
10501058 ? html`<textarea ${shared} rows="${o.rows ?? 6}" data-grow>
test/fleet-graph.test.ts +10
@@ -301,6 +301,16 @@ describe("the graph", () => {
301301 expect(body).toContain("Last 24 hours");
302302 expect(body).toContain("in the UTC hour or day it ended");
303303 expect(body).toContain('href="/fleet/graph.csv?repo=alice%2Ftwo"');
304 // Built from DESIGN.md §5: the field row, the stacked bar chart, the
305 // series dot and the table — no page-local fleet-* class (#3197).
306 expect(body).toContain('<form method="get" action="/fleet/graph" class="field-row mb">');
307 expect(body).toMatch(/<input\s+class="input mono" id="fleet-repos"[^>]*spellcheck="false"/);
308 expect(body).toContain('<div class="plots">');
309 expect(body).toMatch(/<svg\s+class="plot"[^>]*width="600"\s+height="230"/);
310 expect(body).toContain('<ul class="plot-legend">');
311 expect(body).toContain('<span class="dot" data-series="0" aria-hidden="true"></span>');
312 expect(body).toContain('<table class="table" data-dense>');
313 expect(body).not.toMatch(/class="[^"]*\bfleet-/);
304314
305315 const live = await fetch(`${servers.url}/fleet/graph/live?repo=alice/two`, {
306316 headers: { cookie },

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs