@storyshelf/app 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/app-types.d.mts +54 -0
- package/dist/asset-manifest.mjs +41 -0
- package/dist/asset-manifest.mjs.map +1 -0
- package/dist/asset-paths.mjs +23 -0
- package/dist/asset-paths.mjs.map +1 -0
- package/dist/assets/htmx.min.js +3304 -0
- package/dist/assets/mark.svg +1 -0
- package/dist/capture-setup.mjs +139 -0
- package/dist/capture-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +20 -0
- package/dist/credential-keys.mjs.map +1 -0
- package/dist/credentials.mjs +56 -0
- package/dist/credentials.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +310 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lifecycle.mjs +132 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +75 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +68 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/middleware/request-log.mjs +30 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +39 -0
- package/dist/middleware/server-timing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs +37 -0
- package/dist/middleware/setup-gate.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +33 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/notify.d.mts +17 -0
- package/dist/notify.mjs +141 -0
- package/dist/notify.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +243 -0
- package/dist/pages/admin-system.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +392 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff-actions.mjs +162 -0
- package/dist/pages/build-diff-actions.mjs.map +1 -0
- package/dist/pages/build-diff-comments.mjs +104 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +117 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +48 -0
- package/dist/pages/build-diff-model.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +122 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +78 -0
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
- package/dist/pages/build-diff-stale.mjs +70 -0
- package/dist/pages/build-diff-stale.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +70 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +110 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +165 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/invite.mjs +91 -0
- package/dist/pages/invite.mjs.map +1 -0
- package/dist/pages/label-detail.mjs +154 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/library.mjs +343 -0
- package/dist/pages/library.mjs.map +1 -0
- package/dist/pages/login.mjs +117 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +97 -0
- package/dist/pages/passkey-ceremony.mjs.map +1 -0
- package/dist/pages/profile.mjs +364 -0
- package/dist/pages/profile.mjs.map +1 -0
- package/dist/pages/project-builds-table.mjs +151 -0
- package/dist/pages/project-builds-table.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +143 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +90 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +95 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects-card.mjs +194 -0
- package/dist/pages/projects-card.mjs.map +1 -0
- package/dist/pages/projects.mjs +129 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +98 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +138 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +107 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +192 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +240 -0
- package/dist/pages/settings-notifications.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +137 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tests.mjs +111 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +90 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +104 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/pages/storybook.mjs +116 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/retention-timer.mjs +65 -0
- package/dist/retention-timer.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +48 -0
- package/dist/routers/admin-pages.mjs.map +1 -0
- package/dist/routers/admin.mjs +113 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +64 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/attempts.mjs +96 -0
- package/dist/routers/attempts.mjs.map +1 -0
- package/dist/routers/auth-sync.mjs +64 -0
- package/dist/routers/auth-sync.mjs.map +1 -0
- package/dist/routers/auth.mjs +236 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/baseline-guard.mjs +83 -0
- package/dist/routers/baseline-guard.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +217 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +381 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +109 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/flash.mjs +35 -0
- package/dist/routers/flash.mjs.map +1 -0
- package/dist/routers/health-report.mjs +159 -0
- package/dist/routers/health-report.mjs.map +1 -0
- package/dist/routers/health.mjs +46 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +110 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +24 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +118 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +53 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +177 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/notification-channels.mjs +190 -0
- package/dist/routers/notification-channels.mjs.map +1 -0
- package/dist/routers/notification-subscriptions.mjs +120 -0
- package/dist/routers/notification-subscriptions.mjs.map +1 -0
- package/dist/routers/profile.mjs +276 -0
- package/dist/routers/profile.mjs.map +1 -0
- package/dist/routers/projects.mjs +133 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/review-next.mjs +47 -0
- package/dist/routers/review-next.mjs.map +1 -0
- package/dist/routers/schemas.mjs +337 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +104 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +52 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +58 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-notifications.mjs +108 -0
- package/dist/routers/settings-notifications.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +77 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +67 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +149 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +25 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +240 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +108 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/storybook.mjs +171 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/tokens.mjs +91 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +166 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +97 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/routers/well-known.mjs +43 -0
- package/dist/routers/well-known.mjs.map +1 -0
- package/dist/runtime.mjs +52 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store.mjs +20 -0
- package/dist/store.mjs.map +1 -0
- package/dist/ui/avatar.mjs +76 -0
- package/dist/ui/avatar.mjs.map +1 -0
- package/dist/ui/buttons.mjs +205 -0
- package/dist/ui/buttons.mjs.map +1 -0
- package/dist/ui/code-block.mjs +47 -0
- package/dist/ui/code-block.mjs.map +1 -0
- package/dist/ui/compare-stage.mjs +344 -0
- package/dist/ui/compare-stage.mjs.map +1 -0
- package/dist/ui/csrf-field.mjs +20 -0
- package/dist/ui/csrf-field.mjs.map +1 -0
- package/dist/ui/css.mjs +36 -0
- package/dist/ui/css.mjs.map +1 -0
- package/dist/ui/document.mjs +130 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/dropdown.mjs +181 -0
- package/dist/ui/dropdown.mjs.map +1 -0
- package/dist/ui/feedback.mjs +341 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/filter-input.mjs +58 -0
- package/dist/ui/filter-input.mjs.map +1 -0
- package/dist/ui/forms.mjs +291 -0
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/icons/icon.mjs +53 -0
- package/dist/ui/icons/icon.mjs.map +1 -0
- package/dist/ui/icons/paths.mjs +61 -0
- package/dist/ui/icons/paths.mjs.map +1 -0
- package/dist/ui/icons/sprite.mjs +26 -0
- package/dist/ui/icons/sprite.mjs.map +1 -0
- package/dist/ui/kbd.mjs +33 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/layout.mjs +173 -0
- package/dist/ui/layout.mjs.map +1 -0
- package/dist/ui/progress.mjs +44 -0
- package/dist/ui/progress.mjs.map +1 -0
- package/dist/ui/segmented.mjs +77 -0
- package/dist/ui/segmented.mjs.map +1 -0
- package/dist/ui/shell/account.mjs +161 -0
- package/dist/ui/shell/account.mjs.map +1 -0
- package/dist/ui/shell/bare.mjs +69 -0
- package/dist/ui/shell/bare.mjs.map +1 -0
- package/dist/ui/shell/boot-script.mjs +46 -0
- package/dist/ui/shell/boot-script.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +259 -0
- package/dist/ui/shell/client-script.mjs.map +1 -0
- package/dist/ui/shell/nav.mjs +111 -0
- package/dist/ui/shell/nav.mjs.map +1 -0
- package/dist/ui/shell/review-script.mjs +195 -0
- package/dist/ui/shell/review-script.mjs.map +1 -0
- package/dist/ui/shell/sidebar.mjs +323 -0
- package/dist/ui/shell/sidebar.mjs.map +1 -0
- package/dist/ui/shell/topbar.mjs +268 -0
- package/dist/ui/shell/topbar.mjs.map +1 -0
- package/dist/ui/stacks.mjs +82 -0
- package/dist/ui/stacks.mjs.map +1 -0
- package/dist/ui/styles/base.mjs +41 -0
- package/dist/ui/styles/base.mjs.map +1 -0
- package/dist/ui/styles/behavior.mjs +43 -0
- package/dist/ui/styles/behavior.mjs.map +1 -0
- package/dist/ui/styles/review-layout.mjs +347 -0
- package/dist/ui/styles/review-layout.mjs.map +1 -0
- package/dist/ui/styles/review.mjs +86 -0
- package/dist/ui/styles/review.mjs.map +1 -0
- package/dist/ui/styles/tokens.mjs +132 -0
- package/dist/ui/styles/tokens.mjs.map +1 -0
- package/dist/ui/styles.mjs +19 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/subnav.mjs +91 -0
- package/dist/ui/subnav.mjs.map +1 -0
- package/dist/ui/table.mjs +73 -0
- package/dist/ui/table.mjs.map +1 -0
- package/dist/ui/theme.mjs +64 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/ui/thumbnail.mjs +51 -0
- package/dist/ui/thumbnail.mjs.map +1 -0
- package/dist/ui/time.mjs +50 -0
- package/dist/ui/time.mjs.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Badge, statusTone } from "../ui/feedback.mjs";
|
|
5
|
+
import { VStack } from "../ui/stacks.mjs";
|
|
6
|
+
import { CompareStage } from "../ui/compare-stage.mjs";
|
|
7
|
+
import { StaleSnapshotNotice } from "./build-diff-stale.mjs";
|
|
8
|
+
import { viewerHead, viewerMeta, viewerTitle } from "../ui/styles/review-layout.mjs";
|
|
9
|
+
import { diffPercent, snapshotImageUrl } from "./build-diff-model.mjs";
|
|
10
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
//#region src/pages/build-diff-viewer.tsx
|
|
12
|
+
/** Why a snapshot has no diff image, phrased for the empty pane. */
|
|
13
|
+
function diffEmptyText(status) {
|
|
14
|
+
return status === "unchanged" || status === "approved" ? "No diff — within threshold" : "No diff yet";
|
|
15
|
+
}
|
|
16
|
+
/** Story heading plus the comparison stage for the selected snapshot. */
|
|
17
|
+
function DiffViewer(props) {
|
|
18
|
+
const { project, build, selected, hasBaseline, drifted } = props;
|
|
19
|
+
const percent = diffPercent(selected);
|
|
20
|
+
return /* @__PURE__ */ jsxs(VStack, { children: [
|
|
21
|
+
/* @__PURE__ */ jsxs("div", {
|
|
22
|
+
class: viewerHead,
|
|
23
|
+
children: [/* @__PURE__ */ jsxs("h2", {
|
|
24
|
+
class: viewerTitle,
|
|
25
|
+
children: [
|
|
26
|
+
selected.storyTitle,
|
|
27
|
+
" / ",
|
|
28
|
+
selected.storyName
|
|
29
|
+
]
|
|
30
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
31
|
+
class: viewerMeta,
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
34
|
+
tone: statusTone(selected.status),
|
|
35
|
+
icon: true,
|
|
36
|
+
children: selected.status
|
|
37
|
+
}),
|
|
38
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
39
|
+
selected.viewportName,
|
|
40
|
+
" · ",
|
|
41
|
+
selected.viewportWidth,
|
|
42
|
+
"×",
|
|
43
|
+
selected.viewportHeight
|
|
44
|
+
] }),
|
|
45
|
+
selected.diffPixels === null || selected.diffPixels === void 0 ? null : /* @__PURE__ */ jsxs("span", {
|
|
46
|
+
class: "mono",
|
|
47
|
+
children: [selected.diffPixels, " px"]
|
|
48
|
+
}),
|
|
49
|
+
percent ? /* @__PURE__ */ jsx("span", {
|
|
50
|
+
class: "mono",
|
|
51
|
+
children: percent
|
|
52
|
+
}) : null
|
|
53
|
+
]
|
|
54
|
+
})]
|
|
55
|
+
}),
|
|
56
|
+
drifted ? /* @__PURE__ */ jsx(StaleSnapshotNotice, { status: drifted }) : null,
|
|
57
|
+
/* @__PURE__ */ jsx(CompareStage, {
|
|
58
|
+
baselineSrc: hasBaseline[selected.id] ? snapshotImageUrl(project, build, selected, "baseline") : null,
|
|
59
|
+
currentSrc: snapshotImageUrl(project, build, selected, "image"),
|
|
60
|
+
diffSrc: selected.diffPath ? snapshotImageUrl(project, build, selected, "diff") : null,
|
|
61
|
+
subject: `${selected.storyTitle} / ${selected.storyName}`,
|
|
62
|
+
diffEmpty: diffEmptyText(selected.status),
|
|
63
|
+
diffMeta: percent ?? void 0
|
|
64
|
+
})
|
|
65
|
+
] });
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { DiffViewer };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=build-diff-viewer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff-viewer.mjs","names":[],"sources":["../../src/pages/build-diff-viewer.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, CompareStage, VStack, statusTone } from \"../ui/components.tsx\";\nimport { viewerHead, viewerMeta, viewerTitle } from \"../ui/styles/review-layout.ts\";\nimport { diffPercent, snapshotImageUrl } from \"./build-diff-model.ts\";\nimport { StaleSnapshotNotice } from \"./build-diff-stale.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Viewer props for the selected snapshot. */\nexport interface DiffViewerProps {\n project: Project;\n build: Build;\n selected: Snapshot;\n hasBaseline: Record<string, boolean>;\n /** Set when the baseline changed after this snapshot's diff was computed. */\n drifted?: \"stale\" | \"removed\";\n}\n\n/** Why a snapshot has no diff image, phrased for the empty pane. */\nfunction diffEmptyText(status: string): string {\n return status === \"unchanged\" || status === \"approved\"\n ? \"No diff — within threshold\"\n : \"No diff yet\";\n}\n\n/** Story heading plus the comparison stage for the selected snapshot. */\nexport function DiffViewer(props: DiffViewerProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, hasBaseline, drifted } = props;\n const percent = diffPercent(selected);\n return (\n <VStack>\n <div class={viewerHead}>\n <h2 class={viewerTitle}>\n {selected.storyTitle} / {selected.storyName}\n </h2>\n <p class={viewerMeta}>\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n <span>\n {selected.viewportName} · {selected.viewportWidth}×{selected.viewportHeight}\n </span>\n {selected.diffPixels === null || selected.diffPixels === undefined ? null : (\n <span class=\"mono\">{selected.diffPixels} px</span>\n )}\n {percent ? <span class=\"mono\">{percent}</span> : null}\n </p>\n </div>\n {drifted ? <StaleSnapshotNotice status={drifted} /> : null}\n <CompareStage\n baselineSrc={\n hasBaseline[selected.id] ? snapshotImageUrl(project, build, selected, \"baseline\") : null\n }\n currentSrc={snapshotImageUrl(project, build, selected, \"image\")}\n diffSrc={selected.diffPath ? snapshotImageUrl(project, build, selected, \"diff\") : null}\n subject={`${selected.storyTitle} / ${selected.storyName}`}\n diffEmpty={diffEmptyText(selected.status)}\n diffMeta={percent ?? undefined}\n />\n </VStack>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAS,cAAc,QAAwB;CAC7C,OAAO,WAAW,eAAe,WAAW,aACxC,+BACA;AACN;;AAGA,SAAgB,WAAW,OAAwE;CACjG,MAAM,EAAE,SAAS,OAAO,UAAU,aAAa,YAAY;CAC3D,MAAM,UAAU,YAAY,QAAQ;CACpC,OACE,qBAAC,QAAD,EAAA,UAAA;EACE,qBAAC,OAAD;GAAK,OAAO;GAAZ,UAAA,CACE,qBAAC,MAAD;IAAI,OAAO;IAAX,UAAA;KACG,SAAS;KAAW;KAAI,SAAS;IAChC;GACJ,CAAA,GAAA,qBAAC,KAAD;IAAG,OAAO;IAAV,UAAA;KACE,oBAAC,OAAD;MAAO,MAAM,WAAW,SAAS,MAAM;MAAG,MAAA;MACvC,UAAA,SAAS;KACL,CAAA;KACP,qBAAC,QAAD,EAAA,UAAA;MACG,SAAS;MAAa;MAAI,SAAS;MAAc;MAAE,SAAS;KACzD,EAAA,CAAA;KACL,SAAS,eAAe,QAAQ,SAAS,eAAe,KAAA,IAAY,OACnE,qBAAC,QAAD;MAAM,OAAM;MAAZ,UAAA,CAAoB,SAAS,YAAW,KAAS;;KAElD,UAAU,oBAAC,QAAD;MAAM,OAAM;MAAQ,UAAA;KAAc,CAAA,IAAI;IAChD;GACA,CAAA,CAAA;;EACJ,UAAU,oBAAC,qBAAD,EAAqB,QAAQ,QAAU,CAAA,IAAI;EACtD,oBAAC,cAAD;GACE,aACE,YAAY,SAAS,MAAM,iBAAiB,SAAS,OAAO,UAAU,UAAU,IAAI;GAEtF,YAAY,iBAAiB,SAAS,OAAO,UAAU,OAAO;GAC9D,SAAS,SAAS,WAAW,iBAAiB,SAAS,OAAO,UAAU,MAAM,IAAI;GAClF,SAAS,GAAG,SAAS,WAAW,KAAK,SAAS;GAC9C,WAAW,cAAc,SAAS,MAAM;GACxC,UAAU,WAAW,KAAA;EACtB,CAAA;CACK,EAAA,CAAA;AAEZ"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { EmptyState } from "../ui/feedback.mjs";
|
|
5
|
+
import { DocumentLayout } from "../ui/document.mjs";
|
|
6
|
+
import { StaleBaselineNotice } from "./build-diff-stale.mjs";
|
|
7
|
+
import { canvasColumn, workspace } from "../ui/styles/review-layout.mjs";
|
|
8
|
+
import { DiffActionBar } from "./build-diff-actions.mjs";
|
|
9
|
+
import { DiffComments } from "./build-diff-comments.mjs";
|
|
10
|
+
import { DiffHeader } from "./build-diff-header.mjs";
|
|
11
|
+
import { DiffNav } from "./build-diff-nav.mjs";
|
|
12
|
+
import { ShortcutsDialog } from "./build-diff-shortcuts.mjs";
|
|
13
|
+
import { DiffViewer } from "./build-diff-viewer.mjs";
|
|
14
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
15
|
+
//#region src/pages/build-diff.tsx
|
|
16
|
+
/** Snapshot nav plus the viewer/comments column for the selected snapshot. */
|
|
17
|
+
function DiffReviewGrid(props) {
|
|
18
|
+
const { project, build, snapshots, comments, selected, canReview, hasBaseline, drifted } = props;
|
|
19
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
20
|
+
class: workspace,
|
|
21
|
+
children: [/* @__PURE__ */ jsx(DiffNav, {
|
|
22
|
+
project,
|
|
23
|
+
build,
|
|
24
|
+
snapshots,
|
|
25
|
+
selectedId: selected?.id
|
|
26
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
27
|
+
class: canvasColumn,
|
|
28
|
+
children: selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
|
+
/* @__PURE__ */ jsx(DiffViewer, {
|
|
30
|
+
project,
|
|
31
|
+
build,
|
|
32
|
+
selected,
|
|
33
|
+
hasBaseline,
|
|
34
|
+
drifted: drifted[selected.id]
|
|
35
|
+
}),
|
|
36
|
+
/* @__PURE__ */ jsx(DiffComments, {
|
|
37
|
+
project,
|
|
38
|
+
build,
|
|
39
|
+
selectedId: selected.id,
|
|
40
|
+
comments,
|
|
41
|
+
canReview
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ jsx(DiffActionBar, {
|
|
44
|
+
project,
|
|
45
|
+
build,
|
|
46
|
+
snapshots,
|
|
47
|
+
selected,
|
|
48
|
+
canReview,
|
|
49
|
+
drifted: drifted[selected.id] !== void 0
|
|
50
|
+
})
|
|
51
|
+
] }) : /* @__PURE__ */ jsx(EmptyState, { description: "Select a snapshot to review." })
|
|
52
|
+
})]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Sidebar/breadcrumb context for the review page: Builds / <build> / Review. */
|
|
56
|
+
function reviewNav(project, build) {
|
|
57
|
+
return {
|
|
58
|
+
active: "builds",
|
|
59
|
+
projectSlug: project.slug,
|
|
60
|
+
projectName: project.name,
|
|
61
|
+
trail: [{
|
|
62
|
+
label: `${build.gitBranch} · ${build.gitSha.slice(0, 7)}`,
|
|
63
|
+
href: `/projects/${project.slug}/builds/${build.id}`
|
|
64
|
+
}, { label: "Review" }]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Three-up diff review page: baseline, current, and diff with keyboard review. */
|
|
68
|
+
function renderBuildDiffPage(data) {
|
|
69
|
+
const { project, build, snapshots, comments, selectedId, canReview, hasBaseline, drifted } = data;
|
|
70
|
+
const selected = snapshots.find((s) => s.id === selectedId) ?? snapshots.find((s) => s.status === "changed" || s.status === "new") ?? snapshots[0];
|
|
71
|
+
const pending = snapshots.filter((s) => s.status === "new" || s.status === "changed");
|
|
72
|
+
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
73
|
+
title: `Review · ${build.gitBranch}`,
|
|
74
|
+
nav: reviewNav(project, build),
|
|
75
|
+
layout: "wide",
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ jsx(DiffHeader, {
|
|
78
|
+
project,
|
|
79
|
+
build,
|
|
80
|
+
snapshots,
|
|
81
|
+
pendingCount: pending.length,
|
|
82
|
+
canReview
|
|
83
|
+
}),
|
|
84
|
+
Object.keys(drifted).length > 0 ? /* @__PURE__ */ jsx(StaleBaselineNotice, {
|
|
85
|
+
project,
|
|
86
|
+
build,
|
|
87
|
+
staleCount: Object.keys(drifted).length,
|
|
88
|
+
canReview
|
|
89
|
+
}) : null,
|
|
90
|
+
snapshots.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
91
|
+
title: "No snapshots",
|
|
92
|
+
description: "This build has no snapshots yet. Capture may still be running."
|
|
93
|
+
}) : /* @__PURE__ */ jsx(DiffReviewGrid, {
|
|
94
|
+
project,
|
|
95
|
+
build,
|
|
96
|
+
snapshots,
|
|
97
|
+
comments,
|
|
98
|
+
selected,
|
|
99
|
+
canReview,
|
|
100
|
+
hasBaseline,
|
|
101
|
+
drifted
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ jsx(ShortcutsDialog, {})
|
|
104
|
+
]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { renderBuildDiffPage };
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=build-diff.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff.mjs","names":[],"sources":["../../src/pages/build-diff.tsx"],"sourcesContent":["import type { Build } from \"@storyshelf/core/schema\";\nimport type { Comment } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { EmptyState } from \"../ui/components.tsx\";\nimport { DocumentLayout, type NavConfig, type RenderedContent } from \"../ui/document.tsx\";\nimport { canvasColumn, workspace } from \"../ui/styles/review-layout.ts\";\nimport { DiffActionBar } from \"./build-diff-actions.tsx\";\nimport { DiffComments } from \"./build-diff-comments.tsx\";\nimport { DiffHeader } from \"./build-diff-header.tsx\";\nimport { DiffNav } from \"./build-diff-nav.tsx\";\nimport { ShortcutsDialog } from \"./build-diff-shortcuts.tsx\";\nimport { StaleBaselineNotice } from \"./build-diff-stale.tsx\";\nimport { DiffViewer } from \"./build-diff-viewer.tsx\";\n\n/** Data required to render the three-up build diff review page. */\nexport interface BuildDiffData {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n comments: Comment[];\n selectedId?: string;\n canReview: boolean;\n hasBaseline: Record<string, boolean>;\n /** Open snapshots whose baseline changed after their diff was computed. */\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ninterface DiffReviewGridProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n comments: Comment[];\n selected?: Snapshot;\n canReview: boolean;\n hasBaseline: Record<string, boolean>;\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/** Snapshot nav plus the viewer/comments column for the selected snapshot. */\nfunction DiffReviewGrid(\n props: DiffReviewGridProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, comments, selected, canReview, hasBaseline, drifted } = props;\n return (\n <div class={workspace}>\n <DiffNav project={project} build={build} snapshots={snapshots} selectedId={selected?.id} />\n <div class={canvasColumn}>\n {selected ? (\n <>\n <DiffViewer\n project={project}\n build={build}\n selected={selected}\n hasBaseline={hasBaseline}\n drifted={drifted[selected.id]}\n />\n <DiffComments\n project={project}\n build={build}\n selectedId={selected.id}\n comments={comments}\n canReview={canReview}\n />\n <DiffActionBar\n project={project}\n build={build}\n snapshots={snapshots}\n selected={selected}\n canReview={canReview}\n drifted={drifted[selected.id] !== undefined}\n />\n </>\n ) : (\n <EmptyState description=\"Select a snapshot to review.\" />\n )}\n </div>\n </div>\n );\n}\n\n/** Sidebar/breadcrumb context for the review page: Builds / <build> / Review. */\nfunction reviewNav(project: Project, build: Build): NavConfig {\n return {\n active: \"builds\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: [\n {\n label: `${build.gitBranch} · ${build.gitSha.slice(0, 7)}`,\n href: `/projects/${project.slug}/builds/${build.id}`,\n },\n { label: \"Review\" },\n ],\n };\n}\n\n/** Three-up diff review page: baseline, current, and diff with keyboard review. */\nexport function renderBuildDiffPage(data: BuildDiffData): RenderedContent {\n const { project, build, snapshots, comments, selectedId, canReview, hasBaseline, drifted } = data;\n const selected =\n snapshots.find((s) => s.id === selectedId) ??\n snapshots.find((s) => s.status === \"changed\" || s.status === \"new\") ??\n snapshots[0];\n const pending = snapshots.filter((s) => s.status === \"new\" || s.status === \"changed\");\n\n return (\n <DocumentLayout\n title={`Review · ${build.gitBranch}`}\n nav={reviewNav(project, build)}\n layout=\"wide\"\n >\n <DiffHeader\n project={project}\n build={build}\n snapshots={snapshots}\n pendingCount={pending.length}\n canReview={canReview}\n />\n\n {Object.keys(drifted).length > 0 ? (\n <StaleBaselineNotice\n project={project}\n build={build}\n staleCount={Object.keys(drifted).length}\n canReview={canReview}\n />\n ) : null}\n\n {snapshots.length === 0 ? (\n <EmptyState\n title=\"No snapshots\"\n description=\"This build has no snapshots yet. Capture may still be running.\"\n />\n ) : (\n <DiffReviewGrid\n project={project}\n build={build}\n snapshots={snapshots}\n comments={comments}\n selected={selected}\n canReview={canReview}\n hasBaseline={hasBaseline}\n drifted={drifted}\n />\n )}\n <ShortcutsDialog />\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA2CA,SAAS,eACP,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,WAAW,UAAU,UAAU,WAAW,aAAa,YAAY;CAC3F,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,oBAAC,SAAD;GAAkB;GAAgB;GAAkB;GAAW,YAAY,UAAU;EAAK,CAAA,GAC1F,oBAAC,OAAD;GAAK,OAAO;GACT,UAAA,WACC,qBAAA,UAAA,EAAA,UAAA;IACE,oBAAC,YAAD;KACW;KACF;KACG;KACG;KACb,SAAS,QAAQ,SAAS;IAC3B,CAAA;IACD,oBAAC,cAAD;KACW;KACF;KACP,YAAY,SAAS;KACX;KACC;IACZ,CAAA;IACD,oBAAC,eAAD;KACW;KACF;KACI;KACD;KACC;KACX,SAAS,QAAQ,SAAS,QAAQ,KAAA;IACnC,CAAA;GACD,EAAA,CAAA,IAEF,oBAAC,YAAD,EAAY,aAAY,+BAAgC,CAAA;EAEvD,CAAA,CACF;;AAET;;AAGA,SAAS,UAAU,SAAkB,OAAyB;CAC5D,OAAO;EACL,QAAQ;EACR,aAAa,QAAQ;EACrB,aAAa,QAAQ;EACrB,OAAO,CACL;GACE,OAAO,GAAG,MAAM,UAAU,KAAK,MAAM,OAAO,MAAM,GAAG,CAAC;GACtD,MAAM,aAAa,QAAQ,KAAK,UAAU,MAAM;EAClD,GACA,EAAE,OAAO,SAAS,CACpB;CACF;AACF;;AAGA,SAAgB,oBAAoB,MAAsC;CACxE,MAAM,EAAE,SAAS,OAAO,WAAW,UAAU,YAAY,WAAW,aAAa,YAAY;CAC7F,MAAM,WACJ,UAAU,MAAM,MAAM,EAAE,OAAO,UAAU,KACzC,UAAU,MAAM,MAAM,EAAE,WAAW,aAAa,EAAE,WAAW,KAAK,KAClE,UAAU;CACZ,MAAM,UAAU,UAAU,QAAQ,MAAM,EAAE,WAAW,SAAS,EAAE,WAAW,SAAS;CAEpF,OACE,qBAAC,gBAAD;EACE,OAAO,YAAY,MAAM;EACzB,KAAK,UAAU,SAAS,KAAK;EAC7B,QAAO;EAHT,UAAA;GAKE,oBAAC,YAAD;IACW;IACF;IACI;IACX,cAAc,QAAQ;IACX;GACZ,CAAA;GAEA,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,IAC7B,oBAAC,qBAAD;IACW;IACF;IACP,YAAY,OAAO,KAAK,OAAO,CAAC,CAAC;IACtB;GACZ,CAAA,IACC;GAEH,UAAU,WAAW,IACpB,oBAAC,YAAD;IACE,OAAM;IACN,aAAY;GACb,CAAA,IAED,oBAAC,gBAAD;IACW;IACF;IACI;IACD;IACA;IACC;IACE;IACJ;GACV,CAAA;GAEH,oBAAC,iBAAD,CAAkB,CAAA;EACJ;;AAEpB"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { Button } from "../ui/buttons.mjs";
|
|
6
|
+
import { Alert, Badge, EmptyState, Meta, statusTone } from "../ui/feedback.mjs";
|
|
7
|
+
import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
+
import { HStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { Table } from "../ui/table.mjs";
|
|
10
|
+
import { DocumentLayout } from "../ui/document.mjs";
|
|
11
|
+
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
12
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
13
|
+
//#region src/pages/compute-jobs.tsx
|
|
14
|
+
/** Live-refreshing partial showing the currently queued and running captures. */
|
|
15
|
+
function renderActiveQueue(slug, queueView) {
|
|
16
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
17
|
+
id: "active-queue",
|
|
18
|
+
"hx-get": `/projects/${slug}/jobs?partial=queue`,
|
|
19
|
+
"hx-trigger": "every 5s",
|
|
20
|
+
"hx-swap": "outerHTML",
|
|
21
|
+
"hx-target": "#active-queue",
|
|
22
|
+
children: [/* @__PURE__ */ jsx(SectionTitle, { children: "Active queue" }), queueView.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No captures are currently queued or running." }) : /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
23
|
+
/* @__PURE__ */ jsx("th", { children: "Build" }),
|
|
24
|
+
/* @__PURE__ */ jsx("th", { children: "Status" }),
|
|
25
|
+
/* @__PURE__ */ jsx("th", { children: "Queued" }),
|
|
26
|
+
/* @__PURE__ */ jsx("th", { children: "Started" })
|
|
27
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: queueView.map((job) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
28
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("a", {
|
|
29
|
+
href: `/projects/${slug}/builds/${job.buildId}`,
|
|
30
|
+
children: job.buildId.slice(0, 8)
|
|
31
|
+
}) }),
|
|
32
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
33
|
+
tone: statusTone(job.status),
|
|
34
|
+
children: job.status
|
|
35
|
+
}) }),
|
|
36
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
37
|
+
as: "span",
|
|
38
|
+
children: new Date(job.queuedAt).toLocaleTimeString()
|
|
39
|
+
}) }),
|
|
40
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
41
|
+
as: "span",
|
|
42
|
+
children: job.startedAt ? new Date(job.startedAt).toLocaleTimeString() : "—"
|
|
43
|
+
}) })
|
|
44
|
+
] }, job.buildId)) })] }) })]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A `capturing` build the in-process queue no longer tracks was cut short by a
|
|
49
|
+
* restart. Remote queues are invisible to the server, so nothing is flagged there.
|
|
50
|
+
*/
|
|
51
|
+
function isStuckCapture(status, tracked, inProcessQueue) {
|
|
52
|
+
return inProcessQueue && status === "capturing" && !tracked;
|
|
53
|
+
}
|
|
54
|
+
/** Compute jobs page: live capture queue plus recent build history with retry. */
|
|
55
|
+
async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue = false) {
|
|
56
|
+
const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
|
|
57
|
+
if (!project) return null;
|
|
58
|
+
const recentBuilds = (await new BuildModel(getStore().db).list(project.id)).slice(0, 20);
|
|
59
|
+
const queueByBuild = new Map(queueView.map((job) => [job.buildId, job]));
|
|
60
|
+
const stuck = (id, status) => isStuckCapture(status, queueByBuild.has(id), inProcessQueue);
|
|
61
|
+
const stuckCount = recentBuilds.filter((build) => stuck(build.id, build.status)).length;
|
|
62
|
+
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
63
|
+
title: `${project.name} · Compute jobs`,
|
|
64
|
+
nav: {
|
|
65
|
+
active: "jobs",
|
|
66
|
+
projectSlug: project.slug,
|
|
67
|
+
projectName: project.name
|
|
68
|
+
},
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsx(PageHeader, {
|
|
71
|
+
title: "Compute jobs",
|
|
72
|
+
description: "Capture jobs run on this server. Queued and running jobs refresh live; recent history is below.",
|
|
73
|
+
actions: /* @__PURE__ */ jsx(Button, {
|
|
74
|
+
variant: "secondary",
|
|
75
|
+
href: `/projects/${project.slug}/builds`,
|
|
76
|
+
children: "Back to builds"
|
|
77
|
+
})
|
|
78
|
+
}),
|
|
79
|
+
stuckCount > 0 ? /* @__PURE__ */ jsxs(Alert, {
|
|
80
|
+
tone: "warning",
|
|
81
|
+
title: "Interrupted captures",
|
|
82
|
+
children: [
|
|
83
|
+
stuckCount,
|
|
84
|
+
" build",
|
|
85
|
+
stuckCount === 1 ? " was" : "s were",
|
|
86
|
+
" still capturing when the server restarted. On startup an interrupted build is requeued once; if it is interrupted again it is marked failed so a crash loop cannot repeat captures. Retry a build to run it again."
|
|
87
|
+
]
|
|
88
|
+
}) : null,
|
|
89
|
+
renderActiveQueue(project.slug, queueView),
|
|
90
|
+
/* @__PURE__ */ jsxs(Card, { children: [
|
|
91
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Recent builds" }),
|
|
92
|
+
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
93
|
+
"Capture history for ",
|
|
94
|
+
project.name,
|
|
95
|
+
". Failed and interrupted jobs can be retried."
|
|
96
|
+
] }),
|
|
97
|
+
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
98
|
+
/* @__PURE__ */ jsx("th", { children: "Branch" }),
|
|
99
|
+
/* @__PURE__ */ jsx("th", { children: "Status" }),
|
|
100
|
+
/* @__PURE__ */ jsx("th", { children: "Snapshots" }),
|
|
101
|
+
/* @__PURE__ */ jsx("th", { children: "Created" }),
|
|
102
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
103
|
+
class: "visually-hidden",
|
|
104
|
+
children: "Actions"
|
|
105
|
+
}) })
|
|
106
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: recentBuilds.map((build) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
107
|
+
/* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("strong", { children: build.gitBranch }), /* @__PURE__ */ jsx(Meta, {
|
|
108
|
+
as: "div",
|
|
109
|
+
mono: true,
|
|
110
|
+
children: build.gitSha.slice(0, 7)
|
|
111
|
+
})] }),
|
|
112
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
113
|
+
tone: stuck(build.id, build.status) ? "warning" : statusTone(build.status),
|
|
114
|
+
children: stuck(build.id, build.status) ? "interrupted" : build.status
|
|
115
|
+
}) }),
|
|
116
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs(Meta, {
|
|
117
|
+
as: "span",
|
|
118
|
+
children: [
|
|
119
|
+
build.snapshotCount,
|
|
120
|
+
" total · ",
|
|
121
|
+
build.changedCount,
|
|
122
|
+
" changed"
|
|
123
|
+
]
|
|
124
|
+
}) }),
|
|
125
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
126
|
+
as: "span",
|
|
127
|
+
children: new Date(build.createdAt).toLocaleString()
|
|
128
|
+
}) }),
|
|
129
|
+
/* @__PURE__ */ jsxs("td", {
|
|
130
|
+
class: "nowrap",
|
|
131
|
+
children: [/* @__PURE__ */ jsxs(HStack, {
|
|
132
|
+
wrap: false,
|
|
133
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
134
|
+
variant: "secondary",
|
|
135
|
+
size: "sm",
|
|
136
|
+
href: `/projects/${project.slug}/builds/${build.id}`,
|
|
137
|
+
children: "View"
|
|
138
|
+
}), canRetry && (build.status === "failed" || build.status === "pending" || stuck(build.id, build.status)) ? /* @__PURE__ */ jsx("form", {
|
|
139
|
+
method: "post",
|
|
140
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
141
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
142
|
+
"hx-target": "body",
|
|
143
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
144
|
+
variant: "ghost",
|
|
145
|
+
size: "sm",
|
|
146
|
+
type: "submit",
|
|
147
|
+
children: "Retry"
|
|
148
|
+
})
|
|
149
|
+
}) : null]
|
|
150
|
+
}), queueByBuild.get(build.id)?.error ? /* @__PURE__ */ jsx(Meta, {
|
|
151
|
+
as: "div",
|
|
152
|
+
tone: "danger",
|
|
153
|
+
children: queueByBuild.get(build.id)?.error
|
|
154
|
+
}) : null]
|
|
155
|
+
})
|
|
156
|
+
] }, build.id)) })] }) }),
|
|
157
|
+
recentBuilds.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { description: "No builds yet." }) : null
|
|
158
|
+
] })
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
export { isStuckCapture, renderActiveQueue, renderComputeJobsPage };
|
|
164
|
+
|
|
165
|
+
//# sourceMappingURL=compute-jobs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compute-jobs.mjs","names":[],"sources":["../../src/pages/compute-jobs.tsx"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore } from \"../store.ts\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n SectionTitle,\n Table,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\ninterface QueueView {\n buildId: string;\n status: string;\n queuedAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n}\n\n/** Live-refreshing partial showing the currently queued and running captures. */\nexport function renderActiveQueue(slug: string, queueView: QueueView[]): RenderedContent {\n return (\n <Card\n id=\"active-queue\"\n hx-get={`/projects/${slug}/jobs?partial=queue`}\n hx-trigger=\"every 5s\"\n hx-swap=\"outerHTML\"\n hx-target=\"#active-queue\"\n >\n <SectionTitle>Active queue</SectionTitle>\n {queueView.length === 0 ? (\n <Meta>No captures are currently queued or running.</Meta>\n ) : (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Queued</th>\n <th>Started</th>\n </tr>\n </thead>\n <tbody>\n {queueView.map((job): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={job.buildId}>\n <td>\n <a href={`/projects/${slug}/builds/${job.buildId}`}>\n {job.buildId.slice(0, 8)}\n </a>\n </td>\n <td>\n <Badge tone={statusTone(job.status)}>{job.status}</Badge>\n </td>\n <td>\n <Meta as=\"span\">{new Date(job.queuedAt).toLocaleTimeString()}</Meta>\n </td>\n <td>\n <Meta as=\"span\">\n {job.startedAt ? new Date(job.startedAt).toLocaleTimeString() : \"—\"}\n </Meta>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n );\n}\n\n/**\n * A `capturing` build the in-process queue no longer tracks was cut short by a\n * restart. Remote queues are invisible to the server, so nothing is flagged there.\n */\nexport function isStuckCapture(status: string, tracked: boolean, inProcessQueue: boolean): boolean {\n return inProcessQueue && status === \"capturing\" && !tracked;\n}\n\n/** Compute jobs page: live capture queue plus recent build history with retry. */\nexport async function renderComputeJobsPage(\n slug: string,\n queueView: QueueView[],\n canRetry: boolean,\n inProcessQueue = false,\n): Promise<RenderedContent | null> {\n const projects = await new ProjectModel(getStore().db).list();\n const project = projects.find((item) => item.slug === slug);\n if (!project) {\n return null;\n }\n const builds = await new BuildModel(getStore().db).list(project.id);\n const recentBuilds = builds.slice(0, 20);\n const queueByBuild = new Map(queueView.map((job) => [job.buildId, job]));\n const stuck = (id: string, status: string): boolean =>\n isStuckCapture(status, queueByBuild.has(id), inProcessQueue);\n const stuckCount = recentBuilds.filter((build) => stuck(build.id, build.status)).length;\n\n return (\n <DocumentLayout\n title={`${project.name} · Compute jobs`}\n nav={{ active: \"jobs\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Compute jobs\"\n description=\"Capture jobs run on this server. Queued and running jobs refresh live; recent history is below.\"\n actions={\n <Button variant=\"secondary\" href={`/projects/${project.slug}/builds`}>\n Back to builds\n </Button>\n }\n />\n\n {stuckCount > 0 ? (\n <Alert tone=\"warning\" title=\"Interrupted captures\">\n {stuckCount} build{stuckCount === 1 ? \" was\" : \"s were\"} still capturing when the server\n restarted. On startup an interrupted build is requeued once; if it is interrupted again it\n is marked failed so a crash loop cannot repeat captures. Retry a build to run it again.\n </Alert>\n ) : null}\n\n {renderActiveQueue(project.slug, queueView)}\n\n <Card>\n <SectionTitle>Recent builds</SectionTitle>\n <Meta>Capture history for {project.name}. Failed and interrupted jobs can be retried.</Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Branch</th>\n <th>Status</th>\n <th>Snapshots</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {recentBuilds.map((build): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <tr key={build.id}>\n <td>\n <strong>{build.gitBranch}</strong>\n <Meta as=\"div\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n </td>\n <td>\n <Badge\n tone={stuck(build.id, build.status) ? \"warning\" : statusTone(build.status)}\n >\n {stuck(build.id, build.status) ? \"interrupted\" : build.status}\n </Badge>\n </td>\n <td>\n <Meta as=\"span\">\n {build.snapshotCount} total · {build.changedCount} changed\n </Meta>\n </td>\n <td>\n <Meta as=\"span\">{new Date(build.createdAt).toLocaleString()}</Meta>\n </td>\n <td class=\"nowrap\">\n <HStack wrap={false}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`/projects/${project.slug}/builds/${build.id}`}\n >\n View\n </Button>\n {canRetry &&\n (build.status === \"failed\" ||\n build.status === \"pending\" ||\n stuck(build.id, build.status)) ? (\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-target=\"body\"\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Retry\n </Button>\n </form>\n ) : null}\n </HStack>\n {queueByBuild.get(build.id)?.error ? (\n <Meta as=\"div\" tone=\"danger\">\n {queueByBuild.get(build.id)?.error}\n </Meta>\n ) : null}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n {recentBuilds.length === 0 ? <EmptyState description=\"No builds yet.\" /> : null}\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert } from "../ui/feedback.mjs";
|
|
6
|
+
import { Field } from "../ui/forms.mjs";
|
|
7
|
+
import { Card, PageHeader } from "../ui/layout.mjs";
|
|
8
|
+
import { VStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
|
+
import { DocumentLayout } from "../ui/document.mjs";
|
|
11
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
|
+
//#region src/pages/invite.tsx
|
|
13
|
+
/** Set-password page for invite acceptance (no auth required). */
|
|
14
|
+
function renderInvitePage(state) {
|
|
15
|
+
return /* @__PURE__ */ jsx(DocumentLayout, {
|
|
16
|
+
title: "Set password",
|
|
17
|
+
layout: "bare",
|
|
18
|
+
children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(VStack, {
|
|
19
|
+
gap: "lg",
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsx(PageHeader, {
|
|
22
|
+
title: "Set your password",
|
|
23
|
+
description: state.email ? `for ${state.email}` : void 0
|
|
24
|
+
}),
|
|
25
|
+
state.error ? /* @__PURE__ */ jsx(Alert, {
|
|
26
|
+
tone: "danger",
|
|
27
|
+
title: "Invite failed",
|
|
28
|
+
children: state.error
|
|
29
|
+
}) : null,
|
|
30
|
+
/* @__PURE__ */ jsx("form", {
|
|
31
|
+
method: "post",
|
|
32
|
+
action: `/auth/invites/${state.inviteId}`,
|
|
33
|
+
novalidate: true,
|
|
34
|
+
children: /* @__PURE__ */ jsxs(VStack, { children: [
|
|
35
|
+
/* @__PURE__ */ jsx("input", {
|
|
36
|
+
type: "hidden",
|
|
37
|
+
name: "token",
|
|
38
|
+
value: state.token
|
|
39
|
+
}),
|
|
40
|
+
csrfField(),
|
|
41
|
+
/* @__PURE__ */ jsx(Field, {
|
|
42
|
+
label: "New password",
|
|
43
|
+
name: "password",
|
|
44
|
+
type: "password",
|
|
45
|
+
required: true,
|
|
46
|
+
autofocus: true,
|
|
47
|
+
autocomplete: "new-password"
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ jsx(Field, {
|
|
50
|
+
label: "Confirm password",
|
|
51
|
+
name: "confirm",
|
|
52
|
+
type: "password",
|
|
53
|
+
required: true,
|
|
54
|
+
autocomplete: "new-password"
|
|
55
|
+
}),
|
|
56
|
+
/* @__PURE__ */ jsx(Button, {
|
|
57
|
+
variant: "primary",
|
|
58
|
+
type: "submit",
|
|
59
|
+
children: "Set password and sign in"
|
|
60
|
+
})
|
|
61
|
+
] })
|
|
62
|
+
})
|
|
63
|
+
]
|
|
64
|
+
}) })
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function renderInviteInvalidPage(error) {
|
|
68
|
+
return /* @__PURE__ */ jsx(DocumentLayout, {
|
|
69
|
+
title: "Invite",
|
|
70
|
+
layout: "bare",
|
|
71
|
+
children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(VStack, {
|
|
72
|
+
gap: "lg",
|
|
73
|
+
children: [
|
|
74
|
+
/* @__PURE__ */ jsx(PageHeader, { title: "Invite" }),
|
|
75
|
+
/* @__PURE__ */ jsx(Alert, {
|
|
76
|
+
tone: "danger",
|
|
77
|
+
title: "Invalid invite",
|
|
78
|
+
children: error
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("a", {
|
|
81
|
+
href: "/auth/login",
|
|
82
|
+
children: "Back to sign in"
|
|
83
|
+
}) })
|
|
84
|
+
]
|
|
85
|
+
}) })
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { renderInviteInvalidPage, renderInvitePage };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=invite.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite.mjs","names":[],"sources":["../../src/pages/invite.tsx"],"sourcesContent":["import { Alert, Button, Card, Field, PageHeader, VStack } from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\n\nexport interface InvitePageState {\n inviteId: string;\n token: string;\n email?: string;\n name?: string;\n error?: string;\n}\n\n/** Set-password page for invite acceptance (no auth required). */\nexport function renderInvitePage(state: InvitePageState): RenderedContent {\n return (\n <DocumentLayout title=\"Set password\" layout=\"bare\">\n <Card>\n <VStack gap=\"lg\">\n <PageHeader\n title=\"Set your password\"\n description={state.email ? `for ${state.email}` : undefined}\n />\n {state.error ? (\n <Alert tone=\"danger\" title=\"Invite failed\">\n {state.error}\n </Alert>\n ) : null}\n <form method=\"post\" action={`/auth/invites/${state.inviteId}`} novalidate>\n <VStack>\n <input type=\"hidden\" name=\"token\" value={state.token} />\n {csrfField()}\n <Field\n label=\"New password\"\n name=\"password\"\n type=\"password\"\n required\n autofocus\n autocomplete=\"new-password\"\n />\n <Field\n label=\"Confirm password\"\n name=\"confirm\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Button variant=\"primary\" type=\"submit\">\n Set password and sign in\n </Button>\n </VStack>\n </form>\n </VStack>\n </Card>\n </DocumentLayout>\n );\n}\n\nexport function renderInviteInvalidPage(error: string): RenderedContent {\n return (\n <DocumentLayout title=\"Invite\" layout=\"bare\">\n <Card>\n <VStack gap=\"lg\">\n <PageHeader title=\"Invite\" />\n <Alert tone=\"danger\" title=\"Invalid invite\">\n {error}\n </Alert>\n <p>\n <a href=\"/auth/login\">Back to sign in</a>\n </p>\n </VStack>\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,SAAgB,iBAAiB,OAAyC;CACxE,OACE,oBAAC,gBAAD;EAAgB,OAAM;EAAe,QAAO;EAC1C,UAAA,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD;GAAQ,KAAI;GAAZ,UAAA;IACE,oBAAC,YAAD;KACE,OAAM;KACN,aAAa,MAAM,QAAQ,OAAO,MAAM,UAAU,KAAA;IACnD,CAAA;IACA,MAAM,QACL,oBAAC,OAAD;KAAO,MAAK;KAAS,OAAM;KACxB,UAAA,MAAM;IACF,CAAA,IACL;IACJ,oBAAC,QAAD;KAAM,QAAO;KAAO,QAAQ,iBAAiB,MAAM;KAAY,YAAA;KAC7D,UAAA,qBAAC,QAAD,EAAA,UAAA;MACE,oBAAC,SAAD;OAAO,MAAK;OAAS,MAAK;OAAQ,OAAO,MAAM;MAAQ,CAAA;MACtD,UAAU;MACX,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,MAAK;OACL,UAAA;OACA,WAAA;OACA,cAAa;MACd,CAAA;MACD,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,MAAK;OACL,UAAA;OACA,cAAa;MACd,CAAA;MACD,oBAAC,QAAD;OAAQ,SAAQ;OAAU,MAAK;OAAS,UAAA;MAEhC,CAAA;KACF,EAAA,CAAA;IACJ,CAAA;GACA;EACJ,CAAA,EAAA,CAAA;CACQ,CAAA;AAEpB;AAEA,SAAgB,wBAAwB,OAAgC;CACtE,OACE,oBAAC,gBAAD;EAAgB,OAAM;EAAS,QAAO;EACpC,UAAA,oBAAC,MAAD,EAAA,UACE,qBAAC,QAAD;GAAQ,KAAI;GAAZ,UAAA;IACE,oBAAC,YAAD,EAAY,OAAM,SAAU,CAAA;IAC5B,oBAAC,OAAD;KAAO,MAAK;KAAS,OAAM;KACxB,UAAA;IACI,CAAA;IACP,oBAAC,KAAD,EAAA,UACE,oBAAC,KAAD;KAAG,MAAK;KAAc,UAAA;IAAkB,CAAA,EACvC,CAAA;GACG;EACJ,CAAA,EAAA,CAAA;CACQ,CAAA;AAEpB"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { Button } from "../ui/buttons.mjs";
|
|
6
|
+
import { Badge, EmptyState, Meta, statusTone } from "../ui/feedback.mjs";
|
|
7
|
+
import { PageHeader } from "../ui/layout.mjs";
|
|
8
|
+
import { HStack } from "../ui/stacks.mjs";
|
|
9
|
+
import { Table } from "../ui/table.mjs";
|
|
10
|
+
import { DocumentLayout } from "../ui/document.mjs";
|
|
11
|
+
import { BuildModel, LabelModel, ProjectModel } from "@storyshelf/core/models";
|
|
12
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
13
|
+
//#region src/pages/label-detail.tsx
|
|
14
|
+
/** Resolve a label type's `link_template` against a build, or return null. */
|
|
15
|
+
function resolveLabelLink(template, project, build, value) {
|
|
16
|
+
if (!template) return null;
|
|
17
|
+
const repo = project.gitRepository ?? "";
|
|
18
|
+
return template.replaceAll("{value}", value).replaceAll("{repo}", repo).replaceAll("{branch}", build.gitBranch);
|
|
19
|
+
}
|
|
20
|
+
/** Label page: every build bearing `key=value`, latest first (ADR 0013). */
|
|
21
|
+
async function renderLabelDetailPage(slug, key, value) {
|
|
22
|
+
const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
|
|
23
|
+
if (!project) return null;
|
|
24
|
+
const [builds, labelType] = await Promise.all([new BuildModel(getStore().db).list(project.id, {
|
|
25
|
+
labelKey: key,
|
|
26
|
+
labelValue: value
|
|
27
|
+
}), new LabelModel(getStore().db).getType(project.id, key)]);
|
|
28
|
+
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
29
|
+
title: `${key}: ${value}`,
|
|
30
|
+
nav: {
|
|
31
|
+
active: "labels",
|
|
32
|
+
projectSlug: project.slug,
|
|
33
|
+
projectName: project.name,
|
|
34
|
+
trail: [{ label: `${labelType ? labelType.name : key}: ${value}` }]
|
|
35
|
+
},
|
|
36
|
+
children: [/* @__PURE__ */ jsx(PageHeader, {
|
|
37
|
+
title: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
38
|
+
labelType ? labelType.name : key,
|
|
39
|
+
": ",
|
|
40
|
+
value
|
|
41
|
+
] }),
|
|
42
|
+
description: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
44
|
+
tone: "neutral",
|
|
45
|
+
children: key
|
|
46
|
+
}),
|
|
47
|
+
" Builds bearing this",
|
|
48
|
+
" ",
|
|
49
|
+
labelType ? labelType.name.toLowerCase() : "label",
|
|
50
|
+
", latest first."
|
|
51
|
+
] }),
|
|
52
|
+
actions: /* @__PURE__ */ jsx(Button, {
|
|
53
|
+
variant: "ghost",
|
|
54
|
+
href: `/projects/${project.slug}/labels`,
|
|
55
|
+
children: "All labels"
|
|
56
|
+
})
|
|
57
|
+
}), builds.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
58
|
+
title: "No builds",
|
|
59
|
+
description: `No build currently bears the label ${key}: ${value}.`
|
|
60
|
+
}) : /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
61
|
+
/* @__PURE__ */ jsx("th", { children: "Build" }),
|
|
62
|
+
/* @__PURE__ */ jsx("th", { children: "Status" }),
|
|
63
|
+
/* @__PURE__ */ jsx("th", { children: "Author" }),
|
|
64
|
+
/* @__PURE__ */ jsx("th", { children: "Created" }),
|
|
65
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
66
|
+
class: "visually-hidden",
|
|
67
|
+
children: "Actions"
|
|
68
|
+
}) })
|
|
69
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: builds.map((build) => {
|
|
70
|
+
const link = labelType?.linkTemplate ? resolveLabelLink(labelType.linkTemplate, project, build, value) : null;
|
|
71
|
+
return /* @__PURE__ */ jsxs("tr", { children: [
|
|
72
|
+
/* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("strong", { children: build.gitBranch }), /* @__PURE__ */ jsxs(Meta, {
|
|
73
|
+
as: "div",
|
|
74
|
+
mono: true,
|
|
75
|
+
children: [
|
|
76
|
+
build.gitSha.slice(0, 7),
|
|
77
|
+
" ",
|
|
78
|
+
build.message ? `· ${build.message.slice(0, 60)}` : ""
|
|
79
|
+
]
|
|
80
|
+
})] }),
|
|
81
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
82
|
+
tone: statusTone(build.status),
|
|
83
|
+
children: build.status
|
|
84
|
+
}) }),
|
|
85
|
+
/* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("div", { children: build.authorName ?? "—" }), /* @__PURE__ */ jsx(Meta, {
|
|
86
|
+
as: "div",
|
|
87
|
+
children: build.authorEmail ?? ""
|
|
88
|
+
})] }),
|
|
89
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
90
|
+
as: "span",
|
|
91
|
+
children: new Date(build.createdAt).toLocaleString()
|
|
92
|
+
}) }),
|
|
93
|
+
/* @__PURE__ */ jsx("td", {
|
|
94
|
+
class: "nowrap",
|
|
95
|
+
children: /* @__PURE__ */ jsxs(HStack, {
|
|
96
|
+
wrap: false,
|
|
97
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
98
|
+
variant: "secondary",
|
|
99
|
+
size: "sm",
|
|
100
|
+
href: `/projects/${project.slug}/builds/${build.id}`,
|
|
101
|
+
children: "View"
|
|
102
|
+
}), link ? /* @__PURE__ */ jsx(Button, {
|
|
103
|
+
variant: "ghost",
|
|
104
|
+
size: "sm",
|
|
105
|
+
href: link,
|
|
106
|
+
target: "_blank",
|
|
107
|
+
rel: "noreferrer",
|
|
108
|
+
children: "Open link"
|
|
109
|
+
}) : null]
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
] }, build.id);
|
|
113
|
+
}) })] }) })]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Label types overview page (architecture.md `GET /projects/:slug/labels`). */
|
|
117
|
+
async function renderLabelsPage(slug) {
|
|
118
|
+
const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
|
|
119
|
+
if (!project) return null;
|
|
120
|
+
const labelTypes = await new LabelModel(getStore().db).listTypes(project.id);
|
|
121
|
+
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
122
|
+
title: `Labels · ${project.name}`,
|
|
123
|
+
nav: {
|
|
124
|
+
active: "labels",
|
|
125
|
+
projectSlug: project.slug,
|
|
126
|
+
projectName: project.name
|
|
127
|
+
},
|
|
128
|
+
children: [/* @__PURE__ */ jsx(PageHeader, {
|
|
129
|
+
title: "Labels",
|
|
130
|
+
description: "Typed labels attach values to builds and link out to external systems."
|
|
131
|
+
}), labelTypes.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
132
|
+
title: "No label types",
|
|
133
|
+
description: "No label types are configured for this project."
|
|
134
|
+
}) : /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
135
|
+
/* @__PURE__ */ jsx("th", { children: "Key" }),
|
|
136
|
+
/* @__PURE__ */ jsx("th", { children: "Name" }),
|
|
137
|
+
/* @__PURE__ */ jsx("th", { children: "Template" })
|
|
138
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: labelTypes.map((labelType) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
139
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
140
|
+
tone: "neutral",
|
|
141
|
+
children: labelType.key
|
|
142
|
+
}) }),
|
|
143
|
+
/* @__PURE__ */ jsx("td", { children: labelType.name }),
|
|
144
|
+
/* @__PURE__ */ jsx("td", {
|
|
145
|
+
class: "truncate max-w-cell",
|
|
146
|
+
children: labelType.linkTemplate ?? "—"
|
|
147
|
+
})
|
|
148
|
+
] }, labelType.id)) })] }) })]
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { renderLabelDetailPage, renderLabelsPage, resolveLabelLink };
|
|
153
|
+
|
|
154
|
+
//# sourceMappingURL=label-detail.mjs.map
|