@crvouga/atlas 0.3.0 → 0.4.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 +6 -0
- package/README.md +15 -2
- package/dist/reports/src/evidence-live.d.ts +6 -0
- package/dist/reports/src/evidence-live.js +35 -3
- package/dist/reports/src/evidence-types.d.ts +2 -0
- package/dist/reports/src/evidence-view.d.ts +15 -2
- package/dist/reports/src/evidence-view.js +31 -3
- package/dist/reports/src/layer-axes.d.ts +18 -0
- package/dist/reports/src/layer-axes.js +51 -0
- package/dist/reports/src/layer-catalog.js +1 -1
- package/dist/reports/src/layer-merge.d.ts +1 -1
- package/dist/reports/src/layer-merge.js +9 -4
- package/dist/reports/src/layer-source.js +3 -0
- package/dist/reports/src/layer-types.d.ts +2 -0
- package/dist/reports/src/layers-route.js +2 -5
- package/dist/reports/src/protocol.d.ts +4 -0
- package/dist/reports/src/publish.d.ts +2 -0
- package/dist/reports/src/publish.js +4 -0
- package/dist/reports/src/report-parts.js +3 -0
- package/package.json +2 -2
- package/packages/reports/README.md +4 -2
- package/packages/reports/STORAGE.md +1 -1
- package/packages/visualizer/README.md +6 -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/components/Header.tsx +1 -1
- package/packages/visualizer/src/evidence/check-detail.tsx +35 -3
- 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/state/ui-store.ts +6 -2
- package/packages/visualizer/vite/example-reports.ts +475 -0
- package/packages/visualizer/vite.config.ts +9 -1
|
@@ -5,8 +5,7 @@ import { publishData } from '../vite/publish';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* `pnpm publish-data`: the spec and the newest runs into `public/data` (or `ATLAS_OUT`), where
|
|
8
|
-
* `pnpm build` picks them up. Roots come from `ATLAS_SPECS_ROOT` / `ATLAS_RUNS_ROOT
|
|
9
|
-
* `ATLAS_FIXTURE`, as for the dev server.
|
|
8
|
+
* `pnpm build` picks them up. Roots come from `ATLAS_SPECS_ROOT` / `ATLAS_RUNS_ROOT`, as for the dev server.
|
|
10
9
|
*/
|
|
11
10
|
try {
|
|
12
11
|
const roots = resolveRoots();
|
|
@@ -77,7 +77,7 @@ function SourcesButton() {
|
|
|
77
77
|
return (
|
|
78
78
|
<button type="button" className={styles.sources} data-filtered={off > 0 || undefined} onClick={() => setOpen(true)}>
|
|
79
79
|
Sources
|
|
80
|
-
<span>{unavailable ? 'unavailable' : off > 0 ? `${off} off` : plural(layers.length, '
|
|
80
|
+
<span>{unavailable ? 'unavailable' : off > 0 ? `${off} off` : plural(layers.length, 'layer', 'layers')}</span>
|
|
81
81
|
</button>
|
|
82
82
|
);
|
|
83
83
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { recordingPaths } from '@crvouga/atlas/reports/evidence-types';
|
|
2
2
|
import { checkHistory, type CheckSlot, type HistoryEntry } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
4
|
+
import type { EvidenceResult } from '@crvouga/atlas/reports/layer-merge';
|
|
3
5
|
import { flattenFrames } from '@crvouga/atlas/reports/frames';
|
|
4
6
|
import type { ReportStep, RunReport } from '@crvouga/atlas/reports/report-types';
|
|
5
7
|
import { useQuery } from '@tanstack/react-query';
|
|
@@ -34,15 +36,45 @@ export function useRecordedReport(entry: HistoryEntry | null, enabled = true) {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/** One check in a list: its outcome at a glance, and everything recorded for it when opened. */
|
|
39
|
+
const ENV_RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 } as const;
|
|
40
|
+
|
|
41
|
+
/** One mark per client when the same check was recorded on more than one. */
|
|
42
|
+
function environmentMarks(slot: CheckSlot) {
|
|
43
|
+
const seen = new Map<string, CheckSlot['status']>();
|
|
44
|
+
for (const outcome of slot.outcomes) {
|
|
45
|
+
const id = outcome.layer.environment ?? '';
|
|
46
|
+
const prior = seen.get(id);
|
|
47
|
+
if (prior === undefined || ENV_RANK[outcome.row.status] > ENV_RANK[prior]) seen.set(id, outcome.row.status);
|
|
48
|
+
}
|
|
49
|
+
if (seen.size < 2) return [];
|
|
50
|
+
return [...seen].map(([id, status]) => ({ id, status }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function outcomeName(outcome: EvidenceResult) {
|
|
54
|
+
const environment = outcome.layer.environment ? environmentLabel(outcome.layer.environment) : '';
|
|
55
|
+
const parameters = Object.entries(outcome.parameters).map(([name, value]) => `${name}: ${value}`).join(', ');
|
|
56
|
+
return [environment, parameters].filter(Boolean).join(' · ') || outcome.layer.producer;
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
export function ResultRow({ label, slot, open, onToggle, children }: { label: string; slot: CheckSlot | null; open: boolean; onToggle: () => void; children: ReactNode }) {
|
|
38
60
|
const now = useNow();
|
|
39
61
|
const newest = slot?.outcomes[0];
|
|
62
|
+
const marks = slot ? environmentMarks(slot) : [];
|
|
40
63
|
const failure = slot?.status === 'failed' ? slot.outcomes.find((outcome) => outcome.row.status === 'failed')?.row.detail.split('\n')[0] : '';
|
|
41
64
|
return (
|
|
42
65
|
<li className={styles.result} data-status={slot?.status ?? 'none'} data-open={open || undefined}>
|
|
43
66
|
<button type="button" className={styles.resultHead} aria-expanded={open} onClick={onToggle}>
|
|
44
67
|
<StatusMark status={slot?.status ?? 'none'} />
|
|
45
|
-
<span className={styles.resultName}>
|
|
68
|
+
<span className={styles.resultName}>
|
|
69
|
+
{label}
|
|
70
|
+
{marks.length > 0 && (
|
|
71
|
+
<span className={styles.envMarks}>
|
|
72
|
+
{marks.map((mark) => (
|
|
73
|
+
<span key={mark.id || 'unspecified'} data-status={mark.status}>{environmentLabel(mark.id || null)}</span>
|
|
74
|
+
))}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</span>
|
|
46
78
|
{slot?.live && <span className={styles.liveTag}>Running now</span>}
|
|
47
79
|
<span className={styles.resultAside}>
|
|
48
80
|
{newest ? [duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
|
|
@@ -185,11 +217,11 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
|
185
217
|
return (
|
|
186
218
|
<div className={styles.detail}>
|
|
187
219
|
{slot.outcomes.length > 1 && !earlier && (
|
|
188
|
-
<div className={styles.variants} role="group" aria-label="
|
|
220
|
+
<div className={styles.variants} role="group" aria-label="Environments">
|
|
189
221
|
{slot.outcomes.map((outcome, index) => (
|
|
190
222
|
<button key={outcome.key} type="button" aria-pressed={index === variant} onClick={() => setVariant(index)}>
|
|
191
223
|
<StatusMark status={outcome.row.status} size={12} />
|
|
192
|
-
{
|
|
224
|
+
{outcomeName(outcome)}
|
|
193
225
|
</button>
|
|
194
226
|
))}
|
|
195
227
|
</div>
|
|
@@ -455,11 +455,31 @@
|
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
.resultName {
|
|
458
|
+
display: flex;
|
|
458
459
|
flex: 1;
|
|
460
|
+
flex-direction: column;
|
|
459
461
|
min-width: 0;
|
|
460
462
|
font-weight: 500;
|
|
461
463
|
}
|
|
462
464
|
|
|
465
|
+
.envMarks {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-wrap: wrap;
|
|
468
|
+
gap: 8px;
|
|
469
|
+
margin-top: 2px;
|
|
470
|
+
color: var(--ink-3);
|
|
471
|
+
font-size: 0.75rem;
|
|
472
|
+
font-weight: 400;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.envMarks span[data-status='passed'] {
|
|
476
|
+
color: var(--passed-ink);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.envMarks span[data-status='failed'] {
|
|
480
|
+
color: var(--failed-ink);
|
|
481
|
+
}
|
|
482
|
+
|
|
463
483
|
.result[data-status='none'] .resultName {
|
|
464
484
|
color: var(--ink-3);
|
|
465
485
|
font-weight: 400;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BackendInfo, LayerHeader, StatusCounts } from '@crvouga/atlas/reports/evidence-types';
|
|
2
2
|
import type { SpecStatus, SpecSummary } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
3
4
|
import { useState, type ReactNode } from 'react';
|
|
4
5
|
|
|
5
6
|
import styles from './evidence.module.css';
|
|
@@ -89,6 +90,7 @@ export function Origin({ layer, backends, now }: { layer: LayerHeader; backends:
|
|
|
89
90
|
{layer.live && <span className={styles.liveTag}>Running now</span>}
|
|
90
91
|
<span>{backendLabel(backends, layer.backend)}</span>
|
|
91
92
|
{layer.branch && <span>{layer.branch}</span>}
|
|
93
|
+
{layer.environment && <span>{environmentLabel(layer.environment)}</span>}
|
|
92
94
|
{sha && <code>{sha}</code>}
|
|
93
95
|
{layer.freshness === 'modified' && <span>with local changes</span>}
|
|
94
96
|
{layer.freshness === 'unknown' && <span>origin not captured</span>}
|
|
@@ -67,7 +67,7 @@ export function Overview({ kind }: { kind: SpecKind }) {
|
|
|
67
67
|
</p>
|
|
68
68
|
))}
|
|
69
69
|
{!evidence.ready && !evidence.unavailable && <p className={styles.aside} role="status">Loading recorded results…</p>}
|
|
70
|
-
{evidence.ready && evidence.pending > 0 && <p className={styles.aside} role="status">Reading {plural(evidence.pending, 'more
|
|
70
|
+
{evidence.ready && evidence.pending > 0 && <p className={styles.aside} role="status">Reading {plural(evidence.pending, 'more layer', 'more layers')}…</p>}
|
|
71
71
|
|
|
72
72
|
{evidence.active.length > 0 && (
|
|
73
73
|
<section className={styles.block}>
|
|
@@ -111,13 +111,13 @@ export function Overview({ kind }: { kind: SpecKind }) {
|
|
|
111
111
|
<p className={styles.aside}>
|
|
112
112
|
{evidence.layers.length === 0
|
|
113
113
|
? `Nothing has been recorded yet. Results appear here as a spec run finishes each test.`
|
|
114
|
-
: `None of the ${plural(evidence.layers.length, '
|
|
114
|
+
: `None of the ${plural(evidence.layers.length, 'layer', 'layers')} in the combined report checked a ${kind.one}.`}
|
|
115
115
|
</p>
|
|
116
116
|
)}
|
|
117
117
|
|
|
118
118
|
{evidence.ready && (
|
|
119
119
|
<p className={styles.footLine}>
|
|
120
|
-
{plural(evidence.layers.length, '
|
|
120
|
+
{plural(evidence.layers.length, 'layer', 'layers')} from {evidence.backends.filter((backend) => backend.available).map((backend) => backendLabel(evidence.backends, backend.id)).join(' and ') || 'no report store'}.{' '}
|
|
121
121
|
<button type="button" className={styles.link} onClick={() => setSourcesOpen(true)}>Choose sources</button>
|
|
122
122
|
{empty > 0 && (
|
|
123
123
|
<>
|
|
@@ -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
|
+
}
|
|
@@ -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
|
);
|