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

A repository's Flows page, what flows in and out, speaks the home page's language #9907

Merged opened this

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

Merged

mason/forge:mason/9903-a-repository-s-flows-page-what-flows-in → main 2 commits fork

  • Merged. mason/9903-a-repository-s-flows-page-what-flows-in was merged into main as 35a35cf.

Review

Sign in to review this change.
2 files changed +63 −3
src/web/pages/flows.ts +3 −3
@@ -21,7 +21,7 @@ import { crumbsFor, loadRepo, notFoundPage, pageHead, repoBase, repoHeader, repo
2121 import { field, readForm } from "../form.ts";
2222 import { html, type Html } from "../html.ts";
2323 import { flashTo, page } from "../layout.ts";
24 import { badge, btn, type Tone, card, listCard, row, time } from "../ui.ts";
24 import { badge, btn, type Tone, row, time } from "../ui.ts";
2525
2626 export const lineRow = (l: FlowLine): Html =>
2727 row({
@@ -43,7 +43,7 @@ const actionForm = (a: FlowAction): Html =>
4343 : html`${btn(a.label, { size: "sm", variant: "secondary", href: a.href })}`;
4444
4545 function side(title: string, lines: readonly Html[], empty: string): Html {
46 return card(lines.length === 0 ? html`<p class="muted">${empty}</p>` : listCard([...lines]), { head: html`${title}`, pad: lines.length === 0 });
46 return html`<section class="settings-block"><h2>${title}</h2>${lines.length === 0 ? html`<p class="muted">${empty}</p>` : html`<ul class="list setting-list">${[...lines]}</ul>`}</section>`;
4747 }
4848
4949 /**
@@ -82,7 +82,7 @@ export async function flowsPage(ctx: Ctx): Promise<Response> {
8282 : {}),
8383 }),
8484 );
85 const body = html`${repoHeader(ctx, repo, "flows")}
85 const body = html`${repoHeader(ctx, repo, "flows", { compact: true, ownTitle: true })}
8686 ${pageHead("Flows", { sub: "What comes in on the left, what goes out on the right." })}
8787 ${flowMap(repo.name, flows.inbound.map(lineRow), depRows, flows.outbound.map(lineRow))}
8888 ${admin
test/flows-language.test.ts added +60
@@ -0,0 +1,60 @@
1 /** The Flows page speaks the home page's language (#9903): headings and rows, no card boxes. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { createHmac } from "node:crypto";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp, type App } from "../src/app.ts";
8 import { PREVIEW_ADMIN, PREVIEW_REPO } from "../src/spawn/preview-looks.ts";
9 import { seedPreview } from "../src/spawn/preview-seed.ts";
10 import { serve } from "../src/http/server.ts";
11 import { readFileSync } from "node:fs";
12
13 let dir: string;
14 let app: App;
15 let servers: Awaited<ReturnType<typeof serve>>;
16 const KEY = "flows-language-key";
17 const sign = (id: string, name: string, role: string) => {
18 const said = `${id}|${name}|${role}|${Date.now() + 60_000}`;
19 return { "x-forge-identity": said, "x-forge-signature": createHmac("sha256", KEY).update(said).digest("base64url") };
20 };
21 beforeAll(async () => {
22 dir = await mkdtemp(join(tmpdir(), "forge-flows-lang-"));
23 app = createApp({ dataDir: dir, port: 0, previewIdentityKey: KEY });
24 const seeded = await seedPreview(app, { FORGE_APP_ENV: "preview" });
25 if (!seeded.ok) throw new Error(seeded.error);
26 servers = await serve(app);
27 }, 30_000);
28 afterAll(async () => {
29 servers.stop();
30 app.close();
31 await rm(dir, { recursive: true, force: true });
32 });
33
34 const url = () => `${servers.url}/${PREVIEW_ADMIN}/${PREVIEW_REPO}/flows`;
35
36 test("one h1; in and out are plain sections under h2; no card boxes", async () => {
37 const html = await (await fetch(url(), { headers: sign("u1", "trevato", "admin") })).text();
38 expect(html.match(/<h1[ >]/g)?.length).toBe(1);
39 expect(html).toMatch(/<h2>Flows in<\/h2>/);
40 expect(html).toMatch(/<h2>Flows out<\/h2>/);
41 expect(html).toMatch(/<h2>Packages<\/h2>/);
42 expect(html).toContain("flow-map");
43 expect(html).not.toContain('class="card');
44 expect(html).not.toContain("card-body");
45 });
46
47 test("the page source no longer calls card(", () => {
48 expect(readFileSync(join(import.meta.dir, "../src/web/pages/flows.ts"), "utf8")).not.toMatch(/\bcard\(/);
49 });
50
51 test("an admin has the same-origin read form; a guest has none", async () => {
52 const admin = sign("u1", "trevato", "admin");
53 expect(await (await fetch(url(), { headers: admin })).text()).toContain("Read upstream releases now");
54 const post = await fetch(`${url()}/check`, { method: "POST", redirect: "manual", headers: { ...admin, origin: "https://evil.example" } });
55 expect(post.status).toBe(403);
56 const guest = await (await fetch(url())).text();
57 expect(guest).not.toContain("Read upstream releases now");
58 expect(guest).not.toContain("/flows/check");
59 expect(guest).not.toContain("/flows/probe");
60 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs