@storyshelf/app 0.6.2 → 0.7.0
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/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +203 -125
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +91 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +3 -1
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -9
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Badge, EmptyState, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
7
|
+
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
8
|
+
import { Table } from "../ui/table.mjs";
|
|
9
|
+
import { PreviewEmbed } from "../ui/preview-embed.mjs";
|
|
10
|
+
import { PREVIEW_TOKEN_REFRESH, isSignedPreviewBase } from "../routers/preview-base.mjs";
|
|
11
|
+
import { StoryComments } from "./review-comments.mjs";
|
|
12
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
13
|
+
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
14
|
+
//#region src/pages/component-stories.tsx
|
|
15
|
+
/** Stories tab: the selected story in a sandboxed preview, and every story and docs entry. */
|
|
16
|
+
function StoriesTab(props) {
|
|
17
|
+
const { data } = props;
|
|
18
|
+
const { entries } = data.catalog;
|
|
19
|
+
const selected = selectedStory(data);
|
|
20
|
+
if (entries.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
|
|
21
|
+
title: "No stories in this build",
|
|
22
|
+
description: "The build's Storybook index lists no stories for this file."
|
|
23
|
+
});
|
|
24
|
+
return /* @__PURE__ */ jsxs(VStack, { children: [
|
|
25
|
+
selected ? /* @__PURE__ */ jsx(PreviewCard, {
|
|
26
|
+
data,
|
|
27
|
+
story: selected
|
|
28
|
+
}) : null,
|
|
29
|
+
selected && data.review ? /* @__PURE__ */ jsx(StoryComments, {
|
|
30
|
+
project: data.project,
|
|
31
|
+
build: data.catalog.build,
|
|
32
|
+
storyId: selected.id,
|
|
33
|
+
comments: data.review.comments,
|
|
34
|
+
canComment: data.review.canComment
|
|
35
|
+
}) : null,
|
|
36
|
+
/* @__PURE__ */ jsx(Card, {
|
|
37
|
+
padded: false,
|
|
38
|
+
children: /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
39
|
+
/* @__PURE__ */ jsx("th", { children: "Name" }),
|
|
40
|
+
/* @__PURE__ */ jsx("th", { children: "Kind" }),
|
|
41
|
+
/* @__PURE__ */ jsx("th", { children: "Tags" }),
|
|
42
|
+
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
43
|
+
class: "visually-hidden",
|
|
44
|
+
children: "Actions"
|
|
45
|
+
}) })
|
|
46
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: entries.map((entry) => /* @__PURE__ */ jsx(EntryRow, {
|
|
47
|
+
entry,
|
|
48
|
+
data,
|
|
49
|
+
selected: selected?.id
|
|
50
|
+
}, entry.id)) })] }) })
|
|
51
|
+
})
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
/** The story the preview shows: `?story=` when it names one, else the first. */
|
|
55
|
+
function selectedStory(data) {
|
|
56
|
+
const stories = data.catalog.entries.filter((entry) => entry.type === "story");
|
|
57
|
+
return stories.find((entry) => entry.id === data.story) ?? stories[0];
|
|
58
|
+
}
|
|
59
|
+
const previewPath = (story) => `/iframe.html?id=${encodeURIComponent(story.id)}&viewMode=story`;
|
|
60
|
+
/** A frame under a signed URL asks for a new token before the old one expires. */
|
|
61
|
+
function refreshOf(data, story) {
|
|
62
|
+
if (data.previewBase === null || !isSignedPreviewBase(data.previewBase)) return {};
|
|
63
|
+
return { refresh: {
|
|
64
|
+
url: `/api/v1/projects/${data.project.slug}/builds/${data.catalog.build.id}/preview-token`,
|
|
65
|
+
path: previewPath(story),
|
|
66
|
+
seconds: PREVIEW_TOKEN_REFRESH
|
|
67
|
+
} };
|
|
68
|
+
}
|
|
69
|
+
function PreviewCard(props) {
|
|
70
|
+
const { data, story } = props;
|
|
71
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
72
|
+
id: "preview",
|
|
73
|
+
children: [/* @__PURE__ */ jsx(SectionTitle, { children: story.name }), data.previewBase === null ? /* @__PURE__ */ jsx(Meta, { children: "This build needs a sign-in to view, so it cannot run in a frame here. Open it in Storybook instead." }) : /* @__PURE__ */ jsx(PreviewEmbed, {
|
|
74
|
+
src: `${data.previewBase}${previewPath(story)}`,
|
|
75
|
+
title: `${story.title} / ${story.name}`,
|
|
76
|
+
...refreshOf(data, story)
|
|
77
|
+
})]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function EntryRow(props) {
|
|
81
|
+
const { entry, data } = props;
|
|
82
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
83
|
+
id: entry.id,
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsx("td", { children: entry.name }),
|
|
86
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
87
|
+
tone: entry.type === "docs" ? "info" : "neutral",
|
|
88
|
+
children: entry.type
|
|
89
|
+
}) }),
|
|
90
|
+
/* @__PURE__ */ jsx("td", { children: entry.tags.join(", ") }),
|
|
91
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs(HStack, { children: [entry.type === "story" && entry.id !== props.selected ? /* @__PURE__ */ jsx(Button, {
|
|
92
|
+
variant: "ghost",
|
|
93
|
+
size: "sm",
|
|
94
|
+
icon: "image",
|
|
95
|
+
href: `?tab=stories&story=${encodeURIComponent(entry.id)}#preview`,
|
|
96
|
+
children: "Preview"
|
|
97
|
+
}) : null, /* @__PURE__ */ jsx(Button, {
|
|
98
|
+
variant: "ghost",
|
|
99
|
+
size: "sm",
|
|
100
|
+
icon: "external-link",
|
|
101
|
+
href: `${storybookBase(data)}/?storyId=${encodeURIComponent(entry.id)}${entry.type === "docs" ? "&viewMode=docs" : ""}`,
|
|
102
|
+
target: "_blank",
|
|
103
|
+
rel: "noopener",
|
|
104
|
+
children: "Open"
|
|
105
|
+
})] }) })
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Published builds that hold the component, and whether its stories changed since the build before. */
|
|
110
|
+
function ComponentBuilds(props) {
|
|
111
|
+
const { data } = props;
|
|
112
|
+
const urls = createUrlBuilder("/", data.publishedBaseDomain);
|
|
113
|
+
const { history } = data.catalog;
|
|
114
|
+
if (history.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
|
|
115
|
+
title: "No published builds",
|
|
116
|
+
description: "No catalogued build holds this file yet."
|
|
117
|
+
});
|
|
118
|
+
return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, { children: "Published builds" }), /* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
119
|
+
/* @__PURE__ */ jsx("th", { children: "Build" }),
|
|
120
|
+
/* @__PURE__ */ jsx("th", { children: "Branch" }),
|
|
121
|
+
/* @__PURE__ */ jsx("th", { children: "Stories" }),
|
|
122
|
+
/* @__PURE__ */ jsx("th", { children: "Changes" })
|
|
123
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: history.map((row) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
124
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("a", {
|
|
125
|
+
href: urls.build(data.project.slug, row.build.id),
|
|
126
|
+
children: row.build.gitSha.slice(0, 7)
|
|
127
|
+
}) }),
|
|
128
|
+
/* @__PURE__ */ jsx("td", { children: row.build.gitBranch }),
|
|
129
|
+
/* @__PURE__ */ jsx("td", { children: row.component.storyCount }),
|
|
130
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(ChangeCell, { row }) })
|
|
131
|
+
] }, row.build.id)) })] }) })] });
|
|
132
|
+
}
|
|
133
|
+
function ChangeCell(props) {
|
|
134
|
+
const { row } = props;
|
|
135
|
+
if (row.change === "new") return /* @__PURE__ */ jsx(Badge, {
|
|
136
|
+
tone: "info",
|
|
137
|
+
children: "new here"
|
|
138
|
+
});
|
|
139
|
+
if (row.change === "unchanged") return /* @__PURE__ */ jsx(Meta, {
|
|
140
|
+
as: "span",
|
|
141
|
+
children: "unchanged"
|
|
142
|
+
});
|
|
143
|
+
const parts = changeParts(row);
|
|
144
|
+
return /* @__PURE__ */ jsxs(HStack, { children: [/* @__PURE__ */ jsx(Badge, {
|
|
145
|
+
tone: "warning",
|
|
146
|
+
children: "changed"
|
|
147
|
+
}), parts ? /* @__PURE__ */ jsx(Meta, {
|
|
148
|
+
as: "span",
|
|
149
|
+
children: parts
|
|
150
|
+
}) : null] });
|
|
151
|
+
}
|
|
152
|
+
/** "2 added · 1 removed · 1 renamed", or empty when the counts are not known. */
|
|
153
|
+
function changeParts(row) {
|
|
154
|
+
const { changes } = row;
|
|
155
|
+
if (!changes) return "";
|
|
156
|
+
return [
|
|
157
|
+
changes.added > 0 ? `${changes.added} added` : "",
|
|
158
|
+
changes.removed > 0 ? `${changes.removed} removed` : "",
|
|
159
|
+
changes.renamed > 0 ? `${changes.renamed} renamed` : ""
|
|
160
|
+
].filter((part) => part !== "").join(" · ");
|
|
161
|
+
}
|
|
162
|
+
function storybookBase(data) {
|
|
163
|
+
return createUrlBuilder("/", data.publishedBaseDomain).storybookBuild(data.project.slug, data.catalog.build.id);
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { ComponentBuilds, StoriesTab };
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=component-stories.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-stories.mjs","names":[],"sources":["../../src/pages/component-stories.tsx"],"sourcesContent":["/** The catalog's tabs of the component page: its stories with a click-to-load preview, and its builds. */\nimport type { CatalogEntry } from \"@storyshelf/core/capture\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { CatalogHistoryRow } from \"../routers/component-catalog.handlers.ts\";\nimport { isSignedPreviewBase, PREVIEW_TOKEN_REFRESH } from \"../routers/preview-base.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PreviewEmbed,\n SectionTitle,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport type { ComponentPageData } from \"./component-detail.tsx\";\nimport { StoryComments } from \"./review-comments.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** Stories tab: the selected story in a sandboxed preview, and every story and docs entry. */\nexport function StoriesTab(props: { data: ComponentPageData }): Rendered {\n const { data } = props;\n const { entries } = data.catalog;\n const selected = selectedStory(data);\n if (entries.length === 0) {\n return (\n <EmptyState\n title=\"No stories in this build\"\n description=\"The build's Storybook index lists no stories for this file.\"\n />\n );\n }\n return (\n <VStack>\n {selected ? <PreviewCard data={data} story={selected} /> : null}\n {selected && data.review ? (\n <StoryComments\n project={data.project}\n build={data.catalog.build}\n storyId={selected.id}\n comments={data.review.comments}\n canComment={data.review.canComment}\n />\n ) : null}\n <Card padded={false}>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Name</th>\n <th>Kind</th>\n <th>Tags</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {entries.map((entry): Rendered => (\n <EntryRow key={entry.id} entry={entry} data={data} selected={selected?.id} />\n ))}\n </tbody>\n </table>\n </Table>\n </Card>\n </VStack>\n );\n}\n\n/** The story the preview shows: `?story=` when it names one, else the first. */\nfunction selectedStory(data: ComponentPageData): CatalogEntry | undefined {\n const stories = data.catalog.entries.filter((entry) => entry.type === \"story\");\n return stories.find((entry) => entry.id === data.story) ?? stories[0];\n}\n\nconst previewPath = (story: CatalogEntry): string =>\n `/iframe.html?id=${encodeURIComponent(story.id)}&viewMode=story`;\n\n/** A frame under a signed URL asks for a new token before the old one expires. */\nfunction refreshOf(\n data: ComponentPageData,\n story: CatalogEntry,\n): { refresh?: { url: string; path: string; seconds: number } } {\n if (data.previewBase === null || !isSignedPreviewBase(data.previewBase)) {\n return {};\n }\n return {\n refresh: {\n url: `/api/v1/projects/${data.project.slug}/builds/${data.catalog.build.id}/preview-token`,\n path: previewPath(story),\n seconds: PREVIEW_TOKEN_REFRESH,\n },\n };\n}\n\nfunction PreviewCard(props: { data: ComponentPageData; story: CatalogEntry }): Rendered {\n const { data, story } = props;\n return (\n <Card id=\"preview\">\n <SectionTitle>{story.name}</SectionTitle>\n {data.previewBase === null ? (\n <Meta>\n This build needs a sign-in to view, so it cannot run in a frame here. Open it in Storybook\n instead.\n </Meta>\n ) : (\n <PreviewEmbed\n src={`${data.previewBase}${previewPath(story)}`}\n title={`${story.title} / ${story.name}`}\n {...refreshOf(data, story)}\n />\n )}\n </Card>\n );\n}\n\nfunction EntryRow(props: {\n entry: CatalogEntry;\n data: ComponentPageData;\n selected: string | undefined;\n}): Rendered {\n const { entry, data } = props;\n return (\n <tr id={entry.id}>\n <td>{entry.name}</td>\n <td>\n <Badge tone={entry.type === \"docs\" ? \"info\" : \"neutral\"}>{entry.type}</Badge>\n </td>\n <td>{entry.tags.join(\", \")}</td>\n <td>\n <HStack>\n {entry.type === \"story\" && entry.id !== props.selected ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"image\"\n href={`?tab=stories&story=${encodeURIComponent(entry.id)}#preview`}\n >\n Preview\n </Button>\n ) : null}\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={`${storybookBase(data)}/?storyId=${encodeURIComponent(entry.id)}${entry.type === \"docs\" ? \"&viewMode=docs\" : \"\"}`}\n target=\"_blank\"\n rel=\"noopener\"\n >\n Open\n </Button>\n </HStack>\n </td>\n </tr>\n );\n}\n\n/** Published builds that hold the component, and whether its stories changed since the build before. */\nexport function ComponentBuilds(props: { data: ComponentPageData }): Rendered {\n const { data } = props;\n const urls = createUrlBuilder(\"/\", data.publishedBaseDomain);\n const { history } = data.catalog;\n if (history.length === 0) {\n return (\n <EmptyState\n title=\"No published builds\"\n description=\"No catalogued build holds this file yet.\"\n />\n );\n }\n return (\n <Card>\n <SectionTitle>Published builds</SectionTitle>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Branch</th>\n <th>Stories</th>\n <th>Changes</th>\n </tr>\n </thead>\n <tbody>\n {history.map((row): Rendered => (\n <tr key={row.build.id}>\n <td>\n <a href={urls.build(data.project.slug, row.build.id)}>\n {row.build.gitSha.slice(0, 7)}\n </a>\n </td>\n <td>{row.build.gitBranch}</td>\n <td>{row.component.storyCount}</td>\n <td>\n <ChangeCell row={row} />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </Table>\n </Card>\n );\n}\n\nfunction ChangeCell(props: { row: CatalogHistoryRow }): Rendered {\n const { row } = props;\n if (row.change === \"new\") {\n return <Badge tone=\"info\">new here</Badge>;\n }\n if (row.change === \"unchanged\") {\n return <Meta as=\"span\">unchanged</Meta>;\n }\n const parts = changeParts(row);\n return (\n <HStack>\n <Badge tone=\"warning\">changed</Badge>\n {parts ? <Meta as=\"span\">{parts}</Meta> : null}\n </HStack>\n );\n}\n\n/** \"2 added · 1 removed · 1 renamed\", or empty when the counts are not known. */\nfunction changeParts(row: CatalogHistoryRow): string {\n const { changes } = row;\n if (!changes) {\n return \"\";\n }\n return [\n changes.added > 0 ? `${changes.added} added` : \"\",\n changes.removed > 0 ? `${changes.removed} removed` : \"\",\n changes.renamed > 0 ? `${changes.renamed} renamed` : \"\",\n ]\n .filter((part) => part !== \"\")\n .join(\" · \");\n}\n\nfunction storybookBase(data: ComponentPageData): string {\n return createUrlBuilder(\"/\", data.publishedBaseDomain).storybookBuild(\n data.project.slug,\n data.catalog.build.id,\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
5
|
import { Button } from "../ui/buttons.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { Badge, EmptyState, Meta, statusTone } from "../ui/feedback.mjs";
|
|
7
7
|
import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
|
|
8
8
|
import { HStack } from "../ui/stacks.mjs";
|
|
9
9
|
import { Table } from "../ui/table.mjs";
|
|
@@ -11,6 +11,12 @@ import { DocumentLayout } from "../ui/document.mjs";
|
|
|
11
11
|
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
12
12
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
13
13
|
//#region src/pages/compute-jobs.tsx
|
|
14
|
+
/** Build statuses the retry button of the jobs table offers. */
|
|
15
|
+
const RETRYABLE = /* @__PURE__ */ new Set([
|
|
16
|
+
"failed",
|
|
17
|
+
"cancelled",
|
|
18
|
+
"pending"
|
|
19
|
+
]);
|
|
14
20
|
/** Live-refreshing partial showing the currently queued and running captures. */
|
|
15
21
|
function renderActiveQueue(slug, queueView) {
|
|
16
22
|
return /* @__PURE__ */ jsxs(Card, {
|
|
@@ -44,21 +50,12 @@ function renderActiveQueue(slug, queueView) {
|
|
|
44
50
|
] }, job.buildId)) })] }) })]
|
|
45
51
|
});
|
|
46
52
|
}
|
|
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
53
|
/** Compute jobs page: live capture queue plus recent build history with retry. */
|
|
55
|
-
async function renderComputeJobsPage(slug, queueView, canRetry
|
|
54
|
+
async function renderComputeJobsPage(slug, queueView, canRetry) {
|
|
56
55
|
const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
|
|
57
56
|
if (!project) return null;
|
|
58
57
|
const recentBuilds = (await new BuildModel(getStore().db).list(project.id)).slice(0, 20);
|
|
59
58
|
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
59
|
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
63
60
|
title: `${project.name} · Compute jobs`,
|
|
64
61
|
nav: {
|
|
@@ -76,23 +73,13 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
76
73
|
children: "Back to builds"
|
|
77
74
|
})
|
|
78
75
|
}),
|
|
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
76
|
renderActiveQueue(project.slug, queueView),
|
|
90
77
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
91
78
|
/* @__PURE__ */ jsx(SectionTitle, { children: "Recent builds" }),
|
|
92
79
|
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
93
80
|
"Capture history for ",
|
|
94
81
|
project.name,
|
|
95
|
-
". Failed
|
|
82
|
+
". Failed jobs can be retried."
|
|
96
83
|
] }),
|
|
97
84
|
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
98
85
|
/* @__PURE__ */ jsx("th", { children: "Branch" }),
|
|
@@ -110,8 +97,8 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
110
97
|
children: build.gitSha.slice(0, 7)
|
|
111
98
|
})] }),
|
|
112
99
|
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
113
|
-
tone:
|
|
114
|
-
children:
|
|
100
|
+
tone: statusTone(build.status),
|
|
101
|
+
children: build.status
|
|
115
102
|
}) }),
|
|
116
103
|
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs(Meta, {
|
|
117
104
|
as: "span",
|
|
@@ -135,7 +122,7 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
135
122
|
size: "sm",
|
|
136
123
|
href: `/projects/${project.slug}/builds/${build.id}`,
|
|
137
124
|
children: "View"
|
|
138
|
-
}), canRetry &&
|
|
125
|
+
}), canRetry && RETRYABLE.has(build.status) ? /* @__PURE__ */ jsx("form", {
|
|
139
126
|
method: "post",
|
|
140
127
|
action: `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
141
128
|
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
@@ -160,6 +147,6 @@ async function renderComputeJobsPage(slug, queueView, canRetry, inProcessQueue =
|
|
|
160
147
|
});
|
|
161
148
|
}
|
|
162
149
|
//#endregion
|
|
163
|
-
export {
|
|
150
|
+
export { renderActiveQueue, renderComputeJobsPage };
|
|
164
151
|
|
|
165
152
|
//# sourceMappingURL=compute-jobs.mjs.map
|
|
@@ -1 +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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"}
|
|
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 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\";\n\n/** Build statuses the retry button of the jobs table offers. */\nconst RETRYABLE: ReadonlySet<string> = new Set([\"failed\", \"cancelled\", \"pending\"]);\n\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/** 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): 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\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 {renderActiveQueue(project.slug, queueView)}\n\n <Card>\n <SectionTitle>Recent builds</SectionTitle>\n <Meta>Capture history for {project.name}. Failed 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 tone={statusTone(build.status)}>{build.status}</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 && RETRYABLE.has(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,182 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Alert, Meta } from "../ui/feedback.mjs";
|
|
6
|
+
import { HStack } from "../ui/stacks.mjs";
|
|
7
|
+
import { Dropdown, DropdownItem } from "../ui/dropdown.mjs";
|
|
8
|
+
import { DesignPreview, DesignReference } from "../ui/design-reference.mjs";
|
|
9
|
+
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
+
//#region src/pages/design-links.tsx
|
|
12
|
+
/**
|
|
13
|
+
* Design actions for a component's link(s). A link with a cached render opens a
|
|
14
|
+
* click popover (preview + `Open in <provider>`); others are plain deep links.
|
|
15
|
+
*/
|
|
16
|
+
function DesignButtons(props) {
|
|
17
|
+
const { urls, providerName, previews, componentHref, size = "sm" } = props;
|
|
18
|
+
return /* @__PURE__ */ jsx(Fragment, { children: urls.map((url, index) => {
|
|
19
|
+
const name = providerName(url);
|
|
20
|
+
const label = urls.length > 1 ? `${name} ${index + 1}` : name;
|
|
21
|
+
const preview = previews?.get(url);
|
|
22
|
+
return preview?.renderUrl ? /* @__PURE__ */ jsxs(Dropdown, {
|
|
23
|
+
label,
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsx(DesignPreview, {
|
|
26
|
+
src: preview.renderUrl,
|
|
27
|
+
name: preview.name,
|
|
28
|
+
providerName: name
|
|
29
|
+
}),
|
|
30
|
+
/* @__PURE__ */ jsxs(DropdownItem, {
|
|
31
|
+
href: url,
|
|
32
|
+
icon: "external-link",
|
|
33
|
+
target: "_blank",
|
|
34
|
+
rel: "noopener noreferrer",
|
|
35
|
+
children: ["Open in ", name]
|
|
36
|
+
}),
|
|
37
|
+
componentHref ? /* @__PURE__ */ jsx(DropdownItem, {
|
|
38
|
+
href: componentHref,
|
|
39
|
+
children: "View component →"
|
|
40
|
+
}) : null
|
|
41
|
+
]
|
|
42
|
+
}, url) : /* @__PURE__ */ jsx(Button, {
|
|
43
|
+
variant: "ghost",
|
|
44
|
+
size,
|
|
45
|
+
icon: "external-link",
|
|
46
|
+
href: url,
|
|
47
|
+
target: "_blank",
|
|
48
|
+
rel: "noopener noreferrer",
|
|
49
|
+
children: label
|
|
50
|
+
}, url);
|
|
51
|
+
}) });
|
|
52
|
+
}
|
|
53
|
+
/** Reference panes (cached renders) for the review page, shown under the diff stage. */
|
|
54
|
+
function DesignReferences(props) {
|
|
55
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.items.map((item) => item.renderUrl ? /* @__PURE__ */ jsx(DesignReference, {
|
|
56
|
+
src: item.renderUrl,
|
|
57
|
+
name: item.name,
|
|
58
|
+
href: item.url,
|
|
59
|
+
providerName: item.providerName
|
|
60
|
+
}, item.url) : null) });
|
|
61
|
+
}
|
|
62
|
+
/** Review-page design strip: open-in-provider actions plus non-blocking stale/deleted notices. */
|
|
63
|
+
function DesignStrip(props) {
|
|
64
|
+
const { items } = props;
|
|
65
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
66
|
+
/* @__PURE__ */ jsx(HStack, { children: items.map((item) => /* @__PURE__ */ jsxs(HStack, {
|
|
67
|
+
wrap: false,
|
|
68
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
69
|
+
variant: "secondary",
|
|
70
|
+
size: "sm",
|
|
71
|
+
icon: "external-link",
|
|
72
|
+
href: item.url,
|
|
73
|
+
target: "_blank",
|
|
74
|
+
rel: "noopener noreferrer",
|
|
75
|
+
children: ["Open in ", item.providerName]
|
|
76
|
+
}), item.name ? /* @__PURE__ */ jsx(Meta, {
|
|
77
|
+
as: "span",
|
|
78
|
+
children: item.name
|
|
79
|
+
}) : null]
|
|
80
|
+
}, item.url)) }),
|
|
81
|
+
items.map((item) => designNotice(item)),
|
|
82
|
+
connectPrompts(items)
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
/** One connect/disconnect prompt per provider (several links of one provider share it). */
|
|
86
|
+
function connectPrompts(items) {
|
|
87
|
+
return [...new Map(items.flatMap((item) => item.connect ? [[item.providerName, item]] : [])).values()].map((item) => connectPrompt(item, item.connect));
|
|
88
|
+
}
|
|
89
|
+
function connectPrompt(item, connect) {
|
|
90
|
+
if (!connect) return /* @__PURE__ */ jsx(Fragment, {});
|
|
91
|
+
if (connect.state === "connected") return /* @__PURE__ */ jsxs("form", {
|
|
92
|
+
method: "post",
|
|
93
|
+
action: connect.disconnectUrl,
|
|
94
|
+
children: [
|
|
95
|
+
csrfField(),
|
|
96
|
+
/* @__PURE__ */ jsx("input", {
|
|
97
|
+
type: "hidden",
|
|
98
|
+
name: "return",
|
|
99
|
+
value: connect.returnTo
|
|
100
|
+
}),
|
|
101
|
+
/* @__PURE__ */ jsxs(HStack, {
|
|
102
|
+
wrap: false,
|
|
103
|
+
children: [/* @__PURE__ */ jsxs(Meta, {
|
|
104
|
+
as: "span",
|
|
105
|
+
children: [
|
|
106
|
+
"Previews use your ",
|
|
107
|
+
item.providerName,
|
|
108
|
+
" account."
|
|
109
|
+
]
|
|
110
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
111
|
+
variant: "ghost",
|
|
112
|
+
size: "sm",
|
|
113
|
+
type: "submit",
|
|
114
|
+
children: "Disconnect"
|
|
115
|
+
})]
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
}, item.providerName);
|
|
119
|
+
return /* @__PURE__ */ jsxs(Alert, {
|
|
120
|
+
tone: connect.state === "reconnect" ? "warning" : "info",
|
|
121
|
+
title: connect.state === "reconnect" ? `Your ${item.providerName} connection expired` : `Connect ${item.providerName} for design previews`,
|
|
122
|
+
children: [
|
|
123
|
+
"Sign in with your own ",
|
|
124
|
+
item.providerName,
|
|
125
|
+
" account to see names, previews and stale-design warnings. Only you will see what your account can open.",
|
|
126
|
+
" ",
|
|
127
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
128
|
+
variant: "secondary",
|
|
129
|
+
size: "sm",
|
|
130
|
+
href: connect.startUrl,
|
|
131
|
+
children: [
|
|
132
|
+
connect.state === "reconnect" ? "Reconnect" : "Connect",
|
|
133
|
+
" ",
|
|
134
|
+
item.providerName
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}, item.providerName);
|
|
139
|
+
}
|
|
140
|
+
function designNotice(item) {
|
|
141
|
+
if (item.state === "deleted") return /* @__PURE__ */ jsxs(Alert, {
|
|
142
|
+
tone: "warning",
|
|
143
|
+
title: "Design not found",
|
|
144
|
+
children: [
|
|
145
|
+
"The linked ",
|
|
146
|
+
item.providerName,
|
|
147
|
+
" file or node no longer exists. Update the link in the project's Design settings."
|
|
148
|
+
]
|
|
149
|
+
}, item.url);
|
|
150
|
+
if (item.stale) return /* @__PURE__ */ jsxs(Alert, {
|
|
151
|
+
tone: "warning",
|
|
152
|
+
title: "Design changed since this baseline was approved",
|
|
153
|
+
children: [
|
|
154
|
+
item.name ? `${item.name} was` : "The linked design was",
|
|
155
|
+
" edited in ",
|
|
156
|
+
item.providerName,
|
|
157
|
+
" ",
|
|
158
|
+
"after the baseline. This does not block review."
|
|
159
|
+
]
|
|
160
|
+
}, item.url);
|
|
161
|
+
if (item.state === "unauthorized" || item.state === "forbidden") return /* @__PURE__ */ jsx(Meta, {
|
|
162
|
+
as: "div",
|
|
163
|
+
children: item.state === "unauthorized" ? `The project's ${item.providerName} credential is expired or revoked — design status is unavailable.` : `The project's ${item.providerName} credential cannot open this file — design status is unavailable.`
|
|
164
|
+
}, item.url);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/** Library header links to the project's design files. */
|
|
168
|
+
function ProjectDesignLinks(props) {
|
|
169
|
+
return /* @__PURE__ */ jsx(HStack, { children: props.links.map((link) => /* @__PURE__ */ jsx(Button, {
|
|
170
|
+
variant: "ghost",
|
|
171
|
+
size: "sm",
|
|
172
|
+
icon: "external-link",
|
|
173
|
+
href: link.url,
|
|
174
|
+
target: "_blank",
|
|
175
|
+
rel: "noopener noreferrer",
|
|
176
|
+
children: link.label
|
|
177
|
+
}, link.url)) });
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
export { DesignButtons, DesignReferences, DesignStrip, ProjectDesignLinks };
|
|
181
|
+
|
|
182
|
+
//# sourceMappingURL=design-links.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-links.mjs","names":[],"sources":["../../src/pages/design-links.tsx"],"sourcesContent":["import type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Alert,\n Button,\n DesignPreview,\n DesignReference,\n Dropdown,\n DropdownItem,\n HStack,\n Meta,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype Rendered = HtmlEscapedString | Promise<HtmlEscapedString>;\n\n/** \"Connect your own account\" state for a provider whose project has no shared credential. */\nexport interface DesignConnect {\n /** `connect` (not connected), `reconnect` (connection expired) or `connected`. */\n state: \"connect\" | \"reconnect\" | \"connected\";\n startUrl: string;\n disconnectUrl: string;\n returnTo: string;\n}\n\n/** A design link a component resolves to, with cached provider metadata when available. */\nexport interface DesignItem {\n url: string;\n /** Display name of the provider that owns the link (\"Figma\"). */\n providerName: string;\n /** Design node/file name from the metadata cache. */\n name: string | null;\n /** Cache state: `ok` | `deleted` | `unauthorized` | `forbidden` | `unknown` (nothing cached). */\n state: string;\n /** API URL of the cached render, when one exists. */\n renderUrl: string | null;\n /** The design changed after the baseline it is compared with was approved. */\n stale: boolean;\n /** Offer to connect the viewer's own account; absent when not applicable. */\n connect?: DesignConnect;\n}\n\n/** Cached name and render of a design link, for library popovers. */\nexport interface DesignPreviewInfo {\n name: string | null;\n renderUrl: string | null;\n}\n\n/** A project-level design file link for the library header. */\nexport interface ProjectDesignLink {\n label: string;\n url: string;\n}\n\n/**\n * Design actions for a component's link(s). A link with a cached render opens a\n * click popover (preview + `Open in <provider>`); others are plain deep links.\n */\nexport function DesignButtons(props: {\n urls: readonly string[];\n /** Display name of the provider that owns a link. */\n providerName: (url: string) => string;\n previews?: ReadonlyMap<string, DesignPreviewInfo>;\n /** Component page of the story, offered in the popover as `View component →`. */\n componentHref?: string;\n size?: \"sm\" | \"md\";\n}): Rendered {\n const { urls, providerName, previews, componentHref, size = \"sm\" } = props;\n return (\n <>\n {urls.map((url, index): Rendered => {\n const name = providerName(url);\n const label = urls.length > 1 ? `${name} ${index + 1}` : name;\n const preview = previews?.get(url);\n return preview?.renderUrl ? (\n <Dropdown key={url} label={label}>\n <DesignPreview src={preview.renderUrl} name={preview.name} providerName={name} />\n <DropdownItem href={url} icon=\"external-link\" target=\"_blank\" rel=\"noopener noreferrer\">\n Open in {name}\n </DropdownItem>\n {componentHref ? (\n <DropdownItem href={componentHref}>View component →</DropdownItem>\n ) : null}\n </Dropdown>\n ) : (\n <Button\n key={url}\n variant=\"ghost\"\n size={size}\n icon=\"external-link\"\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {label}\n </Button>\n );\n })}\n </>\n );\n}\n\n/** Reference panes (cached renders) for the review page, shown under the diff stage. */\nexport function DesignReferences(props: { items: DesignItem[] }): Rendered {\n return (\n <>\n {props.items.map((item): Rendered | null =>\n item.renderUrl ? (\n <DesignReference\n key={item.url}\n src={item.renderUrl}\n name={item.name}\n href={item.url}\n providerName={item.providerName}\n />\n ) : null,\n )}\n </>\n );\n}\n\n/** Review-page design strip: open-in-provider actions plus non-blocking stale/deleted notices. */\nexport function DesignStrip(props: { items: DesignItem[] }): Rendered {\n const { items } = props;\n return (\n <>\n <HStack>\n {items.map((item): Rendered => (\n <HStack key={item.url} wrap={false}>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon=\"external-link\"\n href={item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Open in {item.providerName}\n </Button>\n {item.name ? <Meta as=\"span\">{item.name}</Meta> : null}\n </HStack>\n ))}\n </HStack>\n {items.map((item): Rendered | null => designNotice(item))}\n {connectPrompts(items)}\n </>\n );\n}\n\n/** One connect/disconnect prompt per provider (several links of one provider share it). */\nfunction connectPrompts(items: DesignItem[]): Rendered[] {\n const byProvider = new Map(\n items.flatMap((item) => (item.connect ? [[item.providerName, item] as const] : [])),\n );\n return [...byProvider.values()].map((item): Rendered => connectPrompt(item, item.connect));\n}\n\nfunction connectPrompt(item: DesignItem, connect: DesignConnect | undefined): Rendered {\n if (!connect) return <></>;\n if (connect.state === \"connected\") {\n return (\n <form key={item.providerName} method=\"post\" action={connect.disconnectUrl}>\n {csrfField()}\n <input type=\"hidden\" name=\"return\" value={connect.returnTo} />\n <HStack wrap={false}>\n <Meta as=\"span\">Previews use your {item.providerName} account.</Meta>\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Disconnect\n </Button>\n </HStack>\n </form>\n );\n }\n return (\n <Alert\n key={item.providerName}\n tone={connect.state === \"reconnect\" ? \"warning\" : \"info\"}\n title={\n connect.state === \"reconnect\"\n ? `Your ${item.providerName} connection expired`\n : `Connect ${item.providerName} for design previews`\n }\n >\n Sign in with your own {item.providerName} account to see names, previews and stale-design\n warnings. Only you will see what your account can open.{\" \"}\n <Button variant=\"secondary\" size=\"sm\" href={connect.startUrl}>\n {connect.state === \"reconnect\" ? \"Reconnect\" : \"Connect\"} {item.providerName}\n </Button>\n </Alert>\n );\n}\n\nfunction designNotice(item: DesignItem): Rendered | null {\n if (item.state === \"deleted\") {\n return (\n <Alert key={item.url} tone=\"warning\" title=\"Design not found\">\n The linked {item.providerName} file or node no longer exists. Update the link in the\n project's Design settings.\n </Alert>\n );\n }\n if (item.stale) {\n return (\n <Alert key={item.url} tone=\"warning\" title=\"Design changed since this baseline was approved\">\n {item.name ? `${item.name} was` : \"The linked design was\"} edited in {item.providerName}{\" \"}\n after the baseline. This does not block review.\n </Alert>\n );\n }\n if (item.state === \"unauthorized\" || item.state === \"forbidden\") {\n return (\n <Meta key={item.url} as=\"div\">\n {item.state === \"unauthorized\"\n ? `The project's ${item.providerName} credential is expired or revoked — design status is unavailable.`\n : `The project's ${item.providerName} credential cannot open this file — design status is unavailable.`}\n </Meta>\n );\n }\n return null;\n}\n\n/** Library header links to the project's design files. */\nexport function ProjectDesignLinks(props: { links: ProjectDesignLink[] }): Rendered {\n return (\n <HStack>\n {props.links.map((link): Rendered => (\n <Button\n key={link.url}\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {link.label}\n </Button>\n ))}\n </HStack>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/pages/environment-change.ts
|
|
5
|
+
/** What changed, in words a reviewer recognises. */
|
|
6
|
+
const FIELD_LABELS = {
|
|
7
|
+
engine: "browser engine",
|
|
8
|
+
browserVersion: "browser version",
|
|
9
|
+
platform: "host platform",
|
|
10
|
+
deviceScaleFactor: "device scale factor",
|
|
11
|
+
fontsHash: "installed fonts",
|
|
12
|
+
viewport: "viewport size",
|
|
13
|
+
sizing: "screenshot sizing",
|
|
14
|
+
unknown: "capture environment"
|
|
15
|
+
};
|
|
16
|
+
/** A snapshot's recorded change; null when it has none or the stored value is unreadable. */
|
|
17
|
+
function environmentChangeOf(snapshot) {
|
|
18
|
+
if (!snapshot.envChange) return null;
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(snapshot.envChange);
|
|
21
|
+
return Array.isArray(value.changed) && value.to ? value : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Summarise the build's environment migrations, or null when there were none. */
|
|
27
|
+
function summariseEnvironmentChanges(snapshots) {
|
|
28
|
+
const changes = snapshots.map((snapshot) => environmentChangeOf(snapshot)).filter((change) => change !== null);
|
|
29
|
+
if (changes.length === 0) return null;
|
|
30
|
+
const fields = [...new Set(changes.flatMap((change) => change.changed))].map((field) => labelOf(field));
|
|
31
|
+
return {
|
|
32
|
+
count: changes.length,
|
|
33
|
+
fields,
|
|
34
|
+
...browserVersionOf(changes)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Human label for a changed component. */
|
|
38
|
+
function labelOf(field) {
|
|
39
|
+
return FIELD_LABELS[field] ?? field;
|
|
40
|
+
}
|
|
41
|
+
function browserVersionOf(changes) {
|
|
42
|
+
const pairs = new Set(changes.map((change) => `${change.from?.browserVersion ?? ""}\u0000${change.to.browserVersion ?? ""}`));
|
|
43
|
+
if (pairs.size !== 1) return {};
|
|
44
|
+
const [from, to] = [...pairs][0]?.split("\0") ?? [];
|
|
45
|
+
return from && to && from !== to ? { browserVersion: {
|
|
46
|
+
from,
|
|
47
|
+
to
|
|
48
|
+
} } : {};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { environmentChangeOf, labelOf, summariseEnvironmentChanges };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=environment-change.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment-change.mjs","names":[],"sources":["../../src/pages/environment-change.ts"],"sourcesContent":["/** Reads the capture-environment migrations recorded on a build's snapshots. */\nimport type { EnvironmentChange } from \"@storyshelf/core/capture\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\n\n/** What changed, in words a reviewer recognises. */\nconst FIELD_LABELS: Record<string, string> = {\n engine: \"browser engine\",\n browserVersion: \"browser version\",\n platform: \"host platform\",\n deviceScaleFactor: \"device scale factor\",\n fontsHash: \"installed fonts\",\n viewport: \"viewport size\",\n sizing: \"screenshot sizing\",\n unknown: \"capture environment\",\n};\n\n/** A snapshot's recorded change; null when it has none or the stored value is unreadable. */\nexport function environmentChangeOf(\n snapshot: Pick<Snapshot, \"envChange\">,\n): EnvironmentChange | null {\n if (!snapshot.envChange) {\n return null;\n }\n try {\n const value = JSON.parse(snapshot.envChange) as Partial<EnvironmentChange>;\n return Array.isArray(value.changed) && value.to ? (value as EnvironmentChange) : null;\n } catch {\n return null;\n }\n}\n\n/** Summary across a build's snapshots. */\nexport interface EnvironmentChangeSummary {\n /** Snapshots accepted as a migration. */\n count: number;\n /** Distinct labels of what changed, in first-seen order. */\n fields: string[];\n /** `from -> to` for the browser version when every migrated snapshot agrees on it. */\n browserVersion?: { from: string; to: string };\n}\n\n/** Summarise the build's environment migrations, or null when there were none. */\nexport function summariseEnvironmentChanges(\n snapshots: readonly Pick<Snapshot, \"envChange\">[],\n): EnvironmentChangeSummary | null {\n const changes = snapshots\n .map((snapshot) => environmentChangeOf(snapshot))\n .filter((change): change is EnvironmentChange => change !== null);\n if (changes.length === 0) {\n return null;\n }\n const fields = [...new Set(changes.flatMap((change) => change.changed))].map((field) =>\n labelOf(field),\n );\n return { count: changes.length, fields, ...browserVersionOf(changes) };\n}\n\n/** Human label for a changed component. */\nexport function labelOf(field: string): string {\n return FIELD_LABELS[field] ?? field;\n}\n\nfunction browserVersionOf(\n changes: EnvironmentChange[],\n): Pick<EnvironmentChangeSummary, \"browserVersion\"> {\n const pairs = new Set(\n changes.map(\n (change) => `${change.from?.browserVersion ?? \"\"}\\u0000${change.to.browserVersion ?? \"\"}`,\n ),\n );\n if (pairs.size !== 1) {\n return {};\n }\n const [from, to] = [...pairs][0]?.split(\"\\u0000\") ?? [];\n return from && to && from !== to ? { browserVersion: { from, to } } : {};\n}\n"],"mappings":";;;;;AAKA,MAAM,eAAuC;CAC3C,QAAQ;CACR,gBAAgB;CAChB,UAAU;CACV,mBAAmB;CACnB,WAAW;CACX,UAAU;CACV,QAAQ;CACR,SAAS;AACX;;AAGA,SAAgB,oBACd,UAC0B;CAC1B,IAAI,CAAC,SAAS,WACZ,OAAO;CAET,IAAI;EACF,MAAM,QAAQ,KAAK,MAAM,SAAS,SAAS;EAC3C,OAAO,MAAM,QAAQ,MAAM,OAAO,KAAK,MAAM,KAAM,QAA8B;CACnF,QAAQ;EACN,OAAO;CACT;AACF;;AAaA,SAAgB,4BACd,WACiC;CACjC,MAAM,UAAU,UACb,KAAK,aAAa,oBAAoB,QAAQ,CAAC,CAAC,CAChD,QAAQ,WAAwC,WAAW,IAAI;CAClE,IAAI,QAAQ,WAAW,GACrB,OAAO;CAET,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,SAAS,WAAW,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,UAC5E,QAAQ,KAAK,CACf;CACA,OAAO;EAAE,OAAO,QAAQ;EAAQ;EAAQ,GAAG,iBAAiB,OAAO;CAAE;AACvE;;AAGA,SAAgB,QAAQ,OAAuB;CAC7C,OAAO,aAAa,UAAU;AAChC;AAEA,SAAS,iBACP,SACkD;CAClD,MAAM,QAAQ,IAAI,IAChB,QAAQ,KACL,WAAW,GAAG,OAAO,MAAM,kBAAkB,GAAG,QAAQ,OAAO,GAAG,kBAAkB,IACvF,CACF;CACA,IAAI,MAAM,SAAS,GACjB,OAAO,CAAC;CAEV,MAAM,CAAC,MAAM,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,IAAQ,KAAK,CAAC;CACtD,OAAO,QAAQ,MAAM,SAAS,KAAK,EAAE,gBAAgB;EAAE;EAAM;CAAG,EAAE,IAAI,CAAC;AACzE"}
|