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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9903-a-repository-s-flows-page-what-flows-in → main 2 commits fork
2 files
changed +63
−3
src/web/pages/flows.ts +3 −3
| @@ -21,7 +21,7 @@ import { crumbsFor, loadRepo, notFoundPage, pageHead, repoBase, repoHeader, repo | ||
| 21 | 21 | import { field, readForm } from "../form.ts"; |
| 22 | 22 | import { html, type Html } from "../html.ts"; |
| 23 | 23 | 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"; | |
| 25 | 25 | |
| 26 | 26 | export const lineRow = (l: FlowLine): Html => |
| 27 | 27 | row({ |
| @@ -43,7 +43,7 @@ const actionForm = (a: FlowAction): Html => | ||
| 43 | 43 | : html`${btn(a.label, { size: "sm", variant: "secondary", href: a.href })}`; |
| 44 | 44 | |
| 45 | 45 | 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>`; | |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | /** |
| @@ -82,7 +82,7 @@ export async function flowsPage(ctx: Ctx): Promise<Response> { | ||
| 82 | 82 | : {}), |
| 83 | 83 | }), |
| 84 | 84 | ); |
| 85 | const body = html`${repoHeader(ctx, repo, "flows")} | |
| 85 | const body = html`${repoHeader(ctx, repo, "flows", { compact: true, ownTitle: true })} | |
| 86 | 86 | ${pageHead("Flows", { sub: "What comes in on the left, what goes out on the right." })} |
| 87 | 87 | ${flowMap(repo.name, flows.inbound.map(lineRow), depRows, flows.outbound.map(lineRow))} |
| 88 | 88 | ${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 | }); |