Public: anyone can read this; the crew can change it.
Settings › Cloud accounts, the sealed provider credentials, speaks the home page's language #9879
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9878-settings-cloud-accounts-the-sealed-provi → main 2 commits fork
3 files
changed +34
−9
docs/design/areas/settings.md +2 −2
| @@ -105,8 +105,8 @@ the row at every width, so nothing squeezes beside the title on a 390 px phone. | ||
| 105 | 105 | |
| 106 | 106 | Settings › Cloud (`src/web/pages/cloud.ts`) adds no component class. As on the home page it is headings over rows: Cloud accounts, |
| 107 | 107 | Add an account and Every read are each a `<section class="settings-block">` with an `<h2>`; each account is a `row()` in |
| 108 | `<ul class="list setting-list">`; with none, one plain sentence says how to add one. `/settings/cloud/audit` is unchanged. | |
| 109 | `test/cloud-page-shape.test.ts` holds the shape. | |
| 108 | `<ul class="list setting-list">`; with none, one plain sentence says how to add one. `/settings/cloud/audit` is one `settings-block`, Today's vendor calls, | |
| 109 | its rows in a `.list` or one plain sentence when there are none (#9878). `test/cloud-page-shape.test.ts` holds the shape. | |
| 110 | 110 | |
| 111 | 111 | Settings › Telemetry (`telemetryPage()` in `src/web/pages/telemetry.ts`, #9806) adds no class: two `<section class="settings-block">` blocks, Telemetry grants (one plain paragraph, then each agent a `row()` with its Grant/Revoke form in a `.setting-list`) and Reads; an empty list is one plain sentence. Pinned by `test/telemetry-blocks.test.ts`. |
| 112 | 112 |
src/web/pages/cloud.ts +7 −7
| @@ -30,7 +30,7 @@ import { notFoundPage, settingsLayout, signInFirst } from "../chrome.ts"; | ||
| 30 | 30 | import { field, readForm } from "../form.ts"; |
| 31 | 31 | import { html, type Html } from "../html.ts"; |
| 32 | 32 | import { flashTo, page } from "../layout.ts"; |
| 33 | import { btn, card, confirmAction, icon, row, time } from "../ui.ts"; | |
| 33 | import { btn, confirmAction, icon, row, time } from "../ui.ts"; | |
| 34 | 34 | import { accountSections } from "./origins.ts"; |
| 35 | 35 | |
| 36 | 36 | const BACK = "/settings/cloud"; |
| @@ -223,9 +223,10 @@ export function cloudAuditPage(ctx: Ctx): Response { | ||
| 223 | 223 | body: html`<div class="page-head"><h1>Cloud audit</h1></div> |
| 224 | 224 | ${settingsLayout( |
| 225 | 225 | accountSections("cloud", { admin: true }), |
| 226 | card( | |
| 227 | rows.length === 0 | |
| 228 | ? html`<p class="muted small card-body">No vendor call today. Rows are kept 90 days.</p>` | |
| 226 | html`<section class="settings-block"> | |
| 227 | <h2>Today's vendor calls</h2> | |
| 228 | ${rows.length === 0 | |
| 229 | ? html`<p class="muted small">No vendor call today. Rows are kept 90 days.</p>` | |
| 229 | 230 | : html`<ul class="list"> |
| 230 | 231 | ${rows.map((r) => |
| 231 | 232 | row({ |
| @@ -235,9 +236,8 @@ export function cloudAuditPage(ctx: Ctx): Response { | ||
| 235 | 236 | meta: html`<span class="mono">${r.target}</span> · ${r.bytesOut} bytes out · ${time(r.at)}`, |
| 236 | 237 | }), |
| 237 | 238 | )} |
| 238 | </ul>`, | |
| 239 | { head: html`${icon("eye")}<h2>Today's vendor calls</h2>`, pad: false }, | |
| 240 | ), | |
| 239 | </ul>`} | |
| 240 | </section>`, | |
| 241 | 241 | "Account settings", |
| 242 | 242 | )}`, |
| 243 | 243 | })); |
test/cloud-page-shape.test.ts +25
| @@ -46,3 +46,28 @@ test("an admin sees headed sections and no card; with no account one sentence sa | ||
| 46 | 46 | expect(main).toContain("No cloud account is sealed yet. Add one below"); |
| 47 | 47 | expect(main).toContain('action="/settings/cloud"'); |
| 48 | 48 | }); |
| 49 | ||
| 50 | test("today's vendor calls are a headed block, no card; with none one sentence says so (#9878)", async () => { | |
| 51 | const page = await (await fetch(`${servers.url}/settings/cloud/audit`, { headers: { cookie } })).text(); | |
| 52 | const main = page.slice(page.indexOf("<main"), page.indexOf("</main>")); | |
| 53 | expect(main).toContain('<section class="settings-block">'); | |
| 54 | expect(main).toContain("<h2>Today's vendor calls</h2>"); | |
| 55 | expect(main).not.toContain('class="card'); | |
| 56 | expect(main).not.toContain("card-body"); | |
| 57 | expect(main).toContain("No vendor call today. Rows are kept 90 days."); | |
| 58 | }); | |
| 59 | ||
| 60 | test("a signed-in non-admin gets the forge's one 404 for the vendor calls", async () => { | |
| 61 | const made = await createUser(app.db, { origin: "seed", trusted: true, username: "bob", email: "[email protected]", password: "a-good-passphrase" }); | |
| 62 | if (!made.ok) throw new Error(made.error); | |
| 63 | const r = await fetch(`${servers.url}/signin`, { | |
| 64 | method: "POST", | |
| 65 | redirect: "manual", | |
| 66 | headers: { origin: servers.url }, | |
| 67 | body: new URLSearchParams({ username: "bob", password: "a-good-passphrase" }), | |
| 68 | }); | |
| 69 | const bob = (r.headers.get("set-cookie") ?? "").split(";")[0] ?? ""; | |
| 70 | const res = await fetch(`${servers.url}/settings/cloud/audit`, { headers: { cookie: bob } }); | |
| 71 | expect(res.status).toBe(404); | |
| 72 | expect(await res.text()).not.toContain("vendor call"); | |
| 73 | }); |