@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.mjs","names":[],"sources":["../../src/pages/profile.tsx"],"sourcesContent":["import type { AuthUser } from \"@storyshelf/core/types\";\nimport { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n HStack,\n PageHeader,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { passkeyRegisterScript } from \"./passkey-ceremony.ts\";\n\nexport interface ProfilePageState {\n user: AuthUser;\n dbUser?: {\n email: string;\n name: string;\n displayNameOverride: string | null;\n authProvider: string;\n lastLoginAt: string | null;\n createdAt: string;\n };\n memberships: Array<{ projectName: string; projectSlug: string; role: string; source: string }>;\n subscribedSlugs: string[];\n error?: string;\n isLocal?: boolean;\n security?: {\n sessions: Array<{\n id: string;\n ipAddress: string | null;\n userAgent: string | null;\n createdAt: string;\n expiresAt: string;\n current: boolean;\n }>;\n passkeys: Array<{\n id: string;\n name: string | null;\n deviceType: string;\n backedUp: boolean;\n transports: string | null;\n aaguid: string | null;\n createdAt: string;\n }>;\n passkeysEnabled: boolean;\n hasPassword: boolean;\n };\n}\n\nconst profileAvatar = css`\n /* profile-avatar */\n width: 64px;\n height: 64px;\n border-radius: 999px;\n object-fit: cover;\n`;\n\nconst profileAvatarFallback = css`\n /* profile-avatar-fallback */\n width: 64px;\n height: 64px;\n display: grid;\n place-items: center;\n background: var(--accent);\n color: #fff;\n border-radius: 999px;\n font-size: 1.5rem;\n font-weight: 700;\n`;\n\nconst profileName = css`\n /* profile-name */\n font-weight: 700;\n font-size: 1.2rem;\n`;\n\nconst profileMeta = css`\n /* profile-meta */\n color: var(--text-secondary);\n font-size: 0.85rem;\n`;\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\ntype SecurityState = NonNullable<ProfilePageState[\"security\"]>;\ntype PasskeyState = SecurityState[\"passkeys\"][number];\n\n/** Registered passkey rows with per-key remove actions. */\nconst PasskeyTable: FC<{ passkeys: PasskeyState[] }> = ({ passkeys }) => {\n return (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Added</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {passkeys.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (key) => (\n <tr key={key.id}>\n <td>\n <div>{key.name ?? \"Unnamed key\"}</div>\n <div class={profileMeta}>{key.deviceType}</div>\n </td>\n <td>{new Date(key.createdAt).toLocaleDateString()}</td>\n <td>\n <HStack>\n {key.backedUp ? <Badge>Synced</Badge> : null}\n <form\n method=\"post\"\n action={`/profile/passkeys/${key.id}/delete`}\n data-confirm=\"Remove this passkey?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Remove\n </Button>\n </form>\n </HStack>\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n );\n};\n\ntype SessionInfo = SecurityState[\"sessions\"][number];\n\n/** One signed-in device with its revoke action. */\nconst SessionRow: FC<{ session: SessionInfo }> = ({ session }) => {\n return (\n <tr>\n <td>\n <div class=\"truncate\">{session.userAgent ?? \"Unknown device\"}</div>\n <div class={profileMeta}>\n <span class=\"mono\">{session.ipAddress ?? \"—\"}</span>\n </div>\n </td>\n <td>{new Date(session.createdAt).toLocaleString()}</td>\n <td>\n {session.current ? (\n <Badge>This device</Badge>\n ) : (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke\"\n data-confirm=\"Sign this device out?\"\n >\n <input type=\"hidden\" name=\"sessionId\" value={session.id} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Revoke\n </Button>\n </form>\n )}\n </td>\n </tr>\n );\n};\n\n/** Active device sessions with per-device revoke and sign-out-others. */\nconst SessionsSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Devices\" description=\"Where you are signed in\" />\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Device</th>\n <th>Signed in</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {security.sessions.map((session) => (\n <SessionRow key={session.id} session={session} />\n ))}\n </tbody>\n </table>\n </Table>\n {security.sessions.length > 1 ? (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke-others\"\n data-confirm=\"Sign out all other devices?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Sign out other devices\n </Button>\n </form>\n ) : null}\n </Card>\n );\n};\n\n/** Registered passkeys with delete, register button, and the backup nudge. */\nconst PasskeysSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Passkeys\" description=\"Sign in without a password using your device\" />\n {security.passkeys.length < 2 ? (\n <Alert title=\"Add a backup key\">\n Register a 2nd key so a lost device doesn't lock you out.\n </Alert>\n ) : null}\n {security.passkeys.length === 0 ? (\n <p class={profileMeta}>No passkeys yet.</p>\n ) : (\n <PasskeyTable passkeys={security.passkeys} />\n )}\n <HStack>\n <Field label=\"Key name\" name=\"keyName\" type=\"text\" placeholder=\"e.g. Security key\" />\n <Button variant=\"primary\" size=\"sm\" type=\"button\" data-passkey-register=\"true\">\n Register a passkey\n </Button>\n </HStack>\n <p class={profileMeta} data-passkey-status=\"true\" aria-live=\"polite\">\n {\"\"}\n </p>\n <script dangerouslySetInnerHTML={{ __html: passkeyRegisterScript() }} />\n </Card>\n );\n};\n\n/** Personal profile page (editable display name, password for local accounts). */\n// oxlint-disable-next-line eslint/max-lines-per-function -- profile page composes several sections, cohesive\nexport function renderProfilePage(state: ProfilePageState): RenderedContent {\n const { user, dbUser, memberships, subscribedSlugs, error, isLocal, security } = state;\n const avatar = safeImageUrl(user.avatarUrl);\n const displayName = dbUser?.displayNameOverride ?? user.name;\n const provider = dbUser?.authProvider ?? user.providerId ?? \"oidc\";\n return (\n <DocumentLayout title=\"Profile\" nav={{ active: \"profile\" }}>\n <PageHeader title=\"Profile\" description=\"Your account and project memberships\" />\n {error ? (\n <Alert tone=\"danger\" title=\"Could not save\">\n {error}\n </Alert>\n ) : null}\n\n <Card>\n <VStack>\n <HStack>\n {avatar ? (\n <img class={profileAvatar} src={avatar} alt=\"\" width=\"64\" height=\"64\" />\n ) : (\n <span class={profileAvatarFallback} aria-hidden=\"true\">\n {user.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <VStack>\n <div class={profileName}>{displayName}</div>\n <div class={profileMeta}>{user.email}</div>\n <HStack>\n <Badge>{user.role}</Badge>\n <Badge>via {provider}</Badge>\n </HStack>\n </VStack>\n </HStack>\n <div class={profileMeta}>\n Member since {dbUser?.createdAt ? new Date(dbUser.createdAt).toLocaleDateString() : \"—\"}{\" \"}\n · Last login {dbUser?.lastLoginAt ? new Date(dbUser.lastLoginAt).toLocaleString() : \"—\"}\n </div>\n </VStack>\n </Card>\n\n <Card>\n <PageHeader\n title=\"Display name\"\n description=\"Shown in comments and the header. Survives SSO refresh.\"\n />\n <form method=\"post\" action=\"/profile\" novalidate>\n <Field label=\"Display name\" name=\"displayName\" type=\"text\" required value={displayName} />\n <Button variant=\"primary\" type=\"submit\">\n Save name\n </Button>\n </form>\n </Card>\n\n {isLocal ? (\n <Card>\n <PageHeader title=\"Change password\" description=\"For local accounts only\" />\n <form method=\"post\" action=\"/profile/password\" novalidate>\n <Field\n label=\"Current password\"\n name=\"currentPassword\"\n type=\"password\"\n required\n autocomplete=\"current-password\"\n />\n <Field\n label=\"New password\"\n name=\"newPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Field\n label=\"Confirm new password\"\n name=\"confirmPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Button variant=\"primary\" type=\"submit\">\n Change password\n </Button>\n </form>\n </Card>\n ) : null}\n\n {security ? <SessionsSection security={security} /> : null}\n\n {security?.passkeysEnabled ? <PasskeysSection security={security} /> : null}\n\n <Card>\n <PageHeader title=\"Project memberships\" description=\"Your role on each project\" />{\" \"}\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Role</th>\n <th>Source</th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>\n <Badge>{m.role}</Badge>\n </td>\n <td>{m.source}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <PageHeader\n title=\"Notifications\"\n description=\"Email alerts per project. Fine-tune topics in each project's settings.\"\n />\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Status</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => {\n const subscribed = subscribedSlugs.includes(m.projectSlug);\n return (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>{subscribed ? <Badge>Email on</Badge> : <Badge>Off</Badge>}</td>\n <td>\n <form method=\"post\" action=\"/profile/notifications\">\n {csrfField()}\n <input type=\"hidden\" name=\"slug\" value={m.projectSlug} />\n <input type=\"hidden\" name=\"enabled\" value={subscribed ? \"\" : \"1\"} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n {subscribed ? \"Disable\" : \"Enable\"}\n </Button>\n </form>\n </td>\n </tr>\n );\n },\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <form method=\"post\" action=\"/auth/logout\">\n {csrfField()}\n <Button variant=\"secondary\" type=\"submit\">\n Sign out\n </Button>\n </form>\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"profile.mjs","names":[],"sources":["../../src/pages/profile.tsx"],"sourcesContent":["import type { AuthUser } from \"@storyshelf/core/types\";\nimport { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n Field,\n HStack,\n PageHeader,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { NotificationsPausedNotice } from \"./notifications-paused.tsx\";\nimport { passkeyRegisterScript } from \"./passkey-ceremony.ts\";\n\nexport interface ProfilePageState {\n user: AuthUser;\n dbUser?: {\n email: string;\n name: string;\n displayNameOverride: string | null;\n authProvider: string;\n lastLoginAt: string | null;\n createdAt: string;\n };\n memberships: Array<{ projectName: string; projectSlug: string; role: string; source: string }>;\n subscribedSlugs: string[];\n error?: string;\n /** Notifiers are wired but no public origin resolves (ADR 0014, decision 11). */\n notificationsPaused?: boolean | undefined;\n isLocal?: boolean;\n security?: {\n sessions: Array<{\n id: string;\n ipAddress: string | null;\n userAgent: string | null;\n createdAt: string;\n expiresAt: string;\n current: boolean;\n }>;\n passkeys: Array<{\n id: string;\n name: string | null;\n deviceType: string;\n backedUp: boolean;\n transports: string | null;\n aaguid: string | null;\n createdAt: string;\n }>;\n passkeysEnabled: boolean;\n hasPassword: boolean;\n };\n}\n\nconst profileAvatar = css`\n /* profile-avatar */\n width: 64px;\n height: 64px;\n border-radius: 999px;\n object-fit: cover;\n`;\n\nconst profileAvatarFallback = css`\n /* profile-avatar-fallback */\n width: 64px;\n height: 64px;\n display: grid;\n place-items: center;\n background: var(--accent);\n color: #fff;\n border-radius: 999px;\n font-size: 1.5rem;\n font-weight: 700;\n`;\n\nconst profileName = css`\n /* profile-name */\n font-weight: 700;\n font-size: 1.2rem;\n`;\n\nconst profileMeta = css`\n /* profile-meta */\n color: var(--text-secondary);\n font-size: 0.85rem;\n`;\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\ntype SecurityState = NonNullable<ProfilePageState[\"security\"]>;\ntype PasskeyState = SecurityState[\"passkeys\"][number];\n\n/** Registered passkey rows with per-key remove actions. */\nconst PasskeyTable: FC<{ passkeys: PasskeyState[] }> = ({ passkeys }) => {\n return (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Key</th>\n <th>Added</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {passkeys.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (key) => (\n <tr key={key.id}>\n <td>\n <div>{key.name ?? \"Unnamed key\"}</div>\n <div class={profileMeta}>{key.deviceType}</div>\n </td>\n <td>{new Date(key.createdAt).toLocaleDateString()}</td>\n <td>\n <HStack>\n {key.backedUp ? <Badge>Synced</Badge> : null}\n <form\n method=\"post\"\n action={`/profile/passkeys/${key.id}/delete`}\n data-confirm=\"Remove this passkey?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Remove\n </Button>\n </form>\n </HStack>\n </td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n );\n};\n\ntype SessionInfo = SecurityState[\"sessions\"][number];\n\n/** One signed-in device with its revoke action. */\nconst SessionRow: FC<{ session: SessionInfo }> = ({ session }) => {\n return (\n <tr>\n <td>\n <div class=\"truncate\">{session.userAgent ?? \"Unknown device\"}</div>\n <div class={profileMeta}>\n <span class=\"mono\">{session.ipAddress ?? \"—\"}</span>\n </div>\n </td>\n <td>{new Date(session.createdAt).toLocaleString()}</td>\n <td>\n {session.current ? (\n <Badge>This device</Badge>\n ) : (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke\"\n data-confirm=\"Sign this device out?\"\n >\n <input type=\"hidden\" name=\"sessionId\" value={session.id} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Revoke\n </Button>\n </form>\n )}\n </td>\n </tr>\n );\n};\n\n/** Active device sessions with per-device revoke and sign-out-others. */\nconst SessionsSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Devices\" description=\"Where you are signed in\" />\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Device</th>\n <th>Signed in</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n {security.sessions.map((session) => (\n <SessionRow key={session.id} session={session} />\n ))}\n </tbody>\n </table>\n </Table>\n {security.sessions.length > 1 ? (\n <form\n method=\"post\"\n action=\"/profile/sessions/revoke-others\"\n data-confirm=\"Sign out all other devices?\"\n >\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n Sign out other devices\n </Button>\n </form>\n ) : null}\n </Card>\n );\n};\n\n/** Registered passkeys with delete, register button, and the backup nudge. */\nconst PasskeysSection: FC<{ security: SecurityState }> = ({ security }) => {\n return (\n <Card>\n <PageHeader title=\"Passkeys\" description=\"Sign in without a password using your device\" />\n {security.passkeys.length < 2 ? (\n <Alert title=\"Add a backup key\">\n Register a 2nd key so a lost device doesn't lock you out.\n </Alert>\n ) : null}\n {security.passkeys.length === 0 ? (\n <p class={profileMeta}>No passkeys yet.</p>\n ) : (\n <PasskeyTable passkeys={security.passkeys} />\n )}\n <HStack>\n <Field label=\"Key name\" name=\"keyName\" type=\"text\" placeholder=\"e.g. Security key\" />\n <Button variant=\"primary\" size=\"sm\" type=\"button\" data-passkey-register=\"true\">\n Register a passkey\n </Button>\n </HStack>\n <p class={profileMeta} data-passkey-status=\"true\" aria-live=\"polite\">\n {\"\"}\n </p>\n <script dangerouslySetInnerHTML={{ __html: passkeyRegisterScript() }} />\n </Card>\n );\n};\n\n/** Personal profile page (editable display name, password for local accounts). */\n// oxlint-disable-next-line eslint/max-lines-per-function -- profile page composes several sections, cohesive\nexport function renderProfilePage(state: ProfilePageState): RenderedContent {\n const { user, dbUser, memberships, subscribedSlugs, error, isLocal, security } = state;\n const avatar = safeImageUrl(user.avatarUrl);\n const displayName = dbUser?.displayNameOverride ?? user.name;\n const provider = dbUser?.authProvider ?? user.providerId ?? \"oidc\";\n return (\n <DocumentLayout title=\"Profile\" nav={{ active: \"profile\" }}>\n <PageHeader title=\"Profile\" description=\"Your account and project memberships\" />\n {error ? (\n <Alert tone=\"danger\" title=\"Could not save\">\n {error}\n </Alert>\n ) : null}\n\n <Card>\n <VStack>\n <HStack>\n {avatar ? (\n <img class={profileAvatar} src={avatar} alt=\"\" width=\"64\" height=\"64\" />\n ) : (\n <span class={profileAvatarFallback} aria-hidden=\"true\">\n {user.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <VStack>\n <div class={profileName}>{displayName}</div>\n <div class={profileMeta}>{user.email}</div>\n <HStack>\n <Badge>{user.role}</Badge>\n <Badge>via {provider}</Badge>\n </HStack>\n </VStack>\n </HStack>\n <div class={profileMeta}>\n Member since {dbUser?.createdAt ? new Date(dbUser.createdAt).toLocaleDateString() : \"—\"}{\" \"}\n · Last login {dbUser?.lastLoginAt ? new Date(dbUser.lastLoginAt).toLocaleString() : \"—\"}\n </div>\n </VStack>\n </Card>\n\n <Card>\n <PageHeader\n title=\"Display name\"\n description=\"Shown in comments and the header. Survives SSO refresh.\"\n />\n <form method=\"post\" action=\"/profile\" novalidate>\n <Field label=\"Display name\" name=\"displayName\" type=\"text\" required value={displayName} />\n <Button variant=\"primary\" type=\"submit\">\n Save name\n </Button>\n </form>\n </Card>\n\n {isLocal ? (\n <Card>\n <PageHeader title=\"Change password\" description=\"For local accounts only\" />\n <form method=\"post\" action=\"/profile/password\" novalidate>\n <Field\n label=\"Current password\"\n name=\"currentPassword\"\n type=\"password\"\n required\n autocomplete=\"current-password\"\n />\n <Field\n label=\"New password\"\n name=\"newPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Field\n label=\"Confirm new password\"\n name=\"confirmPassword\"\n type=\"password\"\n required\n autocomplete=\"new-password\"\n />\n <Button variant=\"primary\" type=\"submit\">\n Change password\n </Button>\n </form>\n </Card>\n ) : null}\n\n {security ? <SessionsSection security={security} /> : null}\n\n {security?.passkeysEnabled ? <PasskeysSection security={security} /> : null}\n\n <Card>\n <PageHeader title=\"Project memberships\" description=\"Your role on each project\" />{\" \"}\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Role</th>\n <th>Source</th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>\n <Badge>{m.role}</Badge>\n </td>\n <td>{m.source}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <PageHeader\n title=\"Notifications\"\n description=\"Email alerts per project. Fine-tune topics in each project's settings.\"\n />\n <NotificationsPausedNotice paused={state.notificationsPaused} />\n {memberships.length === 0 ? (\n <p class={profileMeta}>No project memberships.</p>\n ) : (\n <Table dense>\n <table>\n <thead>\n <tr>\n <th>Project</th>\n <th>Status</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {memberships.map(\n // oxlint-disable-next-line typescript/promise-function-async -- JSX map is sync\n (m) => {\n const subscribed = subscribedSlugs.includes(m.projectSlug);\n return (\n <tr key={m.projectSlug}>\n <td>\n <a href={`/projects/${m.projectSlug}`}>{m.projectName}</a>\n </td>\n <td>{subscribed ? <Badge>Email on</Badge> : <Badge>Off</Badge>}</td>\n <td>\n <form method=\"post\" action=\"/profile/notifications\">\n {csrfField()}\n <input type=\"hidden\" name=\"slug\" value={m.projectSlug} />\n <input type=\"hidden\" name=\"enabled\" value={subscribed ? \"\" : \"1\"} />\n <Button variant=\"secondary\" size=\"sm\" type=\"submit\">\n {subscribed ? \"Disable\" : \"Enable\"}\n </Button>\n </form>\n </td>\n </tr>\n );\n },\n )}\n </tbody>\n </table>\n </Table>\n )}\n </Card>\n\n <Card>\n <form method=\"post\" action=\"/auth/logout\">\n {csrfField()}\n <Button variant=\"secondary\" type=\"submit\">\n Sign out\n </Button>\n </form>\n </Card>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { getStore } from "../store.mjs";
|
|
4
|
+
import { getStore, testingEnabled } from "../store.mjs";
|
|
5
5
|
import { css } from "../ui/css.mjs";
|
|
6
6
|
import { Button } from "../ui/buttons.mjs";
|
|
7
7
|
import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
|
|
@@ -71,7 +71,7 @@ function BuildSummaryCell({ project, build }) {
|
|
|
71
71
|
}
|
|
72
72
|
function BuildActions({ project, build }) {
|
|
73
73
|
const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
|
|
74
|
-
const needsReview = build.status === "reviewing";
|
|
74
|
+
const needsReview = build.status === "reviewing" && testingEnabled();
|
|
75
75
|
return /* @__PURE__ */ jsxs(HStack, {
|
|
76
76
|
wrap: false,
|
|
77
77
|
justify: "end",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-builds-table.mjs","names":[],"sources":["../../src/pages/project-builds-table.tsx"],"sourcesContent":["import type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore } from \"../store.ts\";\nimport {\n Avatar,\n Badge,\n Button,\n HStack,\n Meta,\n RelativeTime,\n Table,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst buildCell = css`\n /* build-cell */\n display: grid;\n gap: 2px;\n min-width: 0;\n max-width: 38ch;\n & strong a {\n color: var(--text-primary);\n }\n & strong a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst messageLine = css`\n /* build-message */\n overflow: hidden;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n text-overflow: ellipsis;\n white-space: nowrap;\n`;\n\nconst authorCell = css`\n /* build-author */\n display: flex;\n gap: var(--space-2);\n align-items: center;\n white-space: nowrap;\n`;\n\n/** \"4 to review · 1 approved · 12 snapshots\", leaving out zero counts. */\nfunction changeSummary(build: Build): string {\n const parts: string[] = [];\n if (build.changedCount > 0) {\n parts.push(`${build.changedCount} to review`);\n }\n if (build.approvedCount > 0) {\n parts.push(`${build.approvedCount} approved`);\n }\n parts.push(`${build.snapshotCount} ${build.snapshotCount === 1 ? \"snapshot\" : \"snapshots\"}`);\n return parts.join(\" · \");\n}\n\nfunction BuildSummaryCell({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <div class={buildCell}>\n <strong>\n <a href={urls.build(project.slug, build.id)}>{build.gitBranch}</a>{\" \"}\n <Meta as=\"span\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n </strong>\n <span class={messageLine}>{build.message ?? \"No message\"}</span>\n </div>\n );\n}\n\nfunction BuildActions({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const needsReview = build.status === \"reviewing\";\n return (\n <HStack wrap={false} justify=\"end\">\n <Button\n variant={needsReview ? \"primary\" : \"secondary\"}\n size=\"sm\"\n href={\n needsReview ? urls.buildDiff(project.slug, build.id) : urls.build(project.slug, build.id)\n }\n >\n {needsReview ? \"Review\" : \"View\"}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={urls.short(build.id)}\n aria-label=\"View Storybook\"\n title=\"View Storybook\"\n />\n </HStack>\n );\n}\n\nfunction BuildRow({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <tr>\n <td>\n <BuildSummaryCell project={project} build={build} />\n </td>\n <td>\n <Badge tone={statusTone(build.status)} icon>\n {build.status}\n </Badge>\n </td>\n <td>\n <Meta as=\"span\">{changeSummary(build)}</Meta>\n </td>\n <td>\n <div class={authorCell}>\n {build.authorName ? <Avatar name={build.authorName} size=\"sm\" /> : null}\n <span>{build.authorName ?? \"—\"}</span>\n </div>\n </td>\n <td class=\"nowrap\">\n <Meta as=\"span\">\n <RelativeTime value={build.createdAt} />\n </Meta>\n </td>\n <td class=\"nowrap\">\n <BuildActions project={project} build={build} />\n </td>\n </tr>\n );\n}\n\n/** Builds as a table: branch/sha, status, change summary, author, age, actions. */\nexport function BuildsTable({\n project,\n builds,\n}: {\n project: Project;\n builds: Build[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Changes</th>\n <th>Author</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {builds.map((build) => (\n <BuildRow key={build.id} project={project} build={build} />\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAM,YAAY,GAAG;;;;;;;;;;;;;AAcrB,MAAM,cAAc,GAAG;;;;;;;;AASvB,MAAM,aAAa,GAAG;;;;;;;;AAStB,SAAS,cAAc,OAAsB;CAC3C,MAAM,QAAkB,CAAC;CACzB,IAAI,MAAM,eAAe,GACvB,MAAM,KAAK,GAAG,MAAM,aAAa,WAAW;CAE9C,IAAI,MAAM,gBAAgB,GACxB,MAAM,KAAK,GAAG,MAAM,cAAc,UAAU;CAE9C,MAAM,KAAK,GAAG,MAAM,cAAc,GAAG,MAAM,kBAAkB,IAAI,aAAa,aAAa;CAC3F,OAAO,MAAM,KAAK,KAAK;AACzB;AAEA,SAAS,iBAAiB,EACxB,SACA,SAIiD;CACjD,MAAM,OAAO,iBAAiB,KAAK,SAAS,CAAC,CAAC,OAAO,mBAAmB;CACxE,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,qBAAC,UAAD,EAAA,UAAA;GACE,oBAAC,KAAD;IAAG,MAAM,KAAK,MAAM,QAAQ,MAAM,MAAM,EAAE;IAAI,UAAA,MAAM;GAAa,CAAA;GAAE;GACnE,oBAAC,MAAD;IAAM,IAAG;IAAO,MAAA;IACb,UAAA,MAAM,OAAO,MAAM,GAAG,CAAC;GACpB,CAAA;EACA,EAAA,CAAA,GACR,oBAAC,QAAD;GAAM,OAAO;GAAc,UAAA,MAAM,WAAW;EAAmB,CAAA,CAC5D;;AAET;AAEA,SAAS,aAAa,EACpB,SACA,SAIiD;CACjD,MAAM,OAAO,iBAAiB,KAAK,SAAS,CAAC,CAAC,OAAO,mBAAmB;
|
|
1
|
+
{"version":3,"file":"project-builds-table.mjs","names":[],"sources":["../../src/pages/project-builds-table.tsx"],"sourcesContent":["import type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport {\n Avatar,\n Badge,\n Button,\n HStack,\n Meta,\n RelativeTime,\n Table,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst buildCell = css`\n /* build-cell */\n display: grid;\n gap: 2px;\n min-width: 0;\n max-width: 38ch;\n & strong a {\n color: var(--text-primary);\n }\n & strong a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst messageLine = css`\n /* build-message */\n overflow: hidden;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n text-overflow: ellipsis;\n white-space: nowrap;\n`;\n\nconst authorCell = css`\n /* build-author */\n display: flex;\n gap: var(--space-2);\n align-items: center;\n white-space: nowrap;\n`;\n\n/** \"4 to review · 1 approved · 12 snapshots\", leaving out zero counts. */\nfunction changeSummary(build: Build): string {\n const parts: string[] = [];\n if (build.changedCount > 0) {\n parts.push(`${build.changedCount} to review`);\n }\n if (build.approvedCount > 0) {\n parts.push(`${build.approvedCount} approved`);\n }\n parts.push(`${build.snapshotCount} ${build.snapshotCount === 1 ? \"snapshot\" : \"snapshots\"}`);\n return parts.join(\" · \");\n}\n\nfunction BuildSummaryCell({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n return (\n <div class={buildCell}>\n <strong>\n <a href={urls.build(project.slug, build.id)}>{build.gitBranch}</a>{\" \"}\n <Meta as=\"span\" mono>\n {build.gitSha.slice(0, 7)}\n </Meta>\n </strong>\n <span class={messageLine}>{build.message ?? \"No message\"}</span>\n </div>\n );\n}\n\nfunction BuildActions({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n // The diff page is a testing route; hosting-only instances link to the build page.\n const needsReview = build.status === \"reviewing\" && testingEnabled();\n return (\n <HStack wrap={false} justify=\"end\">\n <Button\n variant={needsReview ? \"primary\" : \"secondary\"}\n size=\"sm\"\n href={\n needsReview ? urls.buildDiff(project.slug, build.id) : urls.build(project.slug, build.id)\n }\n >\n {needsReview ? \"Review\" : \"View\"}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon=\"external-link\"\n href={urls.short(build.id)}\n aria-label=\"View Storybook\"\n title=\"View Storybook\"\n />\n </HStack>\n );\n}\n\nfunction BuildRow({\n project,\n build,\n}: {\n project: Project;\n build: Build;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <tr>\n <td>\n <BuildSummaryCell project={project} build={build} />\n </td>\n <td>\n <Badge tone={statusTone(build.status)} icon>\n {build.status}\n </Badge>\n </td>\n <td>\n <Meta as=\"span\">{changeSummary(build)}</Meta>\n </td>\n <td>\n <div class={authorCell}>\n {build.authorName ? <Avatar name={build.authorName} size=\"sm\" /> : null}\n <span>{build.authorName ?? \"—\"}</span>\n </div>\n </td>\n <td class=\"nowrap\">\n <Meta as=\"span\">\n <RelativeTime value={build.createdAt} />\n </Meta>\n </td>\n <td class=\"nowrap\">\n <BuildActions project={project} build={build} />\n </td>\n </tr>\n );\n}\n\n/** Builds as a table: branch/sha, status, change summary, author, age, actions. */\nexport function BuildsTable({\n project,\n builds,\n}: {\n project: Project;\n builds: Build[];\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n return (\n <Table>\n <table>\n <thead>\n <tr>\n <th>Build</th>\n <th>Status</th>\n <th>Changes</th>\n <th>Author</th>\n <th>Created</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {builds.map((build) => (\n <BuildRow key={build.id} project={project} build={build} />\n ))}\n </tbody>\n </table>\n </Table>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { getStore } from "../store.mjs";
|
|
4
|
+
import { getStore, testingEnabled } from "../store.mjs";
|
|
5
5
|
import { Button } from "../ui/buttons.mjs";
|
|
6
|
-
import { Badge, EmptyState } from "../ui/feedback.mjs";
|
|
6
|
+
import { Badge, EmptyState, Meta, Stat } from "../ui/feedback.mjs";
|
|
7
7
|
import { SelectField } from "../ui/forms.mjs";
|
|
8
|
-
import { PageHeader } from "../ui/layout.mjs";
|
|
8
|
+
import { Card, PageHeader } from "../ui/layout.mjs";
|
|
9
9
|
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
10
10
|
import { Segmented } from "../ui/segmented.mjs";
|
|
11
11
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
12
|
+
import { loadHealthPanel } from "../insights/health-panel.mjs";
|
|
13
|
+
import { HealthSlot } from "./health-panel.mjs";
|
|
12
14
|
import { BuildsTable } from "./project-builds-table.mjs";
|
|
13
|
-
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
15
|
+
import { BuildModel, ProjectModel, averageInheritedRatio } from "@storyshelf/core/models";
|
|
14
16
|
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
15
17
|
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
16
18
|
//#region src/pages/project-builds.tsx
|
|
@@ -19,6 +21,7 @@ const STATUSES = [
|
|
|
19
21
|
"approved",
|
|
20
22
|
"rejected",
|
|
21
23
|
"failed",
|
|
24
|
+
"cancelled",
|
|
22
25
|
"pending"
|
|
23
26
|
];
|
|
24
27
|
function listHref(base, filters) {
|
|
@@ -86,6 +89,24 @@ function BuildsToolbar(props) {
|
|
|
86
89
|
})]
|
|
87
90
|
});
|
|
88
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Affected-capture efficiency: the average share of a snapshot set that recent builds inherited
|
|
94
|
+
* from the baseline instead of rendering, from the counts stored on the builds (no snapshot reads).
|
|
95
|
+
*/
|
|
96
|
+
function InheritedRatioCard(props) {
|
|
97
|
+
const average = averageInheritedRatio(props.builds);
|
|
98
|
+
if (average === null) return null;
|
|
99
|
+
return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(Stat, {
|
|
100
|
+
label: "Inherited from baseline",
|
|
101
|
+
value: `${Math.round(average.ratio * 100)}%`
|
|
102
|
+
}), /* @__PURE__ */ jsxs(Meta, { children: [
|
|
103
|
+
"Average over the last ",
|
|
104
|
+
average.builds,
|
|
105
|
+
" ",
|
|
106
|
+
average.builds === 1 ? "build" : "builds",
|
|
107
|
+
": the share of snapshots reused without rendering."
|
|
108
|
+
] })] });
|
|
109
|
+
}
|
|
89
110
|
/** Project builds page: filterable build history for one project. */
|
|
90
111
|
async function renderProjectBuildsPage(slug, query = {}) {
|
|
91
112
|
const project = (await new ProjectModel(getStore().db).list()).find((item) => item.slug === slug);
|
|
@@ -94,6 +115,7 @@ async function renderProjectBuildsPage(slug, query = {}) {
|
|
|
94
115
|
const builds = all.filter((build) => matches(build, query));
|
|
95
116
|
const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
|
|
96
117
|
const filtered = Boolean(query.status ?? query.branch);
|
|
118
|
+
const health = await loadHealthPanel(project);
|
|
97
119
|
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
98
120
|
title: project.name,
|
|
99
121
|
nav: {
|
|
@@ -118,23 +140,31 @@ async function renderProjectBuildsPage(slug, query = {}) {
|
|
|
118
140
|
href: urls.settings(project.slug),
|
|
119
141
|
children: "Settings"
|
|
120
142
|
})
|
|
121
|
-
}), /* @__PURE__ */ jsxs(VStack, { children: [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
}), /* @__PURE__ */ jsxs(VStack, { children: [
|
|
144
|
+
/* @__PURE__ */ jsx(HealthSlot, {
|
|
145
|
+
project,
|
|
146
|
+
data: health
|
|
147
|
+
}),
|
|
148
|
+
testingEnabled() ? /* @__PURE__ */ jsx(InheritedRatioCard, { builds: all }) : null,
|
|
149
|
+
/* @__PURE__ */ jsx(BuildsToolbar, {
|
|
150
|
+
project,
|
|
151
|
+
all,
|
|
152
|
+
filters: query,
|
|
153
|
+
base: urls.buildsList(project.slug)
|
|
154
|
+
}),
|
|
155
|
+
builds.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
156
|
+
title: "No builds",
|
|
157
|
+
description: filtered ? "No builds match the current filter." : "Upload your first build with the CLI. Builds appear here once uploaded.",
|
|
158
|
+
action: filtered ? /* @__PURE__ */ jsx(Button, {
|
|
159
|
+
variant: "secondary",
|
|
160
|
+
href: urls.buildsList(project.slug),
|
|
161
|
+
children: "Clear filters"
|
|
162
|
+
}) : null
|
|
163
|
+
}) : /* @__PURE__ */ jsx(BuildsTable, {
|
|
164
|
+
project,
|
|
165
|
+
builds
|
|
166
|
+
})
|
|
167
|
+
] })]
|
|
138
168
|
});
|
|
139
169
|
}
|
|
140
170
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-builds.mjs","names":[],"sources":["../../src/pages/project-builds.tsx"],"sourcesContent":["import { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n EmptyState,\n HStack,\n PageHeader,\n Segmented,\n SelectField,\n VStack,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { BuildsTable } from \"./project-builds-table.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst STATUSES = [\"reviewing\", \"approved\", \"rejected\", \"failed\", \"pending\"] as const;\n\n/** Current list filters (from the query string). */\ninterface BuildFilters {\n status?: string;\n branch?: string;\n}\n\nfunction listHref(base: string, filters: BuildFilters): string {\n const params = new URLSearchParams();\n if (filters.status) {\n params.set(\"status\", filters.status);\n }\n if (filters.branch) {\n params.set(\"branch\", filters.branch);\n }\n const query = params.toString();\n return query ? `${base}?${query}` : base;\n}\n\nfunction matches(build: Build, filters: BuildFilters): boolean {\n return (\n (!filters.status || build.status === filters.status) &&\n (!filters.branch || build.gitBranch === filters.branch)\n );\n}\n\n/** Status chips (with counts) plus a branch select that applies instantly. */\nfunction BuildsToolbar(props: {\n project: Project;\n all: Build[];\n filters: BuildFilters;\n base: string;\n}): ReturnType<typeof HStack> {\n const { all, filters, base } = props;\n const branches = [...new Set(all.map((build) => build.gitBranch))].toSorted();\n const inBranch = all.filter((build) => !filters.branch || build.gitBranch === filters.branch);\n const count = (status: string): number =>\n inBranch.filter((build) => build.status === status).length;\n const items = [\n { label: `All ${inBranch.length}`, value: \"\", status: undefined },\n ...STATUSES.filter((status) => count(status) > 0 || filters.status === status).map(\n (status) => ({ label: `${status} ${count(status)}`, value: status, status }),\n ),\n ];\n return (\n <HStack justify=\"between\">\n <Segmented\n label=\"Filter by status\"\n items={items.map((item) => ({\n label: item.label,\n value: item.value,\n href: listHref(base, { ...filters, status: item.status }),\n active: (filters.status ?? \"\") === item.value,\n }))}\n />\n <form\n method=\"get\"\n action={base}\n hx-get={base}\n hx-trigger=\"change\"\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n {filters.status ? <input type=\"hidden\" name=\"status\" value={filters.status} /> : null}\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={filters.branch ?? \"\"}\n options={[\n { value: \"\", label: \"All branches\" },\n ...branches.map((branch) => ({ value: branch, label: branch })),\n ]}\n />\n </form>\n </HStack>\n );\n}\n\n/** Project builds page: filterable build history for one project. */\nexport async function renderProjectBuildsPage(\n slug: string,\n query: BuildFilters = {},\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 all = await new BuildModel(getStore().db).list(project.id);\n const builds = all.filter((build) => matches(build, query));\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const filtered = Boolean(query.status ?? query.branch);\n\n return (\n <DocumentLayout\n title={project.name}\n nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Builds\"\n description={\n <>\n {project.gitRepository ?? project.slug} · default branch{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </>\n }\n actions={\n <Button variant=\"secondary\" icon=\"settings\" href={urls.settings(project.slug)}>\n Settings\n </Button>\n }\n />\n <VStack>\n <BuildsToolbar\n project={project}\n all={all}\n filters={query}\n base={urls.buildsList(project.slug)}\n />\n {builds.length === 0 ? (\n <EmptyState\n title=\"No builds\"\n description={\n filtered\n ? \"No builds match the current filter.\"\n : \"Upload your first build with the CLI. Builds appear here once uploaded.\"\n }\n action={\n filtered ? (\n <Button variant=\"secondary\" href={urls.buildsList(project.slug)}>\n Clear filters\n </Button>\n ) : null\n }\n />\n ) : (\n <BuildsTable project={project} builds={builds} />\n )}\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"project-builds.mjs","names":[],"sources":["../../src/pages/project-builds.tsx"],"sourcesContent":["import { BuildModel, ProjectModel, averageInheritedRatio } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport { loadHealthPanel } from \"../insights/health-panel.ts\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n Segmented,\n SelectField,\n Stat,\n VStack,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { HealthSlot } from \"./health-panel.tsx\";\nimport { BuildsTable } from \"./project-builds-table.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst STATUSES = [\"reviewing\", \"approved\", \"rejected\", \"failed\", \"cancelled\", \"pending\"] as const;\n\n/** Current list filters (from the query string). */\ninterface BuildFilters {\n status?: string;\n branch?: string;\n}\n\nfunction listHref(base: string, filters: BuildFilters): string {\n const params = new URLSearchParams();\n if (filters.status) {\n params.set(\"status\", filters.status);\n }\n if (filters.branch) {\n params.set(\"branch\", filters.branch);\n }\n const query = params.toString();\n return query ? `${base}?${query}` : base;\n}\n\nfunction matches(build: Build, filters: BuildFilters): boolean {\n return (\n (!filters.status || build.status === filters.status) &&\n (!filters.branch || build.gitBranch === filters.branch)\n );\n}\n\n/** Status chips (with counts) plus a branch select that applies instantly. */\nfunction BuildsToolbar(props: {\n project: Project;\n all: Build[];\n filters: BuildFilters;\n base: string;\n}): ReturnType<typeof HStack> {\n const { all, filters, base } = props;\n const branches = [...new Set(all.map((build) => build.gitBranch))].toSorted();\n const inBranch = all.filter((build) => !filters.branch || build.gitBranch === filters.branch);\n const count = (status: string): number =>\n inBranch.filter((build) => build.status === status).length;\n const items = [\n { label: `All ${inBranch.length}`, value: \"\", status: undefined },\n ...STATUSES.filter((status) => count(status) > 0 || filters.status === status).map(\n (status) => ({ label: `${status} ${count(status)}`, value: status, status }),\n ),\n ];\n return (\n <HStack justify=\"between\">\n <Segmented\n label=\"Filter by status\"\n items={items.map((item) => ({\n label: item.label,\n value: item.value,\n href: listHref(base, { ...filters, status: item.status }),\n active: (filters.status ?? \"\") === item.value,\n }))}\n />\n <form\n method=\"get\"\n action={base}\n hx-get={base}\n hx-trigger=\"change\"\n hx-target=\"body\"\n hx-push-url=\"true\"\n >\n {filters.status ? <input type=\"hidden\" name=\"status\" value={filters.status} /> : null}\n <SelectField\n label=\"Branch\"\n name=\"branch\"\n layout=\"inline\"\n value={filters.branch ?? \"\"}\n options={[\n { value: \"\", label: \"All branches\" },\n ...branches.map((branch) => ({ value: branch, label: branch })),\n ]}\n />\n </form>\n </HStack>\n );\n}\n\n/**\n * Affected-capture efficiency: the average share of a snapshot set that recent builds inherited\n * from the baseline instead of rendering, from the counts stored on the builds (no snapshot reads).\n */\nfunction InheritedRatioCard(props: { builds: Build[] }): ReturnType<typeof Card> | null {\n const average = averageInheritedRatio(props.builds);\n if (average === null) {\n return null;\n }\n return (\n <Card>\n <Stat label=\"Inherited from baseline\" value={`${Math.round(average.ratio * 100)}%`} />\n <Meta>\n Average over the last {average.builds} {average.builds === 1 ? \"build\" : \"builds\"}: the\n share of snapshots reused without rendering.\n </Meta>\n </Card>\n );\n}\n\n/** Project builds page: filterable build history for one project. */\nexport async function renderProjectBuildsPage(\n slug: string,\n query: BuildFilters = {},\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 all = await new BuildModel(getStore().db).list(project.id);\n const builds = all.filter((build) => matches(build, query));\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n const filtered = Boolean(query.status ?? query.branch);\n const health = await loadHealthPanel(project);\n\n return (\n <DocumentLayout\n title={project.name}\n nav={{ active: \"builds\", projectSlug: project.slug, projectName: project.name }}\n >\n <PageHeader\n title=\"Builds\"\n description={\n <>\n {project.gitRepository ?? project.slug} · default branch{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </>\n }\n actions={\n <Button variant=\"secondary\" icon=\"settings\" href={urls.settings(project.slug)}>\n Settings\n </Button>\n }\n />\n <VStack>\n <HealthSlot project={project} data={health} />\n {testingEnabled() ? <InheritedRatioCard builds={all} /> : null}\n <BuildsToolbar\n project={project}\n all={all}\n filters={query}\n base={urls.buildsList(project.slug)}\n />\n {builds.length === 0 ? (\n <EmptyState\n title=\"No builds\"\n description={\n filtered\n ? \"No builds match the current filter.\"\n : \"Upload your first build with the CLI. Builds appear here once uploaded.\"\n }\n action={\n filtered ? (\n <Button variant=\"secondary\" href={urls.buildsList(project.slug)}>\n Clear filters\n </Button>\n ) : null\n }\n />\n ) : (\n <BuildsTable project={project} builds={builds} />\n )}\n </VStack>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { testingEnabled } from "../store.mjs";
|
|
4
5
|
import { PageHeader } from "../ui/layout.mjs";
|
|
5
6
|
import { SubNavLayout } from "../ui/subnav.mjs";
|
|
6
7
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
8
|
+
import { renderSettingsAi } from "./settings-ai.mjs";
|
|
9
|
+
import { renderSettingsCapture } from "./settings-capture.mjs";
|
|
10
|
+
import { renderSettingsDesign } from "./settings-design.mjs";
|
|
7
11
|
import { renderSettingsGeneral } from "./settings-general.mjs";
|
|
8
12
|
import { renderSettingsLabels } from "./settings-labels.mjs";
|
|
13
|
+
import { renderSettingsLinks } from "./settings-links.mjs";
|
|
9
14
|
import { renderSettingsMembers } from "./settings-members.mjs";
|
|
10
15
|
import { renderSettingsNotifications } from "./settings-notifications.mjs";
|
|
11
|
-
import { renderSettingsStatus } from "./settings-status.mjs";
|
|
12
16
|
import { renderSettingsTests } from "./settings-tests.mjs";
|
|
13
17
|
import { renderSettingsTokens } from "./settings-tokens.mjs";
|
|
14
18
|
import { renderSettingsWebhooks } from "./settings-webhooks.mjs";
|
|
@@ -19,44 +23,79 @@ function tabHref(project, tab) {
|
|
|
19
23
|
}
|
|
20
24
|
const TAB_LABELS = {
|
|
21
25
|
general: "General",
|
|
26
|
+
capture: "Capture",
|
|
22
27
|
tests: "Tests",
|
|
23
28
|
labels: "Labels",
|
|
29
|
+
links: "Links",
|
|
30
|
+
design: "Design",
|
|
24
31
|
tokens: "Tokens",
|
|
25
32
|
webhooks: "Webhooks",
|
|
26
33
|
notifications: "Notifications",
|
|
27
34
|
members: "Members",
|
|
28
|
-
|
|
35
|
+
ai: "AI"
|
|
29
36
|
};
|
|
30
37
|
const TAB_ICONS = {
|
|
31
38
|
general: "settings",
|
|
39
|
+
capture: "image",
|
|
32
40
|
tests: "activity",
|
|
33
41
|
labels: "tag",
|
|
42
|
+
links: "external-link",
|
|
43
|
+
design: "image",
|
|
34
44
|
tokens: "key",
|
|
35
45
|
webhooks: "webhook",
|
|
36
46
|
notifications: "bell",
|
|
37
47
|
members: "users",
|
|
38
|
-
|
|
48
|
+
ai: "sparkles"
|
|
39
49
|
};
|
|
50
|
+
/** Tabs whose pages are `testing` routes: hidden on a hosting-only instance. */
|
|
51
|
+
const TESTING_TABS = /* @__PURE__ */ new Set([
|
|
52
|
+
"capture",
|
|
53
|
+
"tests",
|
|
54
|
+
"ai"
|
|
55
|
+
]);
|
|
40
56
|
const SETTINGS_TABS = [
|
|
41
57
|
"general",
|
|
58
|
+
"capture",
|
|
42
59
|
"tests",
|
|
43
60
|
"labels",
|
|
61
|
+
"links",
|
|
62
|
+
"design",
|
|
44
63
|
"tokens",
|
|
45
64
|
"webhooks",
|
|
46
65
|
"notifications",
|
|
47
66
|
"members",
|
|
48
|
-
"
|
|
67
|
+
"ai"
|
|
49
68
|
];
|
|
69
|
+
/** A tab shows when its page exists: `testing` tabs need testing, and the AI tab needs AI. */
|
|
70
|
+
function settingsTabShown(tab, data) {
|
|
71
|
+
if (TESTING_TABS.has(tab) && !testingEnabled()) return false;
|
|
72
|
+
return tab !== "ai" || data.ai?.available === true;
|
|
73
|
+
}
|
|
74
|
+
/** Project-behaviour tabs (general, tests, AI); undefined for any other tab. */
|
|
75
|
+
function renderBehaviourTab(data, formState) {
|
|
76
|
+
const { project, activeTab } = data;
|
|
77
|
+
if (activeTab === "general") return renderSettingsGeneral(project, formState, data.isAdmin, data.targets);
|
|
78
|
+
if (activeTab === "capture") return renderSettingsCapture(project, data.isAdmin, formState, data.targets);
|
|
79
|
+
if (activeTab === "ai") return renderSettingsAi(project, data.ai, formState);
|
|
80
|
+
return activeTab === "tests" ? renderSettingsTests(project, data.isAdmin, formState) : void 0;
|
|
81
|
+
}
|
|
82
|
+
/** Labels, links, tokens, and webhooks tabs; undefined for any other tab. */
|
|
83
|
+
function renderCatalogTab(data, formState) {
|
|
84
|
+
const { project, activeTab, isAdmin } = data;
|
|
85
|
+
if (activeTab === "labels") return renderSettingsLabels(project, data.labelTypes, isAdmin, formState);
|
|
86
|
+
if (activeTab === "links") return renderSettingsLinks(project, data.links, data.linkCandidates, isAdmin, formState);
|
|
87
|
+
if (activeTab === "design") return renderSettingsDesign(project, data.design, isAdmin, formState);
|
|
88
|
+
if (activeTab === "tokens") return renderSettingsTokens(project, data.tokens, isAdmin, formState);
|
|
89
|
+
return activeTab === "webhooks" ? renderSettingsWebhooks(project, data.webhooks, isAdmin, formState) : void 0;
|
|
90
|
+
}
|
|
50
91
|
function renderActiveTab(data, formState) {
|
|
51
92
|
const { project, activeTab } = data;
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
if (activeTab === "
|
|
57
|
-
if (activeTab === "notifications") return renderSettingsNotifications(project, data.notificationChannels, data.notifyProviders, data.mySubscription, data.isAdmin, formState);
|
|
93
|
+
const behaviour = renderBehaviourTab(data, formState);
|
|
94
|
+
if (behaviour !== void 0) return behaviour;
|
|
95
|
+
const catalog = renderCatalogTab(data, formState);
|
|
96
|
+
if (catalog !== void 0) return catalog;
|
|
97
|
+
if (activeTab === "notifications") return renderSettingsNotifications(project, data.notificationChannels, data.notifyProviders, data.mySubscription, data.isAdmin, formState, data.notificationsPaused);
|
|
58
98
|
if (activeTab === "members") return renderSettingsMembers(project, data.members, data.groupMappings, data.isAdmin, formState);
|
|
59
|
-
if (activeTab === "status") return renderSettingsStatus(project, data.statusConfigs, data.gitHosts, data.isAdmin, formState);
|
|
60
99
|
return null;
|
|
61
100
|
}
|
|
62
101
|
/** Project settings shell: tab navigation plus the active settings tab. */
|
|
@@ -79,7 +118,7 @@ function renderProjectSettingsPage(data, formState) {
|
|
|
79
118
|
] })
|
|
80
119
|
}), /* @__PURE__ */ jsx(SubNavLayout, {
|
|
81
120
|
label: "Settings sections",
|
|
82
|
-
items: SETTINGS_TABS.map((tab) => ({
|
|
121
|
+
items: SETTINGS_TABS.filter((tab) => settingsTabShown(tab, data)).map((tab) => ({
|
|
83
122
|
label: TAB_LABELS[tab],
|
|
84
123
|
href: tabHref(project, tab),
|
|
85
124
|
icon: TAB_ICONS[tab],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { GitHostProvider } from \"@storyshelf/core/adapter/git-host\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport { PageHeader, SubNavLayout } from \"../ui/components.tsx\";\nimport type { IconName } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { renderSettingsGeneral } from \"./settings-general.tsx\";\nimport { renderSettingsLabels } from \"./settings-labels.tsx\";\nimport { renderSettingsMembers, type SettingsMember } from \"./settings-members.tsx\";\nimport {\n renderSettingsNotifications,\n type SettingsMySubscription,\n type SettingsNotificationChannel,\n} from \"./settings-notifications.tsx\";\nimport { renderSettingsStatus, type SettingsStatusConfig } from \"./settings-status.tsx\";\nimport { renderSettingsTests } from \"./settings-tests.tsx\";\nimport { renderSettingsTokens } from \"./settings-tokens.tsx\";\nimport { renderSettingsWebhooks, type SettingsWebhook } from \"./settings-webhooks.tsx\";\n\n/** Tabs available in the project settings section. */\nexport type SettingsTab =\n | \"general\"\n | \"tests\"\n | \"labels\"\n | \"tokens\"\n | \"webhooks\"\n | \"notifications\"\n | \"members\"\n | \"status\";\n\n/** Data required to render the project settings page with its active tab. */\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: string[];\n mySubscription: SettingsMySubscription | null;\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\n/** Form state shared by the settings tabs (field errors, global error, one-time secret). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n secret?: string;\n}\n\nfunction tabHref(project: Project, tab: SettingsTab): string {\n return tab === \"general\"\n ? `/projects/${project.slug}/settings`\n : `/projects/${project.slug}/settings/${tab}`;\n}\n\nconst TAB_LABELS: Record<SettingsTab, string> = {\n general: \"General\",\n tests: \"Tests\",\n labels: \"Labels\",\n tokens: \"Tokens\",\n webhooks: \"Webhooks\",\n notifications: \"Notifications\",\n members: \"Members\",\n status: \"Git status\",\n};\n\nconst TAB_ICONS: Record<SettingsTab, IconName> = {\n general: \"settings\",\n tests: \"activity\",\n labels: \"tag\",\n tokens: \"key\",\n webhooks: \"webhook\",\n notifications: \"bell\",\n members: \"users\",\n status: \"git-branch\",\n};\n\nconst SETTINGS_TABS: SettingsTab[] = [\n \"general\",\n \"tests\",\n \"labels\",\n \"tokens\",\n \"webhooks\",\n \"notifications\",\n \"members\",\n \"status\",\n];\n\nfunction renderActiveTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n if (activeTab === \"general\") return renderSettingsGeneral(project, formState, data.isAdmin);\n if (activeTab === \"tests\") return renderSettingsTests(project, data.isAdmin, formState);\n if (activeTab === \"labels\")\n return renderSettingsLabels(project, data.labelTypes, data.isAdmin, formState);\n if (activeTab === \"tokens\")\n return renderSettingsTokens(project, data.tokens, data.isAdmin, formState);\n if (activeTab === \"webhooks\")\n return renderSettingsWebhooks(project, data.webhooks, data.isAdmin, formState);\n if (activeTab === \"notifications\")\n return renderSettingsNotifications(\n project,\n data.notificationChannels,\n data.notifyProviders,\n data.mySubscription,\n data.isAdmin,\n formState,\n );\n if (activeTab === \"members\")\n return renderSettingsMembers(\n project,\n data.members,\n data.groupMappings,\n data.isAdmin,\n formState,\n );\n if (activeTab === \"status\")\n return renderSettingsStatus(\n project,\n data.statusConfigs,\n data.gitHosts,\n data.isAdmin,\n formState,\n );\n return null;\n}\n\n/** Project settings shell: tab navigation plus the active settings tab. */\nexport function renderProjectSettingsPage(\n data: ProjectSettingsData,\n formState?: SettingsFormState,\n): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout\n title={`${project.name} · Settings`}\n nav={{\n active: \"settings\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: activeTab === \"general\" ? [] : [{ label: TAB_LABELS[activeTab] }],\n }}\n >\n <PageHeader\n title=\"Project settings\"\n description={\n <>Manage general settings, labels, tokens, webhooks and members for {project.name}.</>\n }\n />\n\n <SubNavLayout\n label=\"Settings sections\"\n items={SETTINGS_TABS.map((tab) => ({\n label: TAB_LABELS[tab],\n href: tabHref(project, tab),\n icon: TAB_ICONS[tab],\n active: activeTab === tab,\n }))}\n >\n {renderActiveTab(data, formState)}\n </SubNavLayout>\n </DocumentLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwDA,SAAS,QAAQ,SAAkB,KAA0B;CAC3D,OAAO,QAAQ,YACX,aAAa,QAAQ,KAAK,aAC1B,aAAa,QAAQ,KAAK,YAAY;AAC5C;AAEA,MAAM,aAA0C;CAC9C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,SAAS;CACT,QAAQ;AACV;AAEA,MAAM,YAA2C;CAC/C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,eAAe;CACf,SAAS;CACT,QAAQ;AACV;AAEA,MAAM,gBAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,gBAAgB,MAA2B,WAAwC;CAC1F,MAAM,EAAE,SAAS,cAAc;CAC/B,IAAI,cAAc,WAAW,OAAO,sBAAsB,SAAS,WAAW,KAAK,OAAO;CAC1F,IAAI,cAAc,SAAS,OAAO,oBAAoB,SAAS,KAAK,SAAS,SAAS;CACtF,IAAI,cAAc,UAChB,OAAO,qBAAqB,SAAS,KAAK,YAAY,KAAK,SAAS,SAAS;CAC/E,IAAI,cAAc,UAChB,OAAO,qBAAqB,SAAS,KAAK,QAAQ,KAAK,SAAS,SAAS;CAC3E,IAAI,cAAc,YAChB,OAAO,uBAAuB,SAAS,KAAK,UAAU,KAAK,SAAS,SAAS;CAC/E,IAAI,cAAc,iBAChB,OAAO,4BACL,SACA,KAAK,sBACL,KAAK,iBACL,KAAK,gBACL,KAAK,SACL,SACF;CACF,IAAI,cAAc,WAChB,OAAO,sBACL,SACA,KAAK,SACL,KAAK,eACL,KAAK,SACL,SACF;CACF,IAAI,cAAc,UAChB,OAAO,qBACL,SACA,KAAK,eACL,KAAK,UACL,KAAK,SACL,SACF;CACF,OAAO;AACT;;AAGA,SAAgB,0BACd,MACA,WACiB;CACjB,MAAM,EAAE,SAAS,cAAc;CAC/B,OACE,qBAAC,gBAAD;EACE,OAAO,GAAG,QAAQ,KAAK;EACvB,KAAK;GACH,QAAQ;GACR,aAAa,QAAQ;GACrB,aAAa,QAAQ;GACrB,OAAO,cAAc,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,WAAW,WAAW,CAAC;EACzE;EAPF,UAAA,CASE,oBAAC,YAAD;GACE,OAAM;GACN,aACE,qBAAA,UAAA,EAAA,UAAA;IAAE;IAAmE,QAAQ;IAAK;GAAG,EAAA,CAAA;EAExF,CAAA,GAED,oBAAC,cAAD;GACE,OAAM;GACN,OAAO,cAAc,KAAK,SAAS;IACjC,OAAO,WAAW;IAClB,MAAM,QAAQ,SAAS,GAAG;IAC1B,MAAM,UAAU;IAChB,QAAQ,cAAc;GACxB,EAAE;GAED,UAAA,gBAAgB,MAAM,SAAS;EACpB,CAAA,CACA;;AAEpB"}
|
|
1
|
+
{"version":3,"file":"project-settings.mjs","names":[],"sources":["../../src/pages/project-settings.tsx"],"sourcesContent":["import type { RunnerTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { LabelType } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { ProjectGroupMapping } from \"@storyshelf/core/schema\";\nimport type { Token } from \"@storyshelf/core/schema\";\nimport { testingEnabled } from \"../store.ts\";\nimport { PageHeader, SubNavLayout } from \"../ui/components.tsx\";\nimport type { IconName } from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport { renderSettingsAi, type SettingsAiData } from \"./settings-ai.tsx\";\nimport { renderSettingsCapture } from \"./settings-capture.tsx\";\nimport { renderSettingsDesign, type SettingsDesignData } from \"./settings-design.tsx\";\nimport { renderSettingsGeneral } from \"./settings-general.tsx\";\nimport { renderSettingsLabels } from \"./settings-labels.tsx\";\nimport {\n renderSettingsLinks,\n type SettingsLink,\n type SettingsLinkCandidate,\n} from \"./settings-links.tsx\";\nimport { renderSettingsMembers, type SettingsMember } from \"./settings-members.tsx\";\nimport {\n renderSettingsNotifications,\n type SettingsMySubscription,\n type SettingsNotificationChannel,\n type SettingsNotifyProvider,\n} from \"./settings-notifications.tsx\";\nimport { renderSettingsTests } from \"./settings-tests.tsx\";\nimport { renderSettingsTokens } from \"./settings-tokens.tsx\";\nimport { renderSettingsWebhooks, type SettingsWebhook } from \"./settings-webhooks.tsx\";\n\n/** Tabs available in the project settings section. */\nexport type SettingsTab =\n | \"general\"\n | \"capture\"\n | \"tests\"\n | \"labels\"\n | \"links\"\n | \"design\"\n | \"tokens\"\n | \"webhooks\"\n | \"notifications\"\n | \"members\"\n | \"ai\";\n\n/** Data required to render the project settings page with its active tab. */\nexport interface ProjectSettingsData {\n project: Project;\n activeTab: SettingsTab;\n labelTypes: LabelType[];\n links: SettingsLink[];\n linkCandidates: SettingsLinkCandidate[];\n design: SettingsDesignData;\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n groupMappings: ProjectGroupMapping[];\n webhooks: SettingsWebhook[];\n notificationChannels: SettingsNotificationChannel[];\n notifyProviders: SettingsNotifyProvider[];\n mySubscription: SettingsMySubscription | null;\n isAdmin: boolean;\n /** Notifiers are wired but no public origin resolves (ADR 0014, decision 11). */\n notificationsPaused?: boolean | undefined;\n /** AI availability and the profile choices (omitted ⇒ AI off). */\n ai?: SettingsAiData;\n /** What the capture runner offers (ADR 0007); omitted when it publishes no list. */\n targets?: readonly RunnerTarget[] | undefined;\n}\n\n/** Form state shared by the settings tabs (field errors, global error, one-time secret). */\nexport interface SettingsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n secret?: string;\n /** Label type key whose edit form is open (labels tab). */\n editKey?: string;\n}\n\nfunction tabHref(project: Project, tab: SettingsTab): string {\n return tab === \"general\"\n ? `/projects/${project.slug}/settings`\n : `/projects/${project.slug}/settings/${tab}`;\n}\n\nconst TAB_LABELS: Record<SettingsTab, string> = {\n general: \"General\",\n capture: \"Capture\",\n tests: \"Tests\",\n labels: \"Labels\",\n links: \"Links\",\n design: \"Design\",\n tokens: \"Tokens\",\n webhooks: \"Webhooks\",\n notifications: \"Notifications\",\n members: \"Members\",\n ai: \"AI\",\n};\n\nconst TAB_ICONS: Record<SettingsTab, IconName> = {\n general: \"settings\",\n capture: \"image\",\n tests: \"activity\",\n labels: \"tag\",\n links: \"external-link\",\n design: \"image\",\n tokens: \"key\",\n webhooks: \"webhook\",\n notifications: \"bell\",\n members: \"users\",\n ai: \"sparkles\",\n};\n\n/** Tabs whose pages are `testing` routes: hidden on a hosting-only instance. */\nconst TESTING_TABS: ReadonlySet<SettingsTab> = new Set([\"capture\", \"tests\", \"ai\"]);\n\nconst SETTINGS_TABS: SettingsTab[] = [\n \"general\",\n \"capture\",\n \"tests\",\n \"labels\",\n \"links\",\n \"design\",\n \"tokens\",\n \"webhooks\",\n \"notifications\",\n \"members\",\n \"ai\",\n];\n\n/** A tab shows when its page exists: `testing` tabs need testing, and the AI tab needs AI. */\nfunction settingsTabShown(tab: SettingsTab, data: ProjectSettingsData): boolean {\n if (TESTING_TABS.has(tab) && !testingEnabled()) {\n return false;\n }\n return tab !== \"ai\" || data.ai?.available === true;\n}\n\n/** Project-behaviour tabs (general, tests, AI); undefined for any other tab. */\nfunction renderBehaviourTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n if (activeTab === \"general\")\n return renderSettingsGeneral(project, formState, data.isAdmin, data.targets);\n if (activeTab === \"capture\")\n return renderSettingsCapture(project, data.isAdmin, formState, data.targets);\n if (activeTab === \"ai\") return renderSettingsAi(project, data.ai, formState);\n return activeTab === \"tests\" ? renderSettingsTests(project, data.isAdmin, formState) : undefined;\n}\n\n/** Labels, links, tokens, and webhooks tabs; undefined for any other tab. */\nfunction renderCatalogTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab, isAdmin } = data;\n if (activeTab === \"labels\")\n return renderSettingsLabels(project, data.labelTypes, isAdmin, formState);\n if (activeTab === \"links\")\n return renderSettingsLinks(project, data.links, data.linkCandidates, isAdmin, formState);\n if (activeTab === \"design\") return renderSettingsDesign(project, data.design, isAdmin, formState);\n if (activeTab === \"tokens\") return renderSettingsTokens(project, data.tokens, isAdmin, formState);\n return activeTab === \"webhooks\"\n ? renderSettingsWebhooks(project, data.webhooks, isAdmin, formState)\n : undefined;\n}\n\nfunction renderActiveTab(data: ProjectSettingsData, formState?: SettingsFormState): unknown {\n const { project, activeTab } = data;\n const behaviour = renderBehaviourTab(data, formState);\n if (behaviour !== undefined) return behaviour;\n const catalog = renderCatalogTab(data, formState);\n if (catalog !== undefined) return catalog;\n if (activeTab === \"notifications\")\n return renderSettingsNotifications(\n project,\n data.notificationChannels,\n data.notifyProviders,\n data.mySubscription,\n data.isAdmin,\n formState,\n data.notificationsPaused,\n );\n if (activeTab === \"members\")\n return renderSettingsMembers(\n project,\n data.members,\n data.groupMappings,\n data.isAdmin,\n formState,\n );\n return null;\n}\n\n/** Project settings shell: tab navigation plus the active settings tab. */\nexport function renderProjectSettingsPage(\n data: ProjectSettingsData,\n formState?: SettingsFormState,\n): RenderedContent {\n const { project, activeTab } = data;\n return (\n <DocumentLayout\n title={`${project.name} · Settings`}\n nav={{\n active: \"settings\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: activeTab === \"general\" ? [] : [{ label: TAB_LABELS[activeTab] }],\n }}\n >\n <PageHeader\n title=\"Project settings\"\n description={\n <>Manage general settings, labels, tokens, webhooks and members for {project.name}.</>\n }\n />\n\n <SubNavLayout\n label=\"Settings sections\"\n items={SETTINGS_TABS.filter((tab) => settingsTabShown(tab, data)).map((tab) => ({\n label: TAB_LABELS[tab],\n href: tabHref(project, tab),\n icon: TAB_ICONS[tab],\n active: activeTab === tab,\n }))}\n >\n {renderActiveTab(data, formState)}\n </SubNavLayout>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ import { Badge, Meta, statusTone } from "../ui/feedback.mjs";
|
|
|
7
7
|
import { Card } from "../ui/layout.mjs";
|
|
8
8
|
import { Thumbnail } from "../ui/thumbnail.mjs";
|
|
9
9
|
import { RelativeTime } from "../ui/time.mjs";
|
|
10
|
+
import { healthTone } from "./health-panel.mjs";
|
|
10
11
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
12
|
//#region src/pages/projects-card.tsx
|
|
12
13
|
const thumbStrip = css`
|
|
@@ -156,7 +157,19 @@ function ProjectCard(props) {
|
|
|
156
157
|
children: project.gitDefaultBranch
|
|
157
158
|
})
|
|
158
159
|
] }),
|
|
159
|
-
/* @__PURE__ */ jsx(LatestBuild, { latest })
|
|
160
|
+
/* @__PURE__ */ jsx(LatestBuild, { latest }),
|
|
161
|
+
summary.health ? /* @__PURE__ */ jsx("a", {
|
|
162
|
+
href: urls.library,
|
|
163
|
+
children: /* @__PURE__ */ jsxs(Badge, {
|
|
164
|
+
tone: healthTone(summary.health.verdict),
|
|
165
|
+
icon: true,
|
|
166
|
+
children: [
|
|
167
|
+
"health: ",
|
|
168
|
+
summary.health.verdict,
|
|
169
|
+
summary.health.score === null ? "" : ` · ${Math.round(summary.health.score)}`
|
|
170
|
+
]
|
|
171
|
+
})
|
|
172
|
+
}) : null
|
|
160
173
|
]
|
|
161
174
|
}),
|
|
162
175
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects-card.mjs","names":[],"sources":["../../src/pages/projects-card.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n RelativeTime,\n Thumbnail,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst thumbStrip = css`\n /* project-thumbs */\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--space-2);\n padding: var(--space-3);\n background: var(--accent-wash);\n border-bottom: 1px solid var(--border-subtle);\n`;\n\nconst cardBody = css`\n /* project-body */\n flex: 1;\n display: grid;\n align-content: start;\n gap: var(--space-2);\n padding: var(--space-4);\n`;\n\nconst cardTitleRow = css`\n /* project-title-row */\n display: flex;\n gap: var(--space-2);\n align-items: flex-start;\n justify-content: space-between;\n`;\n\nconst cardTitle = css`\n /* project-title */\n margin: 0;\n font-size: var(--text-lg);\n font-weight: 650;\n letter-spacing: -0.01em;\n & a {\n color: var(--text-primary);\n }\n & a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst latestRow = css`\n /* project-latest */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n align-items: center;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n`;\n\nconst cardFooter = css`\n /* project-footer */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-4);\n border-top: 1px solid var(--border-subtle);\n`;\n\n/** What the overview shows about a project's most recent build. */\nexport interface ProjectSummary {\n project: Project;\n latest: Build | null;\n /** Up to three screenshots of the latest build. */\n previews: Snapshot[];\n /** Snapshots in the latest build still waiting for a decision. */\n pending: number;\n}\n\nfunction PendingBadge({\n pending,\n href,\n}: {\n pending: number;\n href: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (pending === 0) {\n return (\n <Badge tone=\"success\" icon>\n All clear\n </Badge>\n );\n }\n return (\n <a href={href}>\n <Badge tone=\"warning\" icon>\n {pending} to review\n </Badge>\n </a>\n );\n}\n\nfunction LatestBuild({\n latest,\n}: {\n latest: Build | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (!latest) {\n return <Meta>No builds yet — upload one from CI.</Meta>;\n }\n return (\n <div class={latestRow}>\n <Meta as=\"span\" mono>\n {latest.gitBranch} · {latest.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(latest.status)}>{latest.status}</Badge>\n <RelativeTime value={latest.createdAt} />\n </div>\n );\n}\n\nfunction Previews({\n project,\n latest,\n previews,\n}: Pick<ProjectSummary, \"project\" | \"latest\" | \"previews\">):\n | HtmlEscapedString\n | Promise<HtmlEscapedString> {\n return (\n <div class={thumbStrip}>\n {[0, 1, 2].map((slot): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const snap = previews[slot];\n return (\n <Thumbnail\n key={String(slot)}\n src={\n snap && latest\n ? `/api/v1/projects/${project.slug}/builds/${latest.id}/snapshots/${snap.id}/image`\n : null\n }\n alt={snap ? `${snap.storyTitle} / ${snap.storyName}` : \"\"}\n placeholder=\"\"\n />\n );\n })}\n </div>\n );\n}\n\n/** One project in the overview grid: previews, latest build, review state, actions. */\nexport function ProjectCard(props: {\n summary: ProjectSummary;\n urls: { library: string; settings: string; storybook: string; review: string };\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { summary, urls } = props;\n const { project, latest, previews, pending } = summary;\n return (\n <Card\n fill\n data-filter-item\n data-filter-text={`${project.name} ${project.slug} ${project.gitRepository ?? \"\"}`}\n >\n {previews.length > 0 ? (\n <Previews project={project} latest={latest} previews={previews} />\n ) : null}\n <div class={cardBody}>\n <div class={cardTitleRow}>\n <h2 class={cardTitle}>\n <a href={urls.library}>{project.name}</a>\n </h2>\n {latest ? <PendingBadge pending={pending} href={urls.review} /> : null}\n </div>\n <Meta>\n {project.gitRepository ?? project.slug} · default{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </Meta>\n <LatestBuild latest={latest} />\n </div>\n <div class={cardFooter}>\n <Button variant=\"secondary\" size=\"sm\" icon=\"image\" href={urls.library}>\n Library\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.storybook}>\n Storybook\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"settings\" href={urls.settings}>\n Settings\n </Button>\n </div>\n </Card>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"projects-card.mjs","names":[],"sources":["../../src/pages/projects-card.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport type { HealthBadge } from \"../insights/health-panel.ts\";\nimport {\n Badge,\n Button,\n Card,\n Meta,\n RelativeTime,\n Thumbnail,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { css } from \"../ui/css.ts\";\nimport { healthTone } from \"./health-panel.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst thumbStrip = css`\n /* project-thumbs */\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--space-2);\n padding: var(--space-3);\n background: var(--accent-wash);\n border-bottom: 1px solid var(--border-subtle);\n`;\n\nconst cardBody = css`\n /* project-body */\n flex: 1;\n display: grid;\n align-content: start;\n gap: var(--space-2);\n padding: var(--space-4);\n`;\n\nconst cardTitleRow = css`\n /* project-title-row */\n display: flex;\n gap: var(--space-2);\n align-items: flex-start;\n justify-content: space-between;\n`;\n\nconst cardTitle = css`\n /* project-title */\n margin: 0;\n font-size: var(--text-lg);\n font-weight: 650;\n letter-spacing: -0.01em;\n & a {\n color: var(--text-primary);\n }\n & a:hover {\n color: var(--accent-fg);\n }\n`;\n\nconst latestRow = css`\n /* project-latest */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n align-items: center;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n`;\n\nconst cardFooter = css`\n /* project-footer */\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-4);\n border-top: 1px solid var(--border-subtle);\n`;\n\n/** What the overview shows about a project's most recent build. */\nexport interface ProjectSummary {\n project: Project;\n latest: Build | null;\n /** Up to three screenshots of the latest build. */\n previews: Snapshot[];\n /** Snapshots in the latest build still waiting for a decision. */\n pending: number;\n /** AI health digest (last 30 days) when one has finished. */\n health?: HealthBadge | null;\n}\n\nfunction PendingBadge({\n pending,\n href,\n}: {\n pending: number;\n href: string;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (pending === 0) {\n return (\n <Badge tone=\"success\" icon>\n All clear\n </Badge>\n );\n }\n return (\n <a href={href}>\n <Badge tone=\"warning\" icon>\n {pending} to review\n </Badge>\n </a>\n );\n}\n\nfunction LatestBuild({\n latest,\n}: {\n latest: Build | null;\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n if (!latest) {\n return <Meta>No builds yet — upload one from CI.</Meta>;\n }\n return (\n <div class={latestRow}>\n <Meta as=\"span\" mono>\n {latest.gitBranch} · {latest.gitSha.slice(0, 7)}\n </Meta>\n <Badge tone={statusTone(latest.status)}>{latest.status}</Badge>\n <RelativeTime value={latest.createdAt} />\n </div>\n );\n}\n\nfunction Previews({\n project,\n latest,\n previews,\n}: Pick<ProjectSummary, \"project\" | \"latest\" | \"previews\">):\n | HtmlEscapedString\n | Promise<HtmlEscapedString> {\n return (\n <div class={thumbStrip}>\n {[0, 1, 2].map((slot): HtmlEscapedString | Promise<HtmlEscapedString> => {\n const snap = previews[slot];\n return (\n <Thumbnail\n key={String(slot)}\n src={\n snap && latest\n ? `/api/v1/projects/${project.slug}/builds/${latest.id}/snapshots/${snap.id}/image`\n : null\n }\n alt={snap ? `${snap.storyTitle} / ${snap.storyName}` : \"\"}\n placeholder=\"\"\n />\n );\n })}\n </div>\n );\n}\n\n/** One project in the overview grid: previews, latest build, review state, actions. */\nexport function ProjectCard(props: {\n summary: ProjectSummary;\n urls: { library: string; settings: string; storybook: string; review: string };\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { summary, urls } = props;\n const { project, latest, previews, pending } = summary;\n return (\n <Card\n fill\n data-filter-item\n data-filter-text={`${project.name} ${project.slug} ${project.gitRepository ?? \"\"}`}\n >\n {previews.length > 0 ? (\n <Previews project={project} latest={latest} previews={previews} />\n ) : null}\n <div class={cardBody}>\n <div class={cardTitleRow}>\n <h2 class={cardTitle}>\n <a href={urls.library}>{project.name}</a>\n </h2>\n {latest ? <PendingBadge pending={pending} href={urls.review} /> : null}\n </div>\n <Meta>\n {project.gitRepository ?? project.slug} · default{\" \"}\n <Badge tone=\"neutral\">{project.gitDefaultBranch}</Badge>\n </Meta>\n <LatestBuild latest={latest} />\n {summary.health ? (\n <a href={urls.library}>\n <Badge tone={healthTone(summary.health.verdict)} icon>\n health: {summary.health.verdict}\n {summary.health.score === null ? \"\" : ` · ${Math.round(summary.health.score)}`}\n </Badge>\n </a>\n ) : null}\n </div>\n <div class={cardFooter}>\n <Button variant=\"secondary\" size=\"sm\" icon=\"image\" href={urls.library}>\n Library\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"external-link\" href={urls.storybook}>\n Storybook\n </Button>\n <Button variant=\"ghost\" size=\"sm\" icon=\"settings\" href={urls.settings}>\n Settings\n </Button>\n </div>\n </Card>\n );\n}\n"],"mappings":"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"}
|
package/dist/pages/projects.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { HStack, VStack } from "../ui/stacks.mjs";
|
|
|
10
10
|
import { CodeBlock } from "../ui/code-block.mjs";
|
|
11
11
|
import { FilterInput } from "../ui/filter-input.mjs";
|
|
12
12
|
import { DocumentLayout } from "../ui/document.mjs";
|
|
13
|
+
import { loadHealthBadge } from "../insights/health-panel.mjs";
|
|
13
14
|
import { ProjectCard } from "./projects-card.mjs";
|
|
14
15
|
import { BuildModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
|
|
15
16
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
@@ -32,11 +33,13 @@ const setupSummary = css`
|
|
|
32
33
|
async function summarize(project) {
|
|
33
34
|
const { db } = getStore();
|
|
34
35
|
const [latest] = await new BuildModel(db).list(project.id);
|
|
36
|
+
const health = await loadHealthBadge(project);
|
|
35
37
|
if (!latest) return {
|
|
36
38
|
project,
|
|
37
39
|
latest: null,
|
|
38
40
|
previews: [],
|
|
39
|
-
pending: 0
|
|
41
|
+
pending: 0,
|
|
42
|
+
health
|
|
40
43
|
};
|
|
41
44
|
const snapshots = await new SnapshotModel(db).listByBuild(latest.id);
|
|
42
45
|
const pending = snapshots.filter((snap) => snap.status === "new" || snap.status === "changed");
|
|
@@ -44,7 +47,8 @@ async function summarize(project) {
|
|
|
44
47
|
project,
|
|
45
48
|
latest,
|
|
46
49
|
previews: (pending.length > 0 ? pending : snapshots).slice(0, 3),
|
|
47
|
-
pending: pending.length
|
|
50
|
+
pending: pending.length,
|
|
51
|
+
health
|
|
48
52
|
};
|
|
49
53
|
}
|
|
50
54
|
/** Copy-ready steps to get a first build uploaded. */
|