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

Merged opened this

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

Merged

mason/forge:mason/75-every-preview-says-how-fast-it-got-there → main 3 commits fork

  • Merged. mason/75-every-preview-says-how-fast-it-got-there was merged into main as d09f2f3.

Review

Sign in to review this change.
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 {
2929 dormantNote,
3030 envActions,
3131 envFacts,
32 envLine,
3233 envPhrase,
3334 icon,
3435 listCard,
3536 previewDataNote,
3637 row,
3738 terminalLine,
38 time,
3939 } from "../ui.ts";
4040 import { lookNow } from "../pages/looks.ts";
4141 import { LOG_TAIL_CAP, logBox, runVoice } from "./log.ts";
@@ -83,10 +83,17 @@ function previewBadge(env: EnvStatus | undefined): Html {
8383
8484 /**
8585 * 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.
9097 */
9198 function previewRow(
9299 base: string,
@@ -105,8 +112,7 @@ function previewRow(
105112 ? undefined
106113 : runtime.urlFor(record, previewEnv(pull.number)).replace(/^https?:\/\//, "");
107114 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));
110116 const say = env?.error ?? env?.failedDeploy?.error ?? env?.note;
111117 if (say) bits.push(html`${say}`);
112118 const asleep = env === undefined ? null : dormantNote(env);
@@ -116,15 +122,18 @@ function previewRow(
116122 if (dataNote !== null) bits.push(dataNote);
117123 }
118124 const meta = bits.reduce((a, b) => html`${a} · ${b}`);
119 if (writer && env !== undefined)
125 if (env !== undefined)
120126 return row({
127 ...(href === undefined ? {} : { nav: true }),
121128 lead: icon("rocket"),
122129 title:
123130 host === undefined || href === undefined
124131 ? `#${pull.number} ${pull.title}`
125132 : html`<a href="${href}" class="mono">${host}</a>`,
126133 meta,
127 trail: html`${previewBadge(env)} ${envActions(base, env, writer)}`,
134 trail: writer
135 ? html`${previewBadge(env)} ${envActions(base, env, writer)}`
136 : previewBadge(env),
128137 });
129138 return row({
130139 ...(href === undefined ? {} : { href, nav: true }),
src/web/ui.ts +42 −16
@@ -354,6 +354,43 @@ export interface EnvFactsOptions {
354354 readonly url?: boolean;
355355 }
356356
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
357394 /**
358395 * What an environment is doing, as facts: the commit it runs, how fast it
359396 * came up, how long it has been up, a link to the deploy that produced it,
@@ -367,23 +404,12 @@ export function envFacts(
367404 env: EnvStatus,
368405 o: EnvFactsOptions = {},
369406 ): Html {
407 const [commit, served, up, deploy] = envLinePieces(base, env);
370408 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}
387413 </div>
388414 ${when(
389415 o.url === true,
test/studio.test.ts +116
@@ -3095,6 +3095,122 @@ describe("the app's live fragment: /:owner/:name/app/state", () => {
30953095 expect(serving).toContain(">live<");
30963096 expect(serving).toContain(link);
30973097 });
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 });
30983214 });
30993215
31003216 /**

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs