@crvouga/atlas 0.4.0 → 0.5.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/CHANGELOG.md +7 -1
- package/README.md +178 -617
- package/dist/bun/src/driver.d.ts +41 -7
- package/dist/bun/src/driver.js +47 -6
- package/dist/bun/src/index.d.ts +8 -0
- package/dist/bun/src/index.js +8 -0
- package/dist/core/src/cli.d.ts +14 -1
- package/dist/core/src/cli.js +30 -1
- package/dist/core/src/config.d.ts +70 -4
- package/dist/core/src/config.js +62 -3
- package/dist/core/src/events/cloudevents.d.ts +54 -9
- package/dist/core/src/events/cloudevents.js +60 -9
- package/dist/core/src/graph.d.ts +21 -0
- package/dist/core/src/graph.js +26 -0
- package/dist/core/src/index.d.ts +11 -0
- package/dist/core/src/index.js +11 -0
- package/dist/core/src/lint.d.ts +30 -0
- package/dist/core/src/lint.js +13 -0
- package/dist/core/src/plan.d.ts +39 -1
- package/dist/core/src/plan.js +28 -1
- package/dist/core/src/privacy.d.ts +29 -2
- package/dist/core/src/privacy.js +19 -2
- package/dist/core/src/report/formats.d.ts +34 -4
- package/dist/core/src/report/formats.js +24 -4
- package/dist/core/src/report/markdown.d.ts +15 -0
- package/dist/core/src/report/markdown.js +8 -0
- package/dist/core/src/run/clients.d.ts +44 -5
- package/dist/core/src/run/clients.js +46 -2
- package/dist/core/src/run/execute.d.ts +69 -16
- package/dist/core/src/run/execute.js +67 -16
- package/dist/core/src/run/frames.d.ts +13 -4
- package/dist/core/src/run/frames.js +19 -4
- package/dist/core/src/run/http.d.ts +34 -1
- package/dist/core/src/run/http.js +14 -1
- package/dist/core/src/run/types.d.ts +76 -3
- package/dist/core/src/run/types.js +19 -3
- package/dist/core/src/signals.d.ts +49 -5
- package/dist/core/src/signals.js +22 -5
- package/dist/core/src/spec/compose.d.ts +23 -1
- package/dist/core/src/spec/compose.js +10 -1
- package/dist/core/src/spec/index.d.ts +9 -1
- package/dist/core/src/spec/index.js +9 -1
- package/dist/core/src/spec/load.d.ts +22 -1
- package/dist/core/src/spec/load.js +31 -1
- package/dist/core/src/spec/parse.d.ts +16 -2
- package/dist/core/src/spec/parse.js +25 -2
- package/dist/core/src/spec/scxml.d.ts +25 -4
- package/dist/core/src/spec/scxml.js +36 -4
- package/dist/core/src/spec/types.d.ts +52 -1
- package/dist/core/src/spec/types.js +24 -0
- package/dist/detox/src/index.d.ts +54 -11
- package/dist/detox/src/index.js +45 -7
- package/dist/playwright/src/driver.d.ts +39 -3
- package/dist/playwright/src/driver.js +27 -3
- package/dist/playwright/src/index.d.ts +10 -0
- package/dist/playwright/src/index.js +10 -0
- package/dist/playwright/src/media.d.ts +50 -4
- package/dist/playwright/src/media.js +55 -4
- package/dist/playwright/src/signals.d.ts +44 -5
- package/dist/playwright/src/signals.js +29 -5
- package/dist/reports/src/attachment-files.d.ts +44 -0
- package/dist/reports/src/attachment-files.js +121 -0
- package/dist/reports/src/attachment-response.d.ts +31 -0
- package/dist/reports/src/attachment-response.js +55 -0
- package/dist/reports/src/aws-credentials.d.ts +48 -0
- package/dist/reports/src/aws-credentials.js +221 -0
- package/dist/reports/src/browser-navigation.d.ts +14 -0
- package/dist/reports/src/browser-navigation.js +7 -0
- package/dist/reports/src/bundle.d.ts +34 -2
- package/dist/reports/src/bundle.js +38 -2
- package/dist/reports/src/catalog.d.ts +34 -3
- package/dist/reports/src/catalog.js +17 -3
- package/dist/reports/src/cli-stores.d.ts +36 -0
- package/dist/reports/src/cli-stores.js +73 -0
- package/dist/reports/src/clock.d.ts +7 -2
- package/dist/reports/src/clock.js +5 -2
- package/dist/reports/src/completion-order.d.ts +18 -5
- package/dist/reports/src/completion-order.js +18 -5
- package/dist/reports/src/compose.d.ts +80 -0
- package/dist/reports/src/compose.js +117 -0
- package/dist/reports/src/copy-store.d.ts +29 -0
- package/dist/reports/src/copy-store.js +52 -0
- package/dist/reports/src/directory-outline.d.ts +38 -0
- package/dist/reports/src/directory-outline.js +103 -0
- package/dist/reports/src/directory-store.d.ts +20 -0
- package/dist/reports/src/directory-store.js +166 -0
- package/dist/reports/src/doctor.d.ts +29 -0
- package/dist/reports/src/doctor.js +83 -0
- package/dist/reports/src/effective-delta.d.ts +34 -0
- package/dist/reports/src/effective-delta.js +25 -0
- package/dist/reports/src/evidence-detail.d.ts +64 -0
- package/dist/reports/src/evidence-detail.js +92 -0
- package/dist/reports/src/evidence-live.d.ts +35 -5
- package/dist/reports/src/evidence-live.js +18 -2
- package/dist/reports/src/evidence-types.d.ts +68 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +43 -4
- package/dist/reports/src/evidence-view.js +13 -2
- package/dist/reports/src/explorer-types.d.ts +40 -3
- package/dist/reports/src/explorer-types.js +8 -0
- package/dist/reports/src/explorer-versions.d.ts +9 -1
- package/dist/reports/src/explorer-versions.js +18 -1
- package/dist/reports/src/export-evidence.d.ts +16 -1
- package/dist/reports/src/export-evidence.js +52 -1
- package/dist/reports/src/fetcher.d.ts +27 -3
- package/dist/reports/src/fetcher.js +11 -3
- package/dist/reports/src/format-date.d.ts +6 -0
- package/dist/reports/src/format-date.js +6 -0
- package/dist/reports/src/format.d.ts +16 -0
- package/dist/reports/src/format.js +10 -0
- package/dist/reports/src/frames.d.ts +36 -3
- package/dist/reports/src/frames.js +22 -3
- package/dist/reports/src/hash.d.ts +18 -0
- package/dist/reports/src/hash.js +13 -0
- package/dist/reports/src/host.d.ts +29 -0
- package/dist/reports/src/host.js +64 -0
- package/dist/reports/src/html-frame.d.ts +5 -6
- package/dist/reports/src/html-frame.js +12 -1
- package/dist/reports/src/http-store.d.ts +52 -0
- package/dist/reports/src/http-store.js +145 -0
- package/dist/reports/src/http.d.ts +7 -0
- package/dist/reports/src/http.js +6 -0
- package/dist/reports/src/ingest.d.ts +54 -0
- package/dist/reports/src/ingest.js +210 -0
- package/dist/reports/src/init.d.ts +40 -0
- package/dist/reports/src/init.js +364 -0
- package/dist/reports/src/junit.d.ts +33 -6
- package/dist/reports/src/junit.js +87 -21
- package/dist/reports/src/keyboard.d.ts +20 -0
- package/dist/reports/src/keyboard.js +11 -0
- package/dist/reports/src/keys.d.ts +21 -3
- package/dist/reports/src/keys.js +21 -3
- package/dist/reports/src/layer-axes.d.ts +35 -5
- package/dist/reports/src/layer-axes.js +25 -5
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +18 -1
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +147 -31
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +11 -0
- package/dist/reports/src/layer-store.d.ts +42 -0
- package/dist/reports/src/layer-store.js +63 -5
- package/dist/reports/src/layer-types.d.ts +32 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +86 -7
- package/dist/reports/src/media-types.d.ts +11 -0
- package/dist/reports/src/media-types.js +22 -0
- package/dist/reports/src/namespace-guard.d.ts +10 -1
- package/dist/reports/src/namespace-guard.js +10 -1
- package/dist/reports/src/navigation.d.ts +12 -0
- package/dist/reports/src/navigation.js +7 -0
- package/dist/reports/src/object-io.d.ts +11 -4
- package/dist/reports/src/object-io.js +4 -4
- package/dist/reports/src/object-store-conformance.d.ts +27 -0
- package/dist/reports/src/object-store-conformance.js +87 -0
- package/dist/reports/src/object-store.d.ts +33 -24
- package/dist/reports/src/object-store.js +17 -24
- package/dist/reports/src/parse-with.d.ts +16 -0
- package/dist/reports/src/parse-with.js +14 -0
- package/dist/reports/src/performance-part.d.ts +25 -1
- package/dist/reports/src/performance-part.js +15 -1
- package/dist/reports/src/performance.d.ts +22 -2
- package/dist/reports/src/performance.js +12 -2
- package/dist/reports/src/persist.d.ts +12 -3
- package/dist/reports/src/persist.js +12 -3
- package/dist/reports/src/phone.d.ts +18 -1
- package/dist/reports/src/phone.js +18 -1
- package/dist/reports/src/protocol.d.ts +26 -4
- package/dist/reports/src/protocol.js +15 -4
- package/dist/reports/src/publication.d.ts +21 -1
- package/dist/reports/src/publication.js +21 -1
- package/dist/reports/src/publish-checks.d.ts +35 -0
- package/dist/reports/src/publish-checks.js +122 -0
- package/dist/reports/src/publish.d.ts +42 -9
- package/dist/reports/src/publish.js +36 -7
- package/dist/reports/src/read.d.ts +46 -16
- package/dist/reports/src/read.js +38 -14
- package/dist/reports/src/recording-bundle.d.ts +13 -2
- package/dist/reports/src/recording-bundle.js +4 -2
- package/dist/reports/src/recording-keys.d.ts +15 -1
- package/dist/reports/src/recording-keys.js +15 -1
- package/dist/reports/src/remote-report.d.ts +28 -7
- package/dist/reports/src/remote-report.js +15 -7
- package/dist/reports/src/report-cli.d.ts +51 -0
- package/dist/reports/src/report-cli.js +485 -0
- package/dist/reports/src/report-index.d.ts +99 -0
- package/dist/reports/src/report-index.js +337 -0
- package/dist/reports/src/report-loader.d.ts +10 -0
- package/dist/reports/src/report-loader.js +6 -0
- package/dist/reports/src/report-parts.d.ts +29 -3
- package/dist/reports/src/report-parts.js +37 -3
- package/dist/reports/src/report-status.d.ts +48 -0
- package/dist/reports/src/report-status.js +156 -0
- package/dist/reports/src/report-types.d.ts +73 -0
- package/dist/reports/src/report-types.js +3 -0
- package/dist/reports/src/reporter.d.ts +67 -0
- package/dist/reports/src/reporter.js +128 -0
- package/dist/reports/src/reports-explorer-route.d.ts +541 -0
- package/dist/reports/src/reports-explorer-route.js +11 -0
- package/dist/reports/src/reports-list-route.d.ts +9 -0
- package/dist/reports/src/reports-list-route.js +9 -0
- package/dist/reports/src/reports-route.d.ts +561 -22
- package/dist/reports/src/reports-route.js +11 -3
- package/dist/reports/src/reports-store.d.ts +19 -2
- package/dist/reports/src/reports-store.js +10 -2
- package/dist/reports/src/resilient-cache.d.ts +14 -1
- package/dist/reports/src/resilient-cache.js +8 -1
- package/dist/reports/src/retention.d.ts +17 -1
- package/dist/reports/src/retention.js +5 -1
- package/dist/reports/src/run-bundle.d.ts +63 -0
- package/dist/reports/src/run-bundle.js +159 -0
- package/dist/reports/src/s3-store.d.ts +79 -0
- package/dist/reports/src/s3-store.js +295 -0
- package/dist/reports/src/scenario.d.ts +14 -4
- package/dist/reports/src/scenario.js +5 -4
- package/dist/reports/src/sigv4.d.ts +72 -0
- package/dist/reports/src/sigv4.js +96 -0
- package/dist/reports/src/snapshot-bundle.d.ts +24 -1
- package/dist/reports/src/snapshot-bundle.js +27 -2
- package/dist/reports/src/snapshot-surface.d.ts +20 -9
- package/dist/reports/src/snapshot-surface.js +32 -19
- package/dist/reports/src/source-content.d.ts +23 -0
- package/dist/reports/src/source-content.js +30 -1
- package/dist/reports/src/source-entries.d.ts +13 -0
- package/dist/reports/src/source-entries.js +6 -0
- package/dist/reports/src/source-part.d.ts +15 -0
- package/dist/reports/src/source-part.js +8 -0
- package/dist/reports/src/source-reader.d.ts +11 -0
- package/dist/reports/src/source-reader.js +5 -0
- package/dist/reports/src/spec-evidence.d.ts +12 -2
- package/dist/reports/src/spec-evidence.js +6 -2
- package/dist/reports/src/spec-inventory.d.ts +15 -1
- package/dist/reports/src/spec-inventory.js +22 -1
- package/dist/reports/src/spec-link.d.ts +28 -0
- package/dist/reports/src/spec-link.js +70 -0
- package/dist/reports/src/spec-set.d.ts +63 -0
- package/dist/reports/src/spec-set.js +177 -0
- package/dist/reports/src/status.d.ts +10 -0
- package/dist/reports/src/status.js +5 -0
- package/dist/reports/src/store-prefix.d.ts +28 -0
- package/dist/reports/src/store-prefix.js +22 -0
- package/dist/reports/src/store-url.d.ts +89 -0
- package/dist/reports/src/store-url.js +160 -0
- package/dist/reports/src/test-catalog-entity.d.ts +11 -0
- package/dist/reports/src/test-catalog-entity.js +6 -0
- package/dist/reports/src/types.d.ts +6 -1
- package/dist/reports/src/use-report-nav.d.ts +35 -0
- package/dist/reports/src/use-report-nav.js +12 -0
- package/dist/schema/src/api.d.ts +799 -0
- package/dist/schema/src/api.js +201 -0
- package/dist/schema/src/cli-results.d.ts +210 -0
- package/dist/schema/src/cli-results.js +124 -0
- package/dist/schema/src/common.d.ts +53 -0
- package/dist/schema/src/common.js +42 -0
- package/dist/schema/src/index.d.ts +14 -0
- package/dist/schema/src/index.js +14 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/manifest.d.ts +95 -0
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/report-sources.d.ts +28 -0
- package/dist/schema/src/report-sources.js +25 -0
- package/dist/schema/src/runs-index.d.ts +16 -0
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +93 -0
- package/dist/schema/src/server-config.js +73 -0
- package/dist/schema/src/spec.d.ts +69 -0
- package/dist/schema/src/spec.js +58 -0
- package/dist/server/src/app.d.ts +62 -0
- package/dist/server/src/app.js +419 -0
- package/dist/server/src/auth.d.ts +41 -0
- package/dist/server/src/auth.js +59 -0
- package/dist/server/src/config.d.ts +83 -0
- package/dist/server/src/config.js +187 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +80 -0
- package/dist/server/src/events.d.ts +29 -0
- package/dist/server/src/events.js +101 -0
- package/dist/server/src/host.d.ts +30 -0
- package/dist/server/src/host.js +115 -0
- package/dist/server/src/index.d.ts +16 -0
- package/dist/server/src/index.js +16 -0
- package/dist/server/src/llms.d.ts +19 -0
- package/dist/server/src/llms.js +63 -0
- package/dist/server/src/main.d.ts +30 -0
- package/dist/server/src/main.js +112 -0
- package/dist/server/src/openapi.d.ts +59 -0
- package/dist/server/src/openapi.js +374 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/site.d.ts +13 -0
- package/dist/server/src/site.js +43 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +159 -0
- package/dist/server/src/validate.d.ts +71 -0
- package/dist/server/src/validate.js +197 -0
- package/dist/vitest/src/index.d.ts +27 -5
- package/dist/vitest/src/index.js +18 -5
- package/dist/webdriver/src/driver.d.ts +63 -11
- package/dist/webdriver/src/driver.js +57 -6
- package/dist/webdriver/src/index.d.ts +8 -0
- package/dist/webdriver/src/index.js +1 -0
- package/dist/webdriver/src/protocol.d.ts +75 -6
- package/dist/webdriver/src/protocol.js +79 -6
- package/docs/agents.md +137 -0
- package/docs/deploying.md +345 -0
- package/docs/dogfooding.md +175 -0
- package/docs/extending.md +242 -0
- package/docs/integrating.md +485 -0
- package/docs/reference/bun.md +141 -0
- package/docs/reference/cli.md +146 -0
- package/docs/reference/core.md +2426 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +303 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7098 -0
- package/docs/reference/schema.md +1651 -0
- package/docs/reference/server.md +685 -0
- package/docs/reference/vitest.md +45 -0
- package/docs/reference/webdriver.md +330 -0
- package/docs/report-backends.md +144 -0
- package/docs/report-contract.md +400 -0
- package/docs/reporting-issues.md +160 -0
- package/docs/server.md +285 -0
- package/docs/statecharts.md +641 -0
- package/llms-full.txt +3506 -0
- package/llms.txt +47 -0
- package/package.json +32 -4
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +65 -0
- package/packages/reports/STORAGE.md +4 -0
- package/packages/schema/README.md +14 -0
- package/packages/schema/schemas/check-detail.schema.json +1032 -0
- package/packages/schema/schemas/checks-result.schema.json +26 -0
- package/packages/schema/schemas/command-error.schema.json +35 -0
- package/packages/schema/schemas/doctor-result.schema.json +60 -0
- package/packages/schema/schemas/effective-delta.schema.json +490 -0
- package/packages/schema/schemas/effective-evidence.schema.json +471 -0
- package/packages/schema/schemas/evidence-page.schema.json +271 -0
- package/packages/schema/schemas/hello-event.schema.json +22 -0
- package/packages/schema/schemas/init-plan.schema.json +347 -0
- package/packages/schema/schemas/layer-rows.schema.json +199 -0
- package/packages/schema/schemas/layer.schema.json +2888 -0
- package/packages/schema/schemas/merge-result.schema.json +40 -0
- package/packages/schema/schemas/outline.schema.json +62 -0
- package/packages/schema/schemas/problem.schema.json +37 -0
- package/packages/schema/schemas/publish-checks.schema.json +1413 -0
- package/packages/schema/schemas/publish-result.schema.json +168 -0
- package/packages/schema/schemas/ready.schema.json +68 -0
- package/packages/schema/schemas/report-event.schema.json +100 -0
- package/packages/schema/schemas/report-status.schema.json +370 -0
- package/packages/schema/schemas/server-config.schema.json +194 -0
- package/packages/schema/schemas/server-info.schema.json +114 -0
- package/packages/schema/schemas/spec-set.schema.json +103 -0
- package/packages/schema/schemas/store-listing.schema.json +29 -0
- package/packages/schema/schemas/store-write-result.schema.json +28 -0
- package/packages/schema/schemas/stream-result.schema.json +59 -0
- package/packages/server/README.md +43 -0
- package/packages/server/openapi.json +6699 -0
- package/packages/visualizer/README.md +21 -0
- package/packages/visualizer/src/app/AppShell.tsx +9 -2
- package/packages/visualizer/src/app/report-sources.tsx +6 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/Header.module.css +35 -0
- package/packages/visualizer/src/components/Header.tsx +40 -3
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/evidence/api.ts +14 -2
- package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +3 -1
- package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-stream.ts +204 -0
- package/packages/visualizer/src/runtime.ts +19 -0
- package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
- package/packages/visualizer/vite.config.ts +11 -3
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { LayerHeader, LayerRow } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
+
import type { CheckDetail, EvidenceHolder } from '@crvouga/atlas/reports/evidence-detail';
|
|
3
|
+
import { roleOfType } from '@crvouga/atlas/reports/evidence-detail';
|
|
4
|
+
import { contentTypeOf } from '@crvouga/atlas/reports/media-types';
|
|
5
|
+
import type { Annotation, Attachment, Attempt, Step } from '@crvouga/atlas/schema';
|
|
6
|
+
import { useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useMemo, type ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { reportAssetUrl, reportJson, reportText } from './api';
|
|
10
|
+
import styles from './artifacts.module.css';
|
|
11
|
+
import { duration, plural } from './format';
|
|
12
|
+
import { StatusMark } from './marks';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How one kind of attachment is shown. The first renderer that matches wins, so a new kind of artifact (a
|
|
16
|
+
* flame graph, a HAR, a coverage map) is one more entry, and an attachment nobody has a renderer for is still
|
|
17
|
+
* listed, with its name, what it is and a way to download it. Nothing here fails on what it does not know.
|
|
18
|
+
*/
|
|
19
|
+
export type ArtifactRenderer = {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly matches: (attachment: Attachment, type: string) => boolean;
|
|
22
|
+
/** `url` is where the bytes are, if they are anywhere a browser can reach; `path` is the report API path of a stored file. */
|
|
23
|
+
readonly render: (attachment: Attachment, context: { url: string | null; path: string | null; type: string }) => ReactNode;
|
|
24
|
+
/** Wide renderers take a whole row of the gallery. */
|
|
25
|
+
readonly wide?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mediaType = (attachment: Attachment) => (attachment.contentType ?? contentTypeOf(attachment.name)).split(';')[0]!.trim().toLowerCase();
|
|
29
|
+
const label = (attachment: Attachment) => attachment.description ?? attachment.name;
|
|
30
|
+
|
|
31
|
+
/** Only addresses a browser may follow: never `javascript:` or `data:` from a producer. */
|
|
32
|
+
function safeUri(uri: string | undefined): string | null {
|
|
33
|
+
if (!uri) return null;
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(uri, 'https://atlas.invalid/');
|
|
36
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? uri : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TextValue({ attachment, path }: { attachment: Attachment; path: string | null }) {
|
|
43
|
+
const stored = useQuery({ queryKey: ['artifact-text', path ?? ''], queryFn: ({ signal }) => reportText(path!, signal), enabled: attachment.text === undefined && path !== null, staleTime: Number.POSITIVE_INFINITY, retry: false });
|
|
44
|
+
const raw = attachment.text ?? stored.data;
|
|
45
|
+
if (raw === undefined) return <p className={styles.where}>{stored.isError ? 'Could not be read.' : 'Loading…'}</p>;
|
|
46
|
+
const shown = raw.length > 200_000 ? raw.slice(0, 200_000) : raw;
|
|
47
|
+
const pretty = /json/.test(mediaType(attachment)) ? tryPretty(shown) : shown;
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<pre className={styles.text}>{pretty}</pre>
|
|
51
|
+
{shown.length < raw.length && <p className={styles.where}>Showing the first 200 000 characters.</p>}
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryPretty(text: string) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
59
|
+
} catch {
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** In order: the first that matches. Add to the front with {@link registerArtifactRenderer}. */
|
|
65
|
+
export const artifactRenderers: ArtifactRenderer[] = [
|
|
66
|
+
{ id: 'image', matches: (_, type) => type.startsWith('image/') && type !== 'image/svg+xml', render: (a, { url }) => (url ? <a href={url} target="_blank" rel="noreferrer"><img src={url} alt={label(a)} loading="lazy" width={a.width} height={a.height} /></a> : null) },
|
|
67
|
+
// SVG can carry script: shown as a picture only, never inlined.
|
|
68
|
+
{ id: 'svg', matches: (_, type) => type === 'image/svg+xml', render: (a, { url }) => (url ? <img src={url} alt={label(a)} loading="lazy" /> : null) },
|
|
69
|
+
{ id: 'video', wide: true, matches: (_, type) => type.startsWith('video/'), render: (a, { url }) => (url ? <video src={url} controls preload="metadata" aria-label={label(a)} /> : null) },
|
|
70
|
+
{ id: 'audio', matches: (_, type) => type.startsWith('audio/'), render: (a, { url }) => (url ? <audio src={url} controls preload="none" aria-label={label(a)} /> : null) },
|
|
71
|
+
// A page snapshot runs nothing: the frame has no permissions and the file is served sandboxed.
|
|
72
|
+
{ id: 'snapshot', wide: true, matches: (_, type) => type === 'text/html' || type === 'application/xhtml+xml', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
|
|
73
|
+
{ id: 'pdf', wide: true, matches: (_, type) => type === 'application/pdf', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
|
|
74
|
+
{ id: 'text', wide: true, matches: (a, type) => a.text !== undefined || type.startsWith('text/') || /(json|xml|yaml|x-ndjson|x-diff)/.test(type), render: (a, { path }) => <TextValue attachment={a} path={path} /> }
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export function registerArtifactRenderer(renderer: ArtifactRenderer) {
|
|
78
|
+
artifactRenderers.unshift(renderer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sizeOf(bytes: number | undefined) {
|
|
82
|
+
if (bytes === undefined) return '';
|
|
83
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
84
|
+
return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)} KiB` : `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Artifact({ attachment, layer }: { attachment: Attachment; layer: string }) {
|
|
88
|
+
const type = mediaType(attachment);
|
|
89
|
+
const path = attachment.bundleKey ? `/layers/${encodeURIComponent(layer)}/blobs/${attachment.bundleKey.split('/').map(encodeURIComponent).join('/')}` : null;
|
|
90
|
+
const url = path ? reportAssetUrl(path) : safeUri(attachment.uri);
|
|
91
|
+
const renderer = artifactRenderers.find((candidate) => candidate.matches(attachment, type));
|
|
92
|
+
const body = renderer?.render(attachment, { url, path, type });
|
|
93
|
+
return (
|
|
94
|
+
<li className={styles.figure} data-wide={renderer?.wide || undefined} data-role={attachment.role ?? roleOfType(type)}>
|
|
95
|
+
{body}
|
|
96
|
+
<figcaption>
|
|
97
|
+
<strong>{attachment.name}</strong>
|
|
98
|
+
{attachment.role && <span className={styles.role}>{attachment.role}</span>}
|
|
99
|
+
<span>{[type, sizeOf(attachment.bytes), attachment.durationMs ? duration(attachment.durationMs) : ''].filter(Boolean).join(' · ')}</span>
|
|
100
|
+
{url && <a href={url} download={attachment.name} target="_blank" rel="noreferrer">{body ? 'Download' : 'Open'}</a>}
|
|
101
|
+
{!url && attachment.text === undefined && <span>No file to show</span>}
|
|
102
|
+
</figcaption>
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function Attachments({ attachments, layer }: { attachments: readonly Attachment[]; layer: string }) {
|
|
108
|
+
if (!attachments.length) return null;
|
|
109
|
+
return (
|
|
110
|
+
<section aria-label="Attachments">
|
|
111
|
+
<h4>{plural(attachments.length, 'attachment', 'attachments')}</h4>
|
|
112
|
+
<ul className={styles.gallery}>
|
|
113
|
+
{attachments.map((attachment, index) => <Artifact key={`${attachment.id ?? attachment.name}:${index}`} attachment={attachment} layer={layer} />)}
|
|
114
|
+
</ul>
|
|
115
|
+
</section>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const STEP_STATUS: Record<string, 'passed' | 'failed' | 'running' | 'not-run'> = { passed: 'passed', pass: 'passed', ok: 'passed', failed: 'failed', fail: 'failed', error: 'failed', running: 'running', skipped: 'not-run', 'not-run': 'not-run', 'not-reached': 'not-run', pending: 'not-run' };
|
|
120
|
+
|
|
121
|
+
/** The timeline: ordered by when each step started, nested by `parentId`. A step whose parent is missing is shown at the top. */
|
|
122
|
+
export function Timeline({ steps, layer }: { steps: readonly Step[]; layer: string }) {
|
|
123
|
+
const tree = useMemo(() => {
|
|
124
|
+
const ids = new Set(steps.flatMap((step) => (step.id ? [step.id] : [])));
|
|
125
|
+
const children = new Map<string | null, Step[]>();
|
|
126
|
+
for (const step of steps) {
|
|
127
|
+
const parent = step.parentId && ids.has(step.parentId) ? step.parentId : null;
|
|
128
|
+
children.set(parent, [...(children.get(parent) ?? []), step]);
|
|
129
|
+
}
|
|
130
|
+
for (const list of children.values()) list.sort((a, b) => (a.offsetMs ?? 0) - (b.offsetMs ?? 0));
|
|
131
|
+
return children;
|
|
132
|
+
}, [steps]);
|
|
133
|
+
const render = (parent: string | null): ReactNode => (
|
|
134
|
+
<ol>
|
|
135
|
+
{(tree.get(parent) ?? []).map((step, index) => {
|
|
136
|
+
const status = STEP_STATUS[(step.status ?? '').toLowerCase()];
|
|
137
|
+
return (
|
|
138
|
+
<li key={`${step.id ?? step.name}:${index}`} className={styles.step} data-status={status ?? step.status ?? 'none'}>
|
|
139
|
+
<div className={styles.stepRow}>
|
|
140
|
+
<StatusMark status={status ?? 'none'} size={14} />
|
|
141
|
+
<span className={styles.stepName}>{step.name}</span>
|
|
142
|
+
{step.kind && <span className={styles.stepKind}>{step.kind}</span>}
|
|
143
|
+
{step.status && !status && <span className={styles.stepKind}>{step.status}</span>}
|
|
144
|
+
<span className={styles.spacer} />
|
|
145
|
+
<span className={styles.stepTime}>{[step.offsetMs !== undefined ? `+${duration(step.offsetMs)}` : '', duration(step.durationMs)].filter(Boolean).join(' · ')}</span>
|
|
146
|
+
</div>
|
|
147
|
+
{step.detail && <p className={styles.stepDetail}>{step.detail}</p>}
|
|
148
|
+
{step.annotations && <Notes notes={step.annotations} />}
|
|
149
|
+
{step.attachments && <Attachments attachments={step.attachments} layer={layer} />}
|
|
150
|
+
{step.id && tree.has(step.id) && render(step.id)}
|
|
151
|
+
</li>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</ol>
|
|
155
|
+
);
|
|
156
|
+
return (
|
|
157
|
+
<section aria-label="Timeline" className={styles.timeline}>
|
|
158
|
+
<h4>{plural(steps.length, 'step', 'steps')}</h4>
|
|
159
|
+
{render(null)}
|
|
160
|
+
</section>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function Notes({ notes }: { notes: readonly Annotation[] }) {
|
|
165
|
+
if (!notes.length) return null;
|
|
166
|
+
return (
|
|
167
|
+
<ul className={styles.notes} aria-label="Annotations">
|
|
168
|
+
{notes.map((note, index) => (
|
|
169
|
+
<li key={index} className={styles.note}>
|
|
170
|
+
<span className={styles.level} data-level={note.level ?? 'info'}>{note.level && ['info', 'warning', 'error'].includes(note.level) ? note.level : note.type}</span>
|
|
171
|
+
<span>
|
|
172
|
+
{note.message}
|
|
173
|
+
{note.source?.file && <span className={styles.where}> {note.source.file}{note.source.line ? `:${note.source.line}` : ''}</span>}
|
|
174
|
+
{note.level && !['info', 'warning', 'error'].includes(note.level) && <span className={styles.where}> ({note.level})</span>}
|
|
175
|
+
</span>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
178
|
+
</ul>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function Tries({ attempts, layer }: { attempts: readonly Attempt[]; layer: string }) {
|
|
183
|
+
if (!attempts.length) return null;
|
|
184
|
+
return (
|
|
185
|
+
<section aria-label="Tries">
|
|
186
|
+
<h4>{plural(attempts.length, 'try', 'tries')}</h4>
|
|
187
|
+
<ol className={styles.timeline}>
|
|
188
|
+
{attempts.map((attempt) => (
|
|
189
|
+
<li key={attempt.index} className={styles.step}>
|
|
190
|
+
<div className={styles.stepRow}>
|
|
191
|
+
<StatusMark status={attempt.status} size={14} />
|
|
192
|
+
<span className={styles.stepName}>Try {attempt.index + 1}</span>
|
|
193
|
+
<span className={styles.spacer} />
|
|
194
|
+
<span className={styles.stepTime}>{duration(attempt.durationMs)}</span>
|
|
195
|
+
</div>
|
|
196
|
+
{attempt.detail && <p className={styles.stepDetail}>{attempt.detail}</p>}
|
|
197
|
+
{attempt.attachments && <Attachments attachments={attempt.attachments} layer={layer} />}
|
|
198
|
+
</li>
|
|
199
|
+
))}
|
|
200
|
+
</ol>
|
|
201
|
+
</section>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** What is not known: kept, and shown as it is, so nothing a tool recorded is out of reach. */
|
|
206
|
+
function Raw({ extensions }: { extensions: Readonly<Record<string, unknown>> }) {
|
|
207
|
+
const names = Object.keys(extensions);
|
|
208
|
+
if (!names.length) return null;
|
|
209
|
+
return (
|
|
210
|
+
<details className={styles.raw}>
|
|
211
|
+
<summary>{plural(names.length, 'extension', 'extensions')}: {names.join(', ')}</summary>
|
|
212
|
+
<pre className={styles.text}>{JSON.stringify(extensions, null, 2)}</pre>
|
|
213
|
+
</details>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Facts({ holder }: { holder: EvidenceHolder }) {
|
|
218
|
+
const labels = Object.entries(holder.labels ?? {});
|
|
219
|
+
const tags = holder.tags ?? [];
|
|
220
|
+
const links = (holder.links ?? []).flatMap((link) => { const url = safeUri(link.url); return url ? [{ ...link, url }] : []; });
|
|
221
|
+
if (!labels.length && !tags.length && !links.length) return null;
|
|
222
|
+
return (
|
|
223
|
+
<div>
|
|
224
|
+
<ul className={styles.chips} aria-label="Facts">
|
|
225
|
+
{tags.map((tag) => <li key={`t:${tag}`} className={styles.chip}>{tag}</li>)}
|
|
226
|
+
{labels.map(([name, value]) => <li key={`l:${name}`} className={styles.chip}>{name}<b>{value}</b></li>)}
|
|
227
|
+
{links.map((link) => <li key={`k:${link.url}`} className={styles.chip}><a href={link.url} target="_blank" rel="noreferrer">{link.name ?? link.rel ?? link.url}</a></li>)}
|
|
228
|
+
</ul>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function Evidence({ holder, layer, steps }: { holder: EvidenceHolder; layer: string; steps?: boolean }) {
|
|
234
|
+
return (
|
|
235
|
+
<>
|
|
236
|
+
<Facts holder={holder} />
|
|
237
|
+
{holder.annotations && <Notes notes={holder.annotations} />}
|
|
238
|
+
{steps && holder.steps && holder.steps.length > 0 && <Timeline steps={holder.steps} layer={layer} />}
|
|
239
|
+
{holder.attempts && holder.attempts.length > 1 && <Tries attempts={holder.attempts} layer={layer} />}
|
|
240
|
+
{holder.attachments && <Attachments attachments={holder.attachments} layer={layer} />}
|
|
241
|
+
{holder.extensions && <Raw extensions={holder.extensions} />}
|
|
242
|
+
</>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Everything one result carries beyond its outcome, read when it is opened, and what the run around it carried. */
|
|
247
|
+
export function ResultEvidence({ layer, row }: { layer: LayerHeader; row: LayerRow }) {
|
|
248
|
+
const wanted = row.evidence !== undefined || layer.evidence !== undefined;
|
|
249
|
+
const detail = useQuery({
|
|
250
|
+
queryKey: ['evidence-detail', layer.backend, layer.id, row.kind, row.id],
|
|
251
|
+
queryFn: ({ signal }) => reportJson<CheckDetail>(`/evidence/detail/${encodeURIComponent(layer.backend)}/${encodeURIComponent(layer.id)}?check=${encodeURIComponent(row.id)}&kind=${encodeURIComponent(row.kind)}`, signal),
|
|
252
|
+
enabled: wanted && /^[0-9a-f]{64}$/.test(layer.id),
|
|
253
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
254
|
+
retry: false
|
|
255
|
+
});
|
|
256
|
+
if (!wanted) return null;
|
|
257
|
+
if (detail.isLoading) return <p className={styles.where} role="status">Loading what this result carries…</p>;
|
|
258
|
+
if (detail.isError || !detail.data) return <p className={styles.where} role="alert">What this result carries could not be read.</p>;
|
|
259
|
+
const { check, run } = detail.data;
|
|
260
|
+
const hasRun = Object.values(run).some((value) => (Array.isArray(value) ? value.length > 0 : value && typeof value === 'object' ? Object.keys(value).length > 0 : false));
|
|
261
|
+
return (
|
|
262
|
+
<div className={styles.panel} aria-label="Evidence">
|
|
263
|
+
<Evidence holder={check} layer={layer.id} steps />
|
|
264
|
+
{hasRun && (
|
|
265
|
+
<section aria-label="From the run">
|
|
266
|
+
<h4>From the run</h4>
|
|
267
|
+
<Evidence holder={run} layer={layer.id} />
|
|
268
|
+
</section>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
@@ -8,6 +8,7 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
8
8
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
9
9
|
|
|
10
10
|
import { reportJson } from './api';
|
|
11
|
+
import { ResultEvidence } from './artifacts';
|
|
11
12
|
import { useEvidence } from './evidence-context';
|
|
12
13
|
import styles from './evidence.module.css';
|
|
13
14
|
import { ago, duration, plural, statusWord, useNow } from './format';
|
|
@@ -77,7 +78,7 @@ export function ResultRow({ label, slot, open, onToggle, children }: { label: st
|
|
|
77
78
|
</span>
|
|
78
79
|
{slot?.live && <span className={styles.liveTag}>Running now</span>}
|
|
79
80
|
<span className={styles.resultAside}>
|
|
80
|
-
{newest ? [duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
|
|
81
|
+
{newest ? [newest.row.evidence?.attachments ? plural(newest.row.evidence.attachments, 'attachment', 'attachments') : '', duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
|
|
81
82
|
</span>
|
|
82
83
|
<span className={styles.chevron} aria-hidden="true" />
|
|
83
84
|
</button>
|
|
@@ -253,6 +254,7 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
|
253
254
|
</p>
|
|
254
255
|
);
|
|
255
256
|
}))}
|
|
257
|
+
<ResultEvidence layer={chosen.layer} row={chosen.row} />
|
|
256
258
|
{(moments > 0 || chosen.layer.runUrl) && (
|
|
257
259
|
<p className={styles.actionsLine}>
|
|
258
260
|
{moments > 0 && (
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import type { ActiveRun, BackendInfo, EvidencePage, LayerEvidence, LayerHeader, LayerRows } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
+
import { applyEffectiveDelta, isEffectiveDelta, type EffectiveDelta } from '@crvouga/atlas/reports/effective-delta';
|
|
3
|
+
import type { EffectiveEvidence } from '@crvouga/atlas/reports/report-index';
|
|
2
4
|
import { contributes, evidenceView, type EvidenceView, type SourceSelection } from '@crvouga/atlas/reports/evidence-view';
|
|
3
5
|
import type { SpecEntry } from '@crvouga/atlas/reports/explorer-types';
|
|
4
6
|
import { combineEvidence, distinctHeaders } from '@crvouga/atlas/reports/layer-merge';
|
|
5
7
|
import type { ReportOutline } from '@crvouga/atlas/reports/layer-types';
|
|
6
8
|
import { SpecIndexSchema } from '@crvouga/atlas/schema';
|
|
7
|
-
import { useInfiniteQuery, useQueries, useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { useInfiniteQuery, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
10
|
import { createContext, useContext, useEffect, useMemo, useRef, type ReactNode } from 'react';
|
|
9
11
|
|
|
10
12
|
import { keys, specKey, useAdapter } from '../data/queries';
|
|
13
|
+
import { eventsAddress, listenLive, throttled, useLiveStatus } from '../live/live-stream';
|
|
11
14
|
import { fileName, folderOf, kindOfFile, outlinePath } from '../spec-kind';
|
|
12
15
|
import { useUiStore } from '../state/ui-store';
|
|
13
|
-
import { reportJson, staticEvidence } from './api';
|
|
16
|
+
import { effectiveEvidence, reportJson, staticEvidence } from './api';
|
|
14
17
|
|
|
15
18
|
/** How often the layer list is asked for while a spec run is writing results, and while nothing is. */
|
|
16
19
|
const LIVE_POLL_MS = 2_000;
|
|
17
20
|
const IDLE_POLL_MS = 15_000;
|
|
21
|
+
/** The effective report is revalidated with its ETag, so asking often costs one small answer. */
|
|
22
|
+
const EFFECTIVE_POLL_MS = 5_000;
|
|
23
|
+
/** While the server's stream is open it says when to read again; this is only the safety net behind it. */
|
|
24
|
+
const LIVE_SAFETY_POLL_MS = 60_000;
|
|
25
|
+
/** At most this often is the report read again because the server said it changed. */
|
|
26
|
+
const LIVE_REREAD_MS = 250;
|
|
18
27
|
|
|
19
28
|
export type EvidenceState = {
|
|
20
29
|
/** The layer list has been read at least once. */
|
|
@@ -63,26 +72,76 @@ function outlineOf(files: readonly string[]): ReportOutline {
|
|
|
63
72
|
return { specs, sha: null, changedFiles: [] };
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
const NONE: readonly LayerEvidence[] = [];
|
|
76
|
+
|
|
77
|
+
function hasExclusions(excluded: SourceSelection): boolean {
|
|
78
|
+
return Object.values(excluded).some((list) => list.length > 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
67
82
|
const adapter = useAdapter();
|
|
68
83
|
const source = specKey(adapter);
|
|
69
84
|
const excluded = useUiStore((s) => s.excluded);
|
|
70
85
|
const setExcluded = useUiStore((s) => s.setExcluded);
|
|
71
86
|
|
|
87
|
+
const client = useQueryClient();
|
|
88
|
+
const stream = useLiveStatus();
|
|
89
|
+
const streaming = stream === 'live';
|
|
90
|
+
// The server says when results arrive: read the report again then, not on a timer.
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const url = eventsAddress();
|
|
93
|
+
if (!url) return;
|
|
94
|
+
// A burst of results (a CI run finishing, many shards at once) is read once, not once for each.
|
|
95
|
+
const reread = throttled(() => {
|
|
96
|
+
void client.invalidateQueries({ queryKey: ['evidence-effective'] });
|
|
97
|
+
void client.invalidateQueries({ queryKey: ['evidence-index'] });
|
|
98
|
+
}, LIVE_REREAD_MS);
|
|
99
|
+
const reoutline = throttled(() => {
|
|
100
|
+
void client.invalidateQueries({ queryKey: ['evidence-outline'] });
|
|
101
|
+
void client.invalidateQueries({ queryKey: ['evidence-effective'] });
|
|
102
|
+
}, LIVE_REREAD_MS);
|
|
103
|
+
const stop = listenLive(url, (event) => {
|
|
104
|
+
if (event.type === 'change' && event.change.scope === 'specs') return reoutline();
|
|
105
|
+
if (event.type !== 'report' && event.type !== 'open') return;
|
|
106
|
+
reread();
|
|
107
|
+
if (event.type === 'open') reoutline();
|
|
108
|
+
});
|
|
109
|
+
return () => { stop(); reread.cancel(); reoutline.cancel(); };
|
|
110
|
+
}, [client]);
|
|
111
|
+
|
|
72
112
|
const specIndex = useQuery({ queryKey: keys.specIndex(source), queryFn: adapter.specIndex, retry: false });
|
|
73
113
|
const files = useMemo(() => {
|
|
74
114
|
const parsed = SpecIndexSchema.safeParse(specIndex.data);
|
|
75
115
|
return parsed.success ? parsed.data.files : [];
|
|
76
116
|
}, [specIndex.data]);
|
|
77
117
|
|
|
118
|
+
// The node answers with only what the report shows, in one request. Leaving a source out needs the
|
|
119
|
+
// history it hides, so that one case reads every layer, as a store without a node always does.
|
|
120
|
+
const effective = effectiveEvidence() && !staticEvidence && !hasExclusions(excluded);
|
|
121
|
+
const overview = useQuery({
|
|
122
|
+
queryKey: ['evidence-effective'],
|
|
123
|
+
// Holding a report, ask only for what changed since it; the server answers with all of it when it cannot.
|
|
124
|
+
queryFn: async ({ signal }) => {
|
|
125
|
+
const held = client.getQueryData<EffectiveEvidence>(['evidence-effective']);
|
|
126
|
+
const answer = await reportJson<EffectiveEvidence | EffectiveDelta>(held ? `/evidence/effective?since=${encodeURIComponent(held.version)}` : '/evidence/effective', signal);
|
|
127
|
+
if (!isEffectiveDelta(answer)) return answer;
|
|
128
|
+
return (held && applyEffectiveDelta(held, answer)) ?? reportJson<EffectiveEvidence>('/evidence/effective', signal);
|
|
129
|
+
},
|
|
130
|
+
enabled: effective,
|
|
131
|
+
staleTime: 0,
|
|
132
|
+
retry: false,
|
|
133
|
+
refetchInterval: streaming ? LIVE_SAFETY_POLL_MS : EFFECTIVE_POLL_MS
|
|
134
|
+
});
|
|
135
|
+
|
|
78
136
|
const index = useInfiniteQuery({
|
|
79
137
|
queryKey: ['evidence-index'],
|
|
138
|
+
enabled: !effective,
|
|
80
139
|
initialPageParam: null as string | null,
|
|
81
140
|
queryFn: ({ pageParam, signal }) => reportJson<EvidencePage>(`/evidence${pageParam ? `?cursor=${encodeURIComponent(pageParam)}` : ''}`, signal),
|
|
82
141
|
getNextPageParam: (page) => page.cursor,
|
|
83
142
|
staleTime: 0,
|
|
84
143
|
retry: false,
|
|
85
|
-
refetchInterval: (query) => (staticEvidence ? false : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
|
|
144
|
+
refetchInterval: (query) => (staticEvidence ? false : streaming ? LIVE_SAFETY_POLL_MS : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
|
|
86
145
|
});
|
|
87
146
|
// Show the newest layers at once, then read the older pages behind them.
|
|
88
147
|
const { hasNextPage, isFetching, isError, fetchNextPage } = index;
|
|
@@ -98,7 +157,7 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
|
98
157
|
const serverOutline = useQuery({ queryKey: ['evidence-outline'], queryFn: ({ signal }) => reportJson<ReportOutline>('/outline', signal), staleTime: 30_000, retry: false });
|
|
99
158
|
const outline = useMemo(() => serverOutline.data ?? outlineOf(files), [serverOutline.data, files]);
|
|
100
159
|
|
|
101
|
-
const wanted = useMemo(() => layers.filter((layer) => contributes(layer, excluded)), [layers, excluded]);
|
|
160
|
+
const wanted = useMemo(() => (effective ? [] : layers.filter((layer) => contributes(layer, excluded))), [layers, excluded, effective]);
|
|
102
161
|
const rows = useQueries({
|
|
103
162
|
queries: wanted.map((layer) => ({
|
|
104
163
|
queryKey: rowsKey(layer),
|
|
@@ -126,23 +185,26 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
|
126
185
|
// `stamp` changes exactly when a layer's rows do.
|
|
127
186
|
}, [wanted, rows.stamp]);
|
|
128
187
|
|
|
129
|
-
const
|
|
188
|
+
const shown = effective ? overview.data?.layers ?? NONE : evidence;
|
|
189
|
+
const view = useMemo(() => evidenceView(combineEvidence(outline, shown)), [outline, shown]);
|
|
130
190
|
const titles = useMemo(() => new Map(outline.specs.map((spec) => [spec.path, spec.title])), [outline]);
|
|
191
|
+
const effectiveHeaders = useMemo(() => (overview.data?.layers ?? NONE).map((entry) => entry.layer), [overview.data]);
|
|
131
192
|
|
|
132
193
|
const state: EvidenceState = {
|
|
133
|
-
ready: pages !== undefined,
|
|
134
|
-
unavailable: pages === undefined && index.error ? index.error.message : null,
|
|
194
|
+
ready: effective ? overview.data !== undefined : pages !== undefined,
|
|
195
|
+
unavailable: effective ? (overview.data === undefined && overview.error ? overview.error.message : null) : pages === undefined && index.error ? index.error.message : null,
|
|
135
196
|
retry: () => {
|
|
197
|
+
if (effective) { void Promise.all([overview.refetch(), serverOutline.refetch()]); return; }
|
|
136
198
|
void reportJson<EvidencePage>('/evidence?retry=1')
|
|
137
199
|
.catch(() => null)
|
|
138
200
|
.then(() => Promise.all([index.refetch(), serverOutline.refetch()]));
|
|
139
201
|
},
|
|
140
202
|
pending: wanted.length - evidence.length,
|
|
141
|
-
layers,
|
|
142
|
-
evidence,
|
|
143
|
-
backends: first?.backends ?? [],
|
|
144
|
-
active: first?.active ?? [],
|
|
145
|
-
notices,
|
|
203
|
+
layers: effective ? effectiveHeaders : layers,
|
|
204
|
+
evidence: shown,
|
|
205
|
+
backends: effective ? overview.data?.backends ?? [] : first?.backends ?? [],
|
|
206
|
+
active: effective ? [] : first?.active ?? [],
|
|
207
|
+
notices: effective ? overview.data?.notices ?? [] : notices,
|
|
146
208
|
view,
|
|
147
209
|
files,
|
|
148
210
|
filesPending: specIndex.isPending,
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
2
|
import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
|
|
3
|
+
import { lazy, Suspense } from 'react';
|
|
3
4
|
|
|
4
5
|
import { keys, specKey, useAdapter } from '../data/queries';
|
|
5
6
|
import { outlinePath, specFile, specKind } from '../spec-kind';
|
|
6
7
|
import type { SelectCheck } from './check-detail';
|
|
7
|
-
import { DocumentPage } from './document-page';
|
|
8
8
|
import { useEvidence } from './evidence-context';
|
|
9
9
|
import styles from './evidence.module.css';
|
|
10
|
-
import { FeaturePage } from './feature-page';
|
|
11
|
-
import { ModelPage } from './model-page';
|
|
12
10
|
import { Overview } from './overview';
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
// Each kind of spec has a page with a parser of its own (Gherkin, Quint, DMN); a page loads only the one it shows.
|
|
13
|
+
const DocumentPage = lazy(() => import('./document-page').then((m) => ({ default: m.DocumentPage })));
|
|
14
|
+
const FeaturePage = lazy(() => import('./feature-page').then((m) => ({ default: m.FeaturePage })));
|
|
15
|
+
const ModelPage = lazy(() => import('./model-page').then((m) => ({ default: m.ModelPage })));
|
|
16
|
+
const RecordingPage = lazy(() => import('./recording-page').then((m) => ({ default: m.RecordingPage })));
|
|
17
|
+
|
|
18
|
+
function Loading({ children }: { children: React.ReactNode }) {
|
|
19
|
+
return <Suspense fallback={<article className={styles.page}><p className={styles.aside} role="status">Loading…</p></article>}>{children}</Suspense>;
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
/** One spec tab: its overview, one of its documents, or a recording opened from that document. */
|
|
16
23
|
export function SpecPage() {
|
|
@@ -63,15 +70,17 @@ export function SpecPage() {
|
|
|
63
70
|
const spec = `${evidence.titleOf(file)}: `;
|
|
64
71
|
const label = slot ? (slot.title.startsWith(spec) ? slot.title.slice(spec.length) : slot.title) : 'Recorded test';
|
|
65
72
|
return (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
<Loading>
|
|
74
|
+
<RecordingPage
|
|
75
|
+
slot={slot}
|
|
76
|
+
layerId={search.layer}
|
|
77
|
+
label={label}
|
|
78
|
+
back={evidence.titleOf(file)}
|
|
79
|
+
moment={search.moment ?? null}
|
|
80
|
+
onMoment={(moment, replace) => void navigate({ search: (prev) => ({ ...prev, moment }), replace })}
|
|
81
|
+
onClose={() => select({ view: null })}
|
|
82
|
+
/>
|
|
83
|
+
</Loading>
|
|
75
84
|
);
|
|
76
85
|
}
|
|
77
86
|
if (text.isPending) return <article className={styles.page}><p className={styles.aside} role="status">Loading the {kind.one}…</p></article>;
|
|
@@ -86,7 +95,5 @@ export function SpecPage() {
|
|
|
86
95
|
);
|
|
87
96
|
}
|
|
88
97
|
const page = { file, text: text.data, kind, entry, selection: { check: search.check, layer: search.layer }, select };
|
|
89
|
-
|
|
90
|
-
if (kind.id === 'quint') return <ModelPage {...page} />;
|
|
91
|
-
return <DocumentPage {...page} />;
|
|
98
|
+
return <Loading>{kind.id === 'gherkin' ? <FeaturePage {...page} /> : kind.id === 'quint' ? <ModelPage {...page} /> : <DocumentPage {...page} />}</Loading>;
|
|
92
99
|
}
|
|
@@ -9,7 +9,7 @@ import { StatusMark } from '../evidence/marks';
|
|
|
9
9
|
import { useEvidence } from '../evidence/evidence-context';
|
|
10
10
|
import { kindOfFile } from '../spec-kind';
|
|
11
11
|
import { corpusFrom } from './corpus';
|
|
12
|
-
import { setFinderOpen, setHelpOpen, toggleFinder, useFinderOpen, useHelpOpen, isHelpOpen } from './open';
|
|
12
|
+
import { commandKey, setFinderOpen, setHelpOpen, toggleFinder, useFinderOpen, useHelpOpen, isHelpOpen } from './open';
|
|
13
13
|
import { FIND_GROUPS, indexCorpus, notationOf, rankIndex, splitQuery, type FindDoc, type FindKind, type FindTarget, type Ranked } from './rank';
|
|
14
14
|
import { applySemantic, createWorkerEmbedder, embeddingsAvailable, embedText, textHash, type Embedder } from './semantic';
|
|
15
15
|
import styles from './Finder.module.css';
|
|
@@ -36,10 +36,6 @@ const SHORTCUTS: readonly { readonly keys: string; readonly does: string }[] = [
|
|
|
36
36
|
{ keys: 'Home End', does: 'Start and end of a recording' }
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
-
function commandKey() {
|
|
40
|
-
return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
39
|
function isTypingTarget(target: EventTarget | null) {
|
|
44
40
|
if (!(target instanceof HTMLElement)) return false;
|
|
45
41
|
const tag = target.tagName;
|
|
@@ -299,5 +295,3 @@ function Row({ hit }: { hit: Ranked }) {
|
|
|
299
295
|
</>
|
|
300
296
|
);
|
|
301
297
|
}
|
|
302
|
-
|
|
303
|
-
export { commandKey };
|
|
@@ -49,3 +49,7 @@ export function useFinderOpen() {
|
|
|
49
49
|
export function useHelpOpen() {
|
|
50
50
|
return useSyncExternalStore(subscribeFinder, isHelpOpen, () => false);
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export function commandKey() {
|
|
54
|
+
return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K';
|
|
55
|
+
}
|