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

/system's btop panels draw §5's card, row and badge, and the .bt-* classes leave the markup #4186

Merged opened this

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

Merged

scout/forge:scout/4166-system-s-btop-panels-draw-5-s-card-row-a → main 1 commit fork

  • Merged. scout/4166-system-s-btop-panels-draw-5-s-card-row-a was merged into main as 11d2ac4.

Review

Sign in to review this change.
2 files changed +39 −15
src/web/pages/system.ts +24 −15
@@ -24,7 +24,7 @@ import { notFoundPage, signInFirst } from "../chrome.ts";
2424 import { html, raw, type Html } from "../html.ts";
2525 import { page } from "../layout.ts";
2626 import { systemLiveJs } from "../system-live.ts";
27 import { card, empty, pill } from "../ui.ts";
27 import { badge, card, empty, meter, pill, row } from "../ui.ts";
2828
2929 /** A group's memory, and a cell's beside its cap and who holds the cap. */
3030 const memoryOf = (g: { rssBytes: number; capBytes: number | null; capBy: "kernel" | "watch" | null }): Html =>
@@ -38,20 +38,29 @@ const coresOf = (n: number | null): string => (n === null ? "—" : n.toFixed(2)
3838 const rateOf = (n: number | null): string => (n === null ? "—" : `${bytes(n)}/s`);
3939
4040 /** One line of the machine card: a name, a meter, its figure and (for a core) its history. */
41 const meterRow = (name: string, value: number | null, max: number, figure: string, history = ""): Html =>
42 html`<div class="bt-row"><span class="bt-name" title="${name}">${name}</span>
43 <meter class="bt-bar" aria-label="${name} ${figure}" min="0" max="${String(max)}" high="${String(max * 0.85)}" value="${String(value ?? 0)}">${figure}</meter>
44 <span class="bt-val">${figure}</span>${history ? html`<span class="bt-hist" aria-hidden="true">${history}</span>` : null}</div>`;
41 const meterRow = (name: string, value: number | null, max: number, figure: string, history = ""): Html => {
42 const share = max > 0 ? ((value ?? 0) / max) * 100 : 0;
43 return row({
44 title: name,
45 titleAttr: name,
46 meta: html`${meter(share, `${name} ${figure}`, share >= 85 ? "attn" : undefined)}${history ? html` <span class="mono" aria-hidden="true">${history}</span>` : null}`,
47 trail: html`<span class="mono">${figure}</span>`,
48 });
49 };
50
51 /** A name and one figure, no bar: load, a disk's I/O, a link's rates. */
52 const figureRow = (name: string, figure: string, data: Record<string, string> = {}): Html =>
53 row({ title: name, trail: html`<span class="mono">${figure}</span>`, data });
4554
4655 /** Processes grouped forge, crew, check, app, host; the client re-sorts them by CPU or memory (`system-live.ts`). */
4756 const KIND_ORDER = ["forge", "crew", "check", "app", "host"] as const;
4857 function procTable(v: SystemView): Html {
4958 const groups = KIND_ORDER.flatMap((k) => v.groups.filter((g) => g.kind === k));
50 return html`<div class="table-wrap"><table class="table bt-procs" aria-label="Processes by what launched them">
59 return html`<div class="table-wrap"><table class="table" data-dense aria-label="Processes by what launched them">
5160 <thead><tr><th scope="col">pid</th><th scope="col">command</th><th scope="col" class="num">cpu</th><th scope="col" class="num">mem</th><th scope="col" class="num">age</th></tr></thead>
5261 ${groups.map(
5362 (g) => html`<tbody data-kind="${g.kind}" data-cpu="${String(g.cpuCores ?? 0)}" data-mem="${String(g.rssBytes)}">
54 <tr class="bt-group"><th scope="colgroup" colspan="2">${g.label} <span class="muted">${String(g.procs)} procs</span></th>
63 <tr data-group><th scope="colgroup" colspan="2"><strong>${g.label}</strong> ${badge(`${String(g.procs)} procs`, { variant: "secondary" })}</th>
5564 <td class="num">${coresOf(g.cpuCores)}</td><td class="num">${memoryOf(g)}</td><td class="num">${span(g.oldestSeconds)}</td></tr>
5665 ${g.top.map(
5766 (p) => html`<tr data-cpu="${String(p.cpuCores ?? 0)}" data-mem="${String(p.rssBytes)}"><td class="mono">${String(p.pid)}</td><td class="mono">${p.comm}</td>
@@ -112,15 +121,15 @@ export function regions(v: SystemView): Record<string, string> {
112121 ? html`${meterRow("mem", v.memory.usedBytes, v.memory.totalBytes, `${bytes(v.memory.usedBytes)} / ${bytes(v.memory.totalBytes)}`)}
113122 ${meterRow("swap", v.memory.swapUsedBytes, v.memory.swapTotalBytes, v.memory.swapTotalBytes === 0 ? "none" : `${bytes(v.memory.swapUsedBytes)} / ${bytes(v.memory.swapTotalBytes)}`)}`
114123 : null}
115 ${v.load ? html`<div class="bt-row"><span class="bt-name">load</span><span class="bt-val">${v.load.map((l) => l.toFixed(2)).join(" ")}</span></div>` : null}`,
124 ${v.load ? figureRow("load", v.load.map((l) => l.toFixed(2)).join(" ")) : null}`,
116125 temps: html`${v.temps.map((t) => `${t.chip} ${t.label} ${t.celsius.toFixed(0)}°C`).join(" · ")}`,
117126 "sys-disks": card(
118 html`<div class="bt-grid">
127 html`<ul class="list">
119128 ${v.disks.map((d) => meterRow(d.mount, d.usedBytes, d.totalBytes, `${bytes(d.usedBytes)} / ${bytes(d.totalBytes)} ${d.fs}`))}
120 ${v.io.map((d) => html`<div class="bt-row" data-io="${d.name}"><span class="bt-name">${d.name}</span><span class="bt-val">R ${rateOf(d.readBytesPerSecond)} W ${rateOf(d.writeBytesPerSecond)}</span></div>`)}
121 ${v.net.map((n) => html`<div class="bt-row" data-net="${n.name}"><span class="bt-name">${n.name}</span><span class="bt-val">↓ ${rateOf(n.rxBytesPerSecond)} ↑ ${rateOf(n.txBytesPerSecond)}</span></div>`)}
122 </div>`,
123 { head: html`<h2>Disks and network</h2>` },
129 ${v.io.map((d) => figureRow(d.name, `R ${rateOf(d.readBytesPerSecond)} W ${rateOf(d.writeBytesPerSecond)}`, { io: d.name }))}
130 ${v.net.map((n) => figureRow(n.name, `↓ ${rateOf(n.rxBytesPerSecond)} ↑ ${rateOf(n.txBytesPerSecond)}`, { net: n.name }))}
131 </ul>`,
132 { head: html`<h2>Disks and network</h2>`, pad: false },
124133 ),
125134 "sys-procs": procTable(v),
126135 };
@@ -171,8 +180,8 @@ export function systemPage(ctx: Ctx): Response {
171180 <div id="sys-dup">${at("sys-dup")}</div>
172181 ${card(
173182 html`<div id="summary" class="row-meta">${at("summary")}</div>
174 <div class="bt-grid" id="cores" aria-label="Per-core CPU, percent, with its last readings">${at("cores")}</div>
175 <div class="bt-grid" id="mem">${at("mem")}</div>
183 <ul class="list" id="cores" aria-label="Per-core CPU, percent, with its last readings">${at("cores")}</ul>
184 <ul class="list" id="mem">${at("mem")}</ul>
176185 ${caps}
177186 <p class="small mono" id="temps">${at("temps")}</p>`,
178187 { head: html`<h2>Machine</h2>` },
test/system-btop.test.ts +15
@@ -101,6 +101,21 @@ describe("/system as btop", () => {
101101 }
102102 });
103103
104 test("the panels are drawn with §5's rows, meters and card, and no .bt-* class remains", () => {
105 const r = regions(view);
106 const all = Object.values(r).join("\n");
107 for (const old of ["bt-bar", "bt-grid", "bt-group", "bt-hist", "bt-name", "bt-procs", "bt-row", "bt-val"]) expect(all).not.toContain(old);
108 expect(r["cores"]).toContain('class="row"');
109 expect(r["cores"]).toContain('class="row-title"');
110 expect(r["cores"]).toContain("<meter");
111 expect(r["mem"]).toContain("load");
112 expect(r["sys-disks"]).toContain('class="card"');
113 expect(r["sys-disks"]).toContain('data-io="nvme0n1"');
114 expect(r["sys-disks"]).toContain('data-net="eth0"');
115 expect(r["sys-procs"]).toContain('class="table"');
116 expect(r["sys-procs"]).toContain("2 procs");
117 });
118
104119 test("diskstats keep whole devices only", () => {
105120 const text = " 259 0 nvme0n1 10 0 8 0 20 0 16 0\n 259 1 nvme0n1p1 1 0 2 0 1 0 2 0\n 7 0 loop0 1 0 2 0 0 0 0 0\n";
106121 expect(parseDiskstats(text, (n) => n === "nvme0n1")).toEqual([{ name: "nvme0n1", read: 8 * 512, write: 16 * 512 }]);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs