Public: anyone can read this; the crew can change it.
Every preview says how fast it got there, in the same words wherever it appears #458
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/75-every-preview-says-how-fast-it-got-there → main 3 commits fork
4 files
changed +176
−32
scratchpad/subs-pages.log deleted −7
| @@ -1,7 +0,0 @@ | ||
| 1 | bun test v1.3.13 (bf2e2cec) | |
| 2 | ||
| 3 | 4 pass | |
| 4 | 9 filtered out | |
| 5 | 0 fail | |
| 6 | 23 expect() calls | |
| 7 | Ran 4 tests across 1 file. [210.00ms] |
src/web/studio/previews.ts +18 −9
| @@ -29,13 +29,13 @@ import { | ||
| 29 | 29 | dormantNote, |
| 30 | 30 | envActions, |
| 31 | 31 | envFacts, |
| 32 | envLine, | |
| 32 | 33 | envPhrase, |
| 33 | 34 | icon, |
| 34 | 35 | listCard, |
| 35 | 36 | previewDataNote, |
| 36 | 37 | row, |
| 37 | 38 | terminalLine, |
| 38 | time, | |
| 39 | 39 | } from "../ui.ts"; |
| 40 | 40 | import { lookNow } from "../pages/looks.ts"; |
| 41 | 41 | import { LOG_TAIL_CAP, logBox, runVoice } from "./log.ts"; |
| @@ -83,10 +83,17 @@ function previewBadge(env: EnvStatus | undefined): Html { | ||
| 83 | 83 | |
| 84 | 84 | /** |
| 85 | 85 | * One open pull's preview, as one row: its host is the click, its state is |
| 86 | * the badge, and a check still running or a check that failed says so right | |
| 87 | * there — the row never hides a preview that is serving. A writer also gets | |
| 88 | * the preview's Redeploy and Stop beside the badge; the row then holds its | |
| 89 | * own controls, so it is no longer one link — the host is. | |
| 86 | * the badge, and its meta line says what the change page's Preview card | |
| 87 | * says, in `envLine`'s words — commit, `live Ns after the push`, `up …`, | |
| 88 | * the deploy run — then any sentence the deployment has of its own. A check | |
| 89 | * still running or a check that failed says so right there; the row never | |
| 90 | * hides a preview that is serving. | |
| 91 | * | |
| 92 | * Once there is an environment its facts carry links of their own, so the | |
| 93 | * row is no longer one link — the host is, and `j`/`k` still land on the | |
| 94 | * row and open it. A writer also gets the preview's Redeploy and Stop | |
| 95 | * beside the badge. The meta line always starts with `#N title`: the live | |
| 96 | * client (`live.ts`) keys a row's announcement on it. | |
| 90 | 97 | */ |
| 91 | 98 | function previewRow( |
| 92 | 99 | base: string, |
| @@ -105,8 +112,7 @@ function previewRow( | ||
| 105 | 112 | ? undefined |
| 106 | 113 | : runtime.urlFor(record, previewEnv(pull.number)).replace(/^https?:\/\//, ""); |
| 107 | 114 | const bits: Html[] = [html`#${pull.number} ${pull.title}`]; |
| 108 | if (env?.commitOid) bits.push(html`${env.commitOid.slice(0, 7)}`); | |
| 109 | if (env?.since) bits.push(html`up ${time(env.since)}`); | |
| 115 | if (env !== undefined) bits.push(...envLine(base, env)); | |
| 110 | 116 | const say = env?.error ?? env?.failedDeploy?.error ?? env?.note; |
| 111 | 117 | if (say) bits.push(html`${say}`); |
| 112 | 118 | const asleep = env === undefined ? null : dormantNote(env); |
| @@ -116,15 +122,18 @@ function previewRow( | ||
| 116 | 122 | if (dataNote !== null) bits.push(dataNote); |
| 117 | 123 | } |
| 118 | 124 | const meta = bits.reduce((a, b) => html`${a} · ${b}`); |
| 119 | if (writer && env !== undefined) | |
| 125 | if (env !== undefined) | |
| 120 | 126 | return row({ |
| 127 | ...(href === undefined ? {} : { nav: true }), | |
| 121 | 128 | lead: icon("rocket"), |
| 122 | 129 | title: |
| 123 | 130 | host === undefined || href === undefined |
| 124 | 131 | ? `#${pull.number} ${pull.title}` |
| 125 | 132 | : html`<a href="${href}" class="mono">${host}</a>`, |
| 126 | 133 | meta, |
| 127 | trail: html`${previewBadge(env)} ${envActions(base, env, writer)}`, | |
| 134 | trail: writer | |
| 135 | ? html`${previewBadge(env)} ${envActions(base, env, writer)}` | |
| 136 | : previewBadge(env), | |
| 128 | 137 | }); |
| 129 | 138 | return row({ |
| 130 | 139 | ...(href === undefined ? {} : { href, nav: true }), |
src/web/ui.ts +42 −16
| @@ -354,6 +354,43 @@ export interface EnvFactsOptions { | ||
| 354 | 354 | readonly url?: boolean; |
| 355 | 355 | } |
| 356 | 356 | |
| 357 | /** | |
| 358 | * An environment's one line of facts, piece by piece: the commit it runs, | |
| 359 | * how fast it came up (`live 3.4s after the push`, only once it serves), | |
| 360 | * how long it has been up, and a link to the deploy that produced it. Each | |
| 361 | * piece is empty when there is nothing to say. `envFacts` sets them in a | |
| 362 | * card; `envLine` strings them into a list row — the same words either way. | |
| 363 | */ | |
| 364 | function envLinePieces(base: string, env: EnvStatus): readonly Html[] { | |
| 365 | return [ | |
| 366 | when(env.commitOid !== "", () => | |
| 367 | sha(env.commitOid, `${base}/commit/${env.commitOid}`), | |
| 368 | ), | |
| 369 | when(servedPhrase(env), (phrase) => html`<span>live ${phrase}</span>`), | |
| 370 | when(env.since, (since) => html`<span>up ${time(since)}</span>`), | |
| 371 | when( | |
| 372 | env.runId, | |
| 373 | (id) => | |
| 374 | html`<a href="${base}/runs/${id}" | |
| 375 | >${env.state === "deploying" | |
| 376 | ? "deploy in progress" | |
| 377 | : env.note !== undefined | |
| 378 | ? "verifying tests" | |
| 379 | : "last deploy"}</a | |
| 380 | >`, | |
| 381 | ), | |
| 382 | ]; | |
| 383 | } | |
| 384 | ||
| 385 | /** | |
| 386 | * `envFacts`' line as the non-empty pieces alone, for a row's meta to join | |
| 387 | * with its own separator: a row is one line, so it takes the facts without | |
| 388 | * the card's block box around them. | |
| 389 | */ | |
| 390 | export function envLine(base: string, env: EnvStatus): Html[] { | |
| 391 | return envLinePieces(base, env).filter((p) => p.text !== ""); | |
| 392 | } | |
| 393 | ||
| 357 | 394 | /** |
| 358 | 395 | * What an environment is doing, as facts: the commit it runs, how fast it |
| 359 | 396 | * came up, how long it has been up, a link to the deploy that produced it, |
| @@ -367,23 +404,12 @@ export function envFacts( | ||
| 367 | 404 | env: EnvStatus, |
| 368 | 405 | o: EnvFactsOptions = {}, |
| 369 | 406 | ): Html { |
| 407 | const [commit, served, up, deploy] = envLinePieces(base, env); | |
| 370 | 408 | return html`<div class="env-facts"> |
| 371 | ${when(env.commitOid !== "", () => | |
| 372 | sha(env.commitOid, `${base}/commit/${env.commitOid}`), | |
| 373 | )} | |
| 374 | ${when(servedPhrase(env), (phrase) => html`<span>live ${phrase}</span>`)} | |
| 375 | ${when(env.since, (since) => html`<span>up ${time(since)}</span>`)} | |
| 376 | ${when( | |
| 377 | env.runId, | |
| 378 | (id) => | |
| 379 | html`<a href="${base}/runs/${id}" | |
| 380 | >${env.state === "deploying" | |
| 381 | ? "deploy in progress" | |
| 382 | : env.note !== undefined | |
| 383 | ? "verifying tests" | |
| 384 | : "last deploy"}</a | |
| 385 | >`, | |
| 386 | )} | |
| 409 | ${commit} | |
| 410 | ${served} | |
| 411 | ${up} | |
| 412 | ${deploy} | |
| 387 | 413 | </div> |
| 388 | 414 | ${when( |
| 389 | 415 | o.url === true, |
test/studio.test.ts +116
| @@ -3095,6 +3095,122 @@ describe("the app's live fragment: /:owner/:name/app/state", () => { | ||
| 3095 | 3095 | expect(serving).toContain(">live<"); |
| 3096 | 3096 | expect(serving).toContain(link); |
| 3097 | 3097 | }); |
| 3098 | test("a preview row says how fast it got there in envFacts' own words, links its deploy run, and keeps its link and sentence while deploying or after its tests failed", async () => { | |
| 3099 | const alice = getUserByName(app.db, "alice")!; | |
| 3100 | const branch = await commitToBranch(app, alice, repo, { | |
| 3101 | branch: "feature/fast", | |
| 3102 | base: repo.defaultBranch, | |
| 3103 | message: "Add F", | |
| 3104 | files: [{ path: "F.md", content: "f\n" }], | |
| 3105 | }); | |
| 3106 | if (!branch.ok) throw new Error(JSON.stringify(branch.error)); | |
| 3107 | const opened = await openPull(app, alice, repo, { | |
| 3108 | title: "Add F", | |
| 3109 | body: "", | |
| 3110 | baseBranch: repo.defaultBranch, | |
| 3111 | headBranch: "feature/fast", | |
| 3112 | }); | |
| 3113 | if (!opened.ok) throw new Error(String(opened.error)); | |
| 3114 | const pull = opened.value; | |
| 3115 | const env = previewEnv(pull.number); | |
| 3116 | const open = `href="/alice/vault/app/${env}/open"`; | |
| 3117 | const label = `#${pull.number} Add F`; | |
| 3118 | const deployRun = createRun(app.db, { | |
| 3119 | repoId: repo.id, | |
| 3120 | kind: "deploy", | |
| 3121 | env, | |
| 3122 | commitOid: pull.headOid, | |
| 3123 | reason: "pull", | |
| 3124 | }); | |
| 3125 | const runLink = `href="/alice/vault/runs/${deployRun.id}"`; | |
| 3126 | // This pull's own row out of the previews panel, so nothing the head | |
| 3127 | // card or another pull's row says can pass for it. | |
| 3128 | const rowOf = (text: string): string => { | |
| 3129 | const panel = text.slice(text.indexOf('id="app-previews"')); | |
| 3130 | const at = panel.indexOf(label); | |
| 3131 | expect(at).toBeGreaterThan(-1); | |
| 3132 | return panel.slice(panel.lastIndexOf("<li>", at), panel.indexOf("</li>", at)); | |
| 3133 | }; | |
| 3134 | ||
| 3135 | claimRun(app.db, deployRun.id); | |
| 3136 | putEnv(env, { | |
| 3137 | deploying: { | |
| 3138 | run: { id: deployRun.id, commitOid: pull.headOid }, | |
| 3139 | abort: new AbortController(), | |
| 3140 | }, | |
| 3141 | }); | |
| 3142 | const deploying = rowOf( | |
| 3143 | await (await get("/alice/vault/app/state", owner)).text(), | |
| 3144 | ); | |
| 3145 | expect(deploying).toContain(open); | |
| 3146 | expect(deploying).toContain(runLink); | |
| 3147 | expect(deploying).toContain("deploy in progress"); | |
| 3148 | expect(deploying).not.toContain("after the push"); | |
| 3149 | expect(deploying).not.toContain("0.0s"); | |
| 3150 | ||
| 3151 | finishRun(app.db, deployRun.id, { state: "ok", summary: "deploy" }); | |
| 3152 | const createdAt = new Date(Date.now() - 60_000).toISOString(); | |
| 3153 | const servingAt = new Date(Date.parse(createdAt) + 3_400).toISOString(); | |
| 3154 | putEnv(env, { | |
| 3155 | live: { | |
| 3156 | commitOid: pull.headOid, | |
| 3157 | handle: { port: 4324 }, | |
| 3158 | runId: deployRun.id, | |
| 3159 | since: servingAt, | |
| 3160 | servingAt, | |
| 3161 | createdAt, | |
| 3162 | deployStartedAt: createdAt, | |
| 3163 | manifest: { tty: null }, | |
| 3164 | }, | |
| 3165 | }); | |
| 3166 | const live = rowOf(await (await get("/alice/vault/app/state", owner)).text()); | |
| 3167 | expect(live).toContain(open); | |
| 3168 | expect(live).toContain("live 3.4s after the push"); | |
| 3169 | expect(live).toContain(pull.headOid.slice(0, 7)); | |
| 3170 | expect(live).toContain(runLink); | |
| 3171 | expect(live).toContain(">live<"); | |
| 3172 | // The live client keys its announcement on the meta line's first words. | |
| 3173 | expect(live).toMatch(new RegExp(`class="row-meta">#${pull.number} Add F ·`)); | |
| 3174 | // The page's first paint draws the same row. | |
| 3175 | expect(await (await get("/alice/vault", owner)).text()).toContain( | |
| 3176 | "live 3.4s after the push", | |
| 3177 | ); | |
| 3178 | ||
| 3179 | putEnv(env, { | |
| 3180 | live: { | |
| 3181 | commitOid: pull.headOid, | |
| 3182 | handle: { port: 4324 }, | |
| 3183 | runId: deployRun.id, | |
| 3184 | since: servingAt, | |
| 3185 | servingAt, | |
| 3186 | createdAt, | |
| 3187 | deployStartedAt: createdAt, | |
| 3188 | testsFailed: "2 failing", | |
| 3189 | manifest: { tty: null }, | |
| 3190 | }, | |
| 3191 | }); | |
| 3192 | const red = rowOf(await (await get("/alice/vault/app/state", owner)).text()); | |
| 3193 | expect(red).toContain(open); | |
| 3194 | expect(red).toContain(runLink); | |
| 3195 | expect(red).toContain("serving · tests failed: 2 failing"); | |
| 3196 | expect(red).toContain("live 3.4s after the push"); | |
| 3197 | ||
| 3198 | putEnv(env, { | |
| 3199 | live: { | |
| 3200 | commitOid: pull.headOid, | |
| 3201 | handle: { port: 4324 }, | |
| 3202 | runId: deployRun.id, | |
| 3203 | since: servingAt, | |
| 3204 | manifest: { tty: null }, | |
| 3205 | }, | |
| 3206 | }); | |
| 3207 | const unmeasured = rowOf( | |
| 3208 | await (await get("/alice/vault/app/state", owner)).text(), | |
| 3209 | ); | |
| 3210 | expect(unmeasured).toContain(open); | |
| 3211 | expect(unmeasured).not.toContain("after the"); | |
| 3212 | expect(unmeasured).not.toContain("0.0s"); | |
| 3213 | }); | |
| 3098 | 3214 | }); |
| 3099 | 3215 | |
| 3100 | 3216 | /** |
new above
offline