@crvouga/atlas 0.3.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 +12 -0
- package/README.md +178 -604
- 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 +40 -4
- package/dist/reports/src/evidence-live.js +52 -4
- package/dist/reports/src/evidence-types.d.ts +70 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +58 -6
- package/dist/reports/src/evidence-view.js +44 -5
- 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 +48 -0
- package/dist/reports/src/layer-axes.js +71 -0
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +19 -2
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +154 -33
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +14 -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 +34 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +88 -12
- 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 +30 -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 +44 -9
- package/dist/reports/src/publish.js +40 -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 +40 -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 +33 -5
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +69 -2
- package/packages/reports/STORAGE.md +5 -1
- 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 +27 -5
- package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/scripts/make-fixtures.ts +2 -0
- package/packages/visualizer/scripts/publish-data.ts +1 -2
- 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 +41 -4
- 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 +38 -4
- package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
- package/packages/visualizer/src/evidence/evidence.module.css +20 -0
- package/packages/visualizer/src/evidence/marks.tsx +2 -0
- package/packages/visualizer/src/evidence/overview.tsx +3 -3
- package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
- 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/src/state/ui-store.ts +6 -2
- package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
- package/packages/visualizer/vite/example-reports.ts +475 -0
- package/packages/visualizer/vite.config.ts +18 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LayerHeader } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
-
import { branchOf, contributes, sourceFacets, type SourceSelection } from '@crvouga/atlas/reports/evidence-view';
|
|
2
|
+
import { ALL_SOURCES, branchOf, contributes, sourceFacets, sourceSelection, type SourceFacet, type SourceSelection } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
3
4
|
import * as Dialog from '@radix-ui/react-dialog';
|
|
4
5
|
import { useMemo } from 'react';
|
|
5
6
|
|
|
@@ -20,16 +21,23 @@ function counts(layer: LayerHeader) {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/** How many sources are switched off, for the button that opens the panel. */
|
|
23
|
-
export function excludedCount(excluded: SourceSelection) {
|
|
24
|
-
|
|
24
|
+
export function excludedCount(excluded: Partial<SourceSelection>) {
|
|
25
|
+
const selection = sourceSelection(excluded);
|
|
26
|
+
return selection.backends.length + selection.branches.length + selection.versions.length + selection.environments.length + selection.layers.length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function versionLabel(id: string) {
|
|
30
|
+
return id === 'unknown' ? 'Unknown version' : id.slice(0, 7);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
|
-
* Where the combined report comes from. Every backend, branch and
|
|
29
|
-
* switched off here, which is also how an earlier result is brought back
|
|
34
|
+
* Where the combined report comes from. Every backend, environment, branch, version and layer
|
|
35
|
+
* contributes unless it is switched off here, which is also how an earlier result is brought back.
|
|
30
36
|
*/
|
|
31
37
|
export function SourcesPanel() {
|
|
32
|
-
const
|
|
38
|
+
const evidence = useEvidence();
|
|
39
|
+
const { layers, backends, setExcluded, notices, retry } = evidence;
|
|
40
|
+
const excluded = sourceSelection(evidence.excluded);
|
|
33
41
|
const open = useUiStore((s) => s.sourcesOpen);
|
|
34
42
|
const setOpen = useUiStore((s) => s.setSourcesOpen);
|
|
35
43
|
const now = useNow();
|
|
@@ -48,10 +56,10 @@ export function SourcesPanel() {
|
|
|
48
56
|
</Dialog.Close>
|
|
49
57
|
</div>
|
|
50
58
|
<p className={styles.lead}>
|
|
51
|
-
{contributing} of {plural(layers.length, '
|
|
59
|
+
{contributing} of {plural(layers.length, 'layer', 'layers')} contribute. For each check and environment, the newest completed result is shown. A layer fills only the checks it ran, so the rest of the report stays in place. Switch a source off to see an earlier result.
|
|
52
60
|
</p>
|
|
53
61
|
{excludedCount(excluded) > 0 && (
|
|
54
|
-
<button type="button" className={styles.reset} onClick={() => setExcluded(
|
|
62
|
+
<button type="button" className={styles.reset} onClick={() => setExcluded(ALL_SOURCES)}>
|
|
55
63
|
Use every source
|
|
56
64
|
</button>
|
|
57
65
|
)}
|
|
@@ -71,42 +79,52 @@ export function SourcesPanel() {
|
|
|
71
79
|
<label className={styles.option}>
|
|
72
80
|
<input type="checkbox" checked={!excluded.backends.includes(backend.id)} onChange={() => setExcluded({ ...excluded, backends: toggled(excluded.backends, backend.id) })} />
|
|
73
81
|
<span className={styles.optionName}>{backend.label}</span>
|
|
74
|
-
<span className={styles.optionMeta}>{backend.available ? plural(layerCount(backend.id), '
|
|
82
|
+
<span className={styles.optionMeta}>{backend.available ? plural(layerCount(backend.id), 'layer', 'layers') : 'Unavailable'}</span>
|
|
75
83
|
</label>
|
|
76
84
|
</li>
|
|
77
85
|
))}
|
|
78
86
|
</ul>
|
|
79
87
|
</section>
|
|
80
88
|
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
{facets.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
<FacetGroup
|
|
90
|
+
title="Environments"
|
|
91
|
+
facets={facets.environments.filter((facet) => facets.environments.length > 1 || facet.id !== 'unspecified')}
|
|
92
|
+
excluded={excluded.environments}
|
|
93
|
+
label={(id) => environmentLabel(id === 'unspecified' ? null : id)}
|
|
94
|
+
empty={null}
|
|
95
|
+
now={now}
|
|
96
|
+
onToggle={(id) => setExcluded({ ...excluded, environments: toggled(excluded.environments, id) })}
|
|
97
|
+
onOnly={(id) => setExcluded({ ...excluded, environments: facets.environments.map((facet) => facet.id).filter((facet) => facet !== id) })}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
<FacetGroup
|
|
101
|
+
title="Branches"
|
|
102
|
+
facets={facets.branches}
|
|
103
|
+
excluded={excluded.branches}
|
|
104
|
+
label={(id) => id}
|
|
105
|
+
empty="No layers yet."
|
|
106
|
+
now={now}
|
|
107
|
+
onToggle={(id) => setExcluded({ ...excluded, branches: toggled(excluded.branches, id) })}
|
|
108
|
+
onOnly={(id) => setExcluded({ ...excluded, branches: facets.branches.map((facet) => facet.id).filter((facet) => facet !== id) })}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<FacetGroup
|
|
112
|
+
title="Versions"
|
|
113
|
+
facets={facets.versions}
|
|
114
|
+
excluded={excluded.versions}
|
|
115
|
+
label={versionLabel}
|
|
116
|
+
empty={null}
|
|
117
|
+
now={now}
|
|
118
|
+
onToggle={(id) => setExcluded({ ...excluded, versions: toggled(excluded.versions, id) })}
|
|
119
|
+
onOnly={(id) => setExcluded({ ...excluded, versions: facets.versions.map((facet) => facet.id).filter((facet) => facet !== id) })}
|
|
120
|
+
/>
|
|
103
121
|
|
|
104
122
|
<section className={styles.group}>
|
|
105
|
-
<h3>
|
|
123
|
+
<h3>Layers</h3>
|
|
106
124
|
<ul>
|
|
107
|
-
<Bounded items={layers} size={40} more="Earlier
|
|
125
|
+
<Bounded items={layers} size={40} more="Earlier layers">
|
|
108
126
|
{(layer) => {
|
|
109
|
-
const off =
|
|
127
|
+
const off = !contributes(layer, { ...excluded, layers: [] });
|
|
110
128
|
return (
|
|
111
129
|
<li key={`${layer.backend}|${layer.id}`}>
|
|
112
130
|
<label className={styles.option} data-off={off || undefined}>
|
|
@@ -115,6 +133,7 @@ export function SourcesPanel() {
|
|
|
115
133
|
{layer.packageDir ?? layer.producer}
|
|
116
134
|
<span className={styles.run}>
|
|
117
135
|
{layer.live ? 'Running now' : <time title={exactTime(layer.recordedAtMs)}>{ago(layer.recordedAtMs, now)}</time>} · {branchOf(layer)}
|
|
136
|
+
{layer.environment && <> · {environmentLabel(layer.environment)}</>}
|
|
118
137
|
{shortSha(layer.sha) && <> · <code>{shortSha(layer.sha)}</code></>}
|
|
119
138
|
{layer.freshness === 'modified' && ' · local changes'}
|
|
120
139
|
</span>
|
|
@@ -132,3 +151,38 @@ export function SourcesPanel() {
|
|
|
132
151
|
</Dialog.Root>
|
|
133
152
|
);
|
|
134
153
|
}
|
|
154
|
+
|
|
155
|
+
function FacetGroup({ title, facets, excluded, label, empty, now, onToggle, onOnly }: {
|
|
156
|
+
title: string;
|
|
157
|
+
facets: readonly SourceFacet[];
|
|
158
|
+
excluded: readonly string[];
|
|
159
|
+
label: (id: string) => string;
|
|
160
|
+
empty: string | null;
|
|
161
|
+
now: number;
|
|
162
|
+
onToggle: (id: string) => void;
|
|
163
|
+
onOnly: (id: string) => void;
|
|
164
|
+
}) {
|
|
165
|
+
if (!facets.length && empty === null) return null;
|
|
166
|
+
return (
|
|
167
|
+
<section className={styles.group}>
|
|
168
|
+
<h3>{title}</h3>
|
|
169
|
+
{facets.length === 0 && <p className={styles.empty}>{empty}</p>}
|
|
170
|
+
<ul>
|
|
171
|
+
{facets.map((facet) => (
|
|
172
|
+
<li key={facet.id}>
|
|
173
|
+
<label className={styles.option}>
|
|
174
|
+
<input type="checkbox" checked={!excluded.includes(facet.id)} onChange={() => onToggle(facet.id)} />
|
|
175
|
+
<span className={styles.optionName} title={facet.id}>{label(facet.id)}</span>
|
|
176
|
+
<span className={styles.optionMeta}>
|
|
177
|
+
{plural(facet.layers, 'layer', 'layers')} · {ago(facet.latestAtMs, now)}
|
|
178
|
+
</span>
|
|
179
|
+
</label>
|
|
180
|
+
{facets.length > 1 && (
|
|
181
|
+
<button type="button" className={styles.only} onClick={() => onOnly(facet.id)}>Only</button>
|
|
182
|
+
)}
|
|
183
|
+
</li>
|
|
184
|
+
))}
|
|
185
|
+
</ul>
|
|
186
|
+
</section>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AtlasChangeSchema, type AtlasChange } from '@crvouga/atlas/schema';
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
|
|
4
|
+
import { runtimeConfig, runtimeUrl } from '../runtime';
|
|
5
|
+
|
|
6
|
+
/** What the server tells a viewer as it happens. */
|
|
7
|
+
export type LiveEvent =
|
|
8
|
+
| { readonly type: 'open' }
|
|
9
|
+
| { readonly type: 'report'; readonly report: ReportAnnouncement }
|
|
10
|
+
| { readonly type: 'change'; readonly change: AtlasChange };
|
|
11
|
+
|
|
12
|
+
/** One announcement that results arrived: enough to say who sent them without asking again. */
|
|
13
|
+
export type ReportAnnouncement = {
|
|
14
|
+
readonly version: string;
|
|
15
|
+
readonly totalLayers: number;
|
|
16
|
+
readonly addedTotal: number;
|
|
17
|
+
readonly added: readonly {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly producer: string;
|
|
20
|
+
readonly environment?: string | null;
|
|
21
|
+
readonly branch?: string | null;
|
|
22
|
+
readonly counts?: Readonly<Record<string, number>>;
|
|
23
|
+
}[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** `connecting` before the first answer, `live` while the stream is open, `reconnecting` after it was lost. */
|
|
27
|
+
export type LiveStatus = 'off' | 'connecting' | 'live' | 'reconnecting';
|
|
28
|
+
|
|
29
|
+
type Listener = (event: LiveEvent) => void;
|
|
30
|
+
|
|
31
|
+
/** The server sends a comment every 15 seconds; this long without a word means the connection is dead. */
|
|
32
|
+
const SILENCE_MS = 45_000;
|
|
33
|
+
const BACKOFF_MS = [1_000, 2_000, 5_000, 10_000, 30_000] as const;
|
|
34
|
+
|
|
35
|
+
type Stream = {
|
|
36
|
+
readonly url: string;
|
|
37
|
+
readonly listeners: Set<Listener>;
|
|
38
|
+
source: EventSource | null;
|
|
39
|
+
status: LiveStatus;
|
|
40
|
+
attempts: number;
|
|
41
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
42
|
+
watchdog: ReturnType<typeof setTimeout> | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const streams = new Map<string, Stream>();
|
|
46
|
+
const statusListeners = new Set<() => void>();
|
|
47
|
+
|
|
48
|
+
function setStatus(stream: Stream, status: LiveStatus) {
|
|
49
|
+
if (stream.status === status) return;
|
|
50
|
+
stream.status = status;
|
|
51
|
+
for (const listener of statusListeners) listener();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function emit(stream: Stream, event: LiveEvent) {
|
|
55
|
+
for (const listener of [...stream.listeners]) {
|
|
56
|
+
try {
|
|
57
|
+
listener(event);
|
|
58
|
+
} catch {
|
|
59
|
+
// One listener's mistake must not stop the others hearing.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parse(data: string): unknown {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(data) as unknown;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function connect(stream: Stream) {
|
|
73
|
+
if (typeof EventSource === 'undefined') return;
|
|
74
|
+
stream.timer = null;
|
|
75
|
+
const source = new EventSource(stream.url);
|
|
76
|
+
stream.source = source;
|
|
77
|
+
const heard = () => {
|
|
78
|
+
if (stream.watchdog) clearTimeout(stream.watchdog);
|
|
79
|
+
stream.watchdog = setTimeout(() => lose(stream), SILENCE_MS);
|
|
80
|
+
};
|
|
81
|
+
source.addEventListener('open', () => {
|
|
82
|
+
heard();
|
|
83
|
+
stream.attempts = 0;
|
|
84
|
+
setStatus(stream, 'live');
|
|
85
|
+
// Anything may have happened while the stream was away, so a (re)connection reads everything again.
|
|
86
|
+
emit(stream, { type: 'open' });
|
|
87
|
+
});
|
|
88
|
+
source.addEventListener('hello', heard);
|
|
89
|
+
source.addEventListener('ping', heard);
|
|
90
|
+
source.addEventListener('report', (event) => {
|
|
91
|
+
heard();
|
|
92
|
+
const report = parse((event as MessageEvent<string>).data) as ReportAnnouncement | null;
|
|
93
|
+
if (report && typeof report.version === 'string') emit(stream, { type: 'report', report: { ...report, added: Array.isArray(report.added) ? report.added : [] } });
|
|
94
|
+
});
|
|
95
|
+
source.addEventListener('atlas:change', (event) => {
|
|
96
|
+
heard();
|
|
97
|
+
const change = AtlasChangeSchema.safeParse(parse((event as MessageEvent<string>).data));
|
|
98
|
+
if (change.success) emit(stream, { type: 'change', change: change.data });
|
|
99
|
+
});
|
|
100
|
+
// The server's heartbeat is a named `ping` event, so a quiet server is told apart from a dead connection.
|
|
101
|
+
source.addEventListener('error', () => {
|
|
102
|
+
setStatus(stream, stream.attempts === 0 && stream.status === 'connecting' ? 'connecting' : 'reconnecting');
|
|
103
|
+
// The browser retries a dropped stream by itself. A refused one (401, 503) it gives up on: retry that here, slowly.
|
|
104
|
+
if (source.readyState === EventSource.CLOSED) lose(stream);
|
|
105
|
+
});
|
|
106
|
+
heard();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function lose(stream: Stream) {
|
|
110
|
+
if (stream.watchdog) clearTimeout(stream.watchdog);
|
|
111
|
+
stream.watchdog = null;
|
|
112
|
+
stream.source?.close();
|
|
113
|
+
stream.source = null;
|
|
114
|
+
if (!stream.listeners.size) return;
|
|
115
|
+
setStatus(stream, 'reconnecting');
|
|
116
|
+
if (stream.timer) return;
|
|
117
|
+
const wait = BACKOFF_MS[Math.min(stream.attempts, BACKOFF_MS.length - 1)]!;
|
|
118
|
+
stream.attempts += 1;
|
|
119
|
+
stream.timer = setTimeout(() => connect(stream), wait);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function release(stream: Stream) {
|
|
123
|
+
if (stream.listeners.size) return;
|
|
124
|
+
if (stream.timer) clearTimeout(stream.timer);
|
|
125
|
+
if (stream.watchdog) clearTimeout(stream.watchdog);
|
|
126
|
+
stream.source?.close();
|
|
127
|
+
streams.delete(stream.url);
|
|
128
|
+
setStatus(stream, 'off');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Hear what the server at `url` announces. Everything in the page that listens to one address shares one
|
|
133
|
+
* connection, which is opened with the first listener and closed with the last.
|
|
134
|
+
*/
|
|
135
|
+
export function listenLive(url: string, listener: Listener): () => void {
|
|
136
|
+
let stream = streams.get(url);
|
|
137
|
+
if (!stream) {
|
|
138
|
+
stream = { url, listeners: new Set(), source: null, status: 'connecting', attempts: 0, timer: null, watchdog: null };
|
|
139
|
+
streams.set(url, stream);
|
|
140
|
+
stream.listeners.add(listener);
|
|
141
|
+
connect(stream);
|
|
142
|
+
for (const notify of statusListeners) notify();
|
|
143
|
+
} else {
|
|
144
|
+
stream.listeners.add(listener);
|
|
145
|
+
}
|
|
146
|
+
const mine = stream;
|
|
147
|
+
return () => {
|
|
148
|
+
mine.listeners.delete(listener);
|
|
149
|
+
release(mine);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Changes to specs and runs, in the form the data layer already understands. */
|
|
154
|
+
export function listenChanges(url: string, onChange: (change: AtlasChange) => void): () => void {
|
|
155
|
+
return listenLive(url, (event) => {
|
|
156
|
+
if (event.type === 'change') onChange(event.change);
|
|
157
|
+
// A fresh connection may have missed changes while it was away.
|
|
158
|
+
if (event.type === 'open') onChange({ scope: 'runs', files: [], runIds: [] });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The address of this server's event stream, when the page has one. */
|
|
163
|
+
export const eventsAddress = () => runtimeUrl(runtimeConfig()?.eventsUrl);
|
|
164
|
+
|
|
165
|
+
function snapshot(): LiveStatus {
|
|
166
|
+
const url = eventsAddress();
|
|
167
|
+
return (url && streams.get(url)?.status) || (url ? 'connecting' : 'off');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function subscribeStatus(listener: () => void) {
|
|
171
|
+
statusListeners.add(listener);
|
|
172
|
+
return () => statusListeners.delete(listener);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Whether the report on screen is being kept up to date by the server. `off` for a static site. */
|
|
176
|
+
export function useLiveStatus(): LiveStatus {
|
|
177
|
+
return useSyncExternalStore(subscribeStatus, snapshot, () => 'off');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Test seam: forget every stream. */
|
|
181
|
+
export function resetLiveStreams() {
|
|
182
|
+
for (const stream of streams.values()) {
|
|
183
|
+
stream.listeners.clear();
|
|
184
|
+
release(stream);
|
|
185
|
+
}
|
|
186
|
+
streams.clear();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `act` at once, and then no more than once per `ms` however often it is called: a call made inside the wait
|
|
191
|
+
* runs when the wait ends. The last word is never lost.
|
|
192
|
+
*/
|
|
193
|
+
export function throttled(act: () => void, ms: number): (() => void) & { cancel: () => void } {
|
|
194
|
+
let last = -Infinity;
|
|
195
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
196
|
+
const call = () => {
|
|
197
|
+
if (timer) return;
|
|
198
|
+
const wait = last + ms - Date.now();
|
|
199
|
+
if (wait <= 0) { last = Date.now(); act(); return; }
|
|
200
|
+
timer = setTimeout(() => { timer = null; last = Date.now(); act(); }, wait);
|
|
201
|
+
};
|
|
202
|
+
call.cancel = () => { if (timer) clearTimeout(timer); timer = null; };
|
|
203
|
+
return call;
|
|
204
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The address book an Atlas server writes into the page it serves (`window.__ATLAS_CONFIG__`), so one build
|
|
3
|
+
* of the viewer works for every project and on every host. Under `atlas dev` the Vite dev server writes the
|
|
4
|
+
* same thing, so the viewer cannot tell the two apart: both are one host, with `/api` behind it.
|
|
5
|
+
*/
|
|
6
|
+
export type RuntimeConfig = {
|
|
7
|
+
readonly api?: string;
|
|
8
|
+
readonly reportsUrl?: string;
|
|
9
|
+
readonly dataUrl?: string;
|
|
10
|
+
readonly eventsUrl?: string;
|
|
11
|
+
readonly effective?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const runtimeConfig = (): RuntimeConfig | undefined => (globalThis as { __ATLAS_CONFIG__?: RuntimeConfig }).__ATLAS_CONFIG__;
|
|
15
|
+
|
|
16
|
+
/** An address from the config, resolved against the page, as an absolute URL. */
|
|
17
|
+
export function runtimeUrl(value: string | undefined): string | undefined {
|
|
18
|
+
return value ? new URL(value, window.location.href).href : undefined;
|
|
19
|
+
}
|
|
@@ -11,7 +11,7 @@ type UiState = {
|
|
|
11
11
|
issuesOpen: boolean;
|
|
12
12
|
sourcesOpen: boolean;
|
|
13
13
|
resultFilter: ResultFilter;
|
|
14
|
-
/** Backends, branches and layers left out of the combined report. */
|
|
14
|
+
/** Backends, branches, versions, environments and layers left out of the combined report. */
|
|
15
15
|
excluded: SourceSelection;
|
|
16
16
|
saveViewport: (chartId: string, viewport: Viewport) => void;
|
|
17
17
|
setIssuesOpen: (open: boolean) => void;
|
|
@@ -38,7 +38,11 @@ export const useUiStore = create<UiState>()(
|
|
|
38
38
|
{
|
|
39
39
|
name: 'atlas-ui',
|
|
40
40
|
storage: createJSONStorage(() => sessionStorage),
|
|
41
|
-
partialize: (s) => ({ viewports: s.viewports, excluded: s.excluded, resultFilter: s.resultFilter })
|
|
41
|
+
partialize: (s) => ({ viewports: s.viewports, excluded: s.excluded, resultFilter: s.resultFilter }),
|
|
42
|
+
merge: (persisted, current) => {
|
|
43
|
+
const saved = (persisted ?? {}) as Partial<UiState>;
|
|
44
|
+
return { ...current, ...saved, excluded: { ...ALL_SOURCES, ...saved.excluded } };
|
|
45
|
+
}
|
|
42
46
|
}
|
|
43
47
|
)
|
|
44
48
|
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Plugin, ProxyOptions } from 'vite';
|
|
2
|
+
|
|
3
|
+
/** What the viewer is told about the server it is served with: the same words `atlas serve` writes into its page. */
|
|
4
|
+
export const serverPageConfig = { api: './api', reportsUrl: './api/reports', dataUrl: './api/data/', eventsUrl: './api/events', effective: true } as const;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Put the Atlas server behind the dev server. The browser talks to one host; `/api` goes to the server, so the
|
|
8
|
+
* page, its data and its event stream have one origin in development exactly as they do when deployed.
|
|
9
|
+
*/
|
|
10
|
+
export function atlasServerProxy(target: string): { readonly proxy: Record<string, ProxyOptions> } {
|
|
11
|
+
return {
|
|
12
|
+
proxy: {
|
|
13
|
+
'/api': {
|
|
14
|
+
target,
|
|
15
|
+
changeOrigin: false,
|
|
16
|
+
ws: false,
|
|
17
|
+
// The event stream stays open for hours: no timeout, and nothing may hold its bytes back.
|
|
18
|
+
timeout: 0,
|
|
19
|
+
proxyTimeout: 0,
|
|
20
|
+
configure: (proxy) => {
|
|
21
|
+
proxy.on('proxyRes', (response) => {
|
|
22
|
+
if (String(response.headers['content-type'] ?? '').startsWith('text/event-stream')) response.headers['cache-control'] = 'no-cache, no-transform';
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Write the server's address book into the page the dev server serves. */
|
|
31
|
+
export function atlasServerPage(): Plugin {
|
|
32
|
+
return {
|
|
33
|
+
name: 'atlas-server-page',
|
|
34
|
+
transformIndexHtml: () => [{ tag: 'script', children: `window.__ATLAS_CONFIG__=${JSON.stringify(serverPageConfig)}`, injectTo: 'head-prepend' }]
|
|
35
|
+
};
|
|
36
|
+
}
|