@crvouga/atlas 0.10.2 → 0.12.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 +7 -0
- package/dist/reports/src/catalog-health.d.ts +142 -0
- package/dist/reports/src/catalog-health.js +257 -0
- package/dist/reports/src/test-catalog.d.ts +8 -0
- package/dist/reports/src/test-catalog.js +5 -1
- package/dist/schema/src/api.d.ts +79 -0
- package/dist/schema/src/api.js +50 -0
- package/dist/server/src/app.js +9 -0
- package/dist/server/src/llms.js +1 -0
- package/dist/server/src/openapi.js +18 -2
- package/dist/server/src/settings-page.d.ts +4 -4
- package/dist/server/src/settings-page.js +17 -78
- package/docs/reference/http-api.md +34 -0
- package/docs/reference/reports.md +224 -0
- package/docs/reference/schema.md +10 -0
- package/docs/reference/server.md +4 -4
- package/docs/server.md +10 -0
- package/docs/ui.md +142 -0
- package/docs/workspaces.md +130 -0
- package/llms-full.txt +322 -0
- package/llms.txt +2 -0
- package/package.json +17 -10
- package/packages/schema/schemas/catalog-health.schema.json +400 -0
- package/packages/server/openapi.json +596 -0
- package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
- package/packages/visualizer/src/admin/main.tsx +11 -0
- package/packages/visualizer/src/app/AppShell.tsx +62 -38
- package/packages/visualizer/src/app/gazetteer.tsx +90 -83
- package/packages/visualizer/src/app/router.tsx +33 -15
- package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
- package/packages/visualizer/src/components/DataIssues.tsx +36 -54
- package/packages/visualizer/src/components/Header.tsx +197 -125
- package/packages/visualizer/src/components/Legend.tsx +13 -41
- package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
- package/packages/visualizer/src/components/Screen.tsx +80 -71
- package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
- package/packages/visualizer/src/components/Status.tsx +20 -34
- package/packages/visualizer/src/components/labels.tsx +8 -3
- package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
- package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
- package/packages/visualizer/src/components/spec-logos/README.md +23 -0
- package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
- package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
- package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
- package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
- package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
- package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
- package/packages/visualizer/src/evidence/document-page.tsx +51 -29
- package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
- package/packages/visualizer/src/evidence/marks.tsx +112 -79
- package/packages/visualizer/src/evidence/model-page.tsx +77 -46
- package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
- package/packages/visualizer/src/evidence/overview.tsx +137 -69
- package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
- package/packages/visualizer/src/evidence/recording.tsx +135 -68
- package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
- package/packages/visualizer/src/evidence/source-text.tsx +33 -0
- package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
- package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
- package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
- package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
- package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
- package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
- package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
- package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
- package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
- package/packages/visualizer/src/explorer/parts.tsx +35 -13
- package/packages/visualizer/src/find/Finder.tsx +106 -90
- package/packages/visualizer/src/find/open.ts +17 -20
- package/packages/visualizer/src/live/test-progress.tsx +60 -22
- package/packages/visualizer/src/looks/Stage.tsx +223 -110
- package/packages/visualizer/src/main.tsx +9 -11
- package/packages/visualizer/src/map/ChartMap.tsx +125 -69
- package/packages/visualizer/src/map/EventEdge.tsx +59 -37
- package/packages/visualizer/src/map/map.module.css +12 -546
- package/packages/visualizer/src/map/nodes.tsx +124 -70
- package/packages/visualizer/src/reachable.test.ts +3 -2
- package/packages/visualizer/src/spec-kind.ts +2 -2
- package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
- package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/ui/grammars/README.md +14 -0
- package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
- package/packages/visualizer/src/ui/grammars/quint.json +152 -0
- package/packages/visualizer/src/ui/highlight.ts +50 -0
- package/packages/visualizer/src/ui/theme.tsx +109 -0
- package/packages/visualizer/src/views/ChartPage.tsx +46 -26
- package/packages/visualizer/src/views/ChartTests.tsx +59 -22
- package/packages/visualizer/src/views/ProductPage.tsx +16 -15
- package/packages/visualizer/src/views/SidePanel.tsx +241 -131
- package/packages/visualizer/src/app/gazetteer.module.css +0 -107
- package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
- package/packages/visualizer/src/components/DataIssues.module.css +0 -136
- package/packages/visualizer/src/components/Header.module.css +0 -114
- package/packages/visualizer/src/components/Legend.module.css +0 -84
- package/packages/visualizer/src/components/Screen.module.css +0 -312
- package/packages/visualizer/src/components/Status.module.css +0 -116
- package/packages/visualizer/src/components/icons.tsx +0 -156
- package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
- package/packages/visualizer/src/evidence/evidence.module.css +0 -907
- package/packages/visualizer/src/evidence/recording.module.css +0 -460
- package/packages/visualizer/src/evidence/sources.module.css +0 -168
- package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
- package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
- package/packages/visualizer/src/find/Finder.module.css +0 -168
- package/packages/visualizer/src/looks/stage.module.css +0 -300
- package/packages/visualizer/src/styles/global.css +0 -105
- package/packages/visualizer/src/styles/tokens.css +0 -80
- package/packages/visualizer/src/views/views.module.css +0 -546
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { PHONE_H, PHONE_W } from '@crvouga/atlas/reports/phone';
|
|
2
2
|
import type { ReportSnapshotBlob, ReportStepHit } from '@crvouga/atlas/reports/report-types';
|
|
3
3
|
import { createSnapshotHost, paintSnapshotHost, type HitResult } from '@crvouga/atlas/reports/snapshot-surface';
|
|
4
|
+
import { Accordion, Anchor, Box, Button, Code, Image, Stack, Text } from '@mantine/core';
|
|
4
5
|
import { useQuery } from '@tanstack/react-query';
|
|
5
6
|
import { useEffect, useRef, useState } from 'react';
|
|
6
|
-
|
|
7
7
|
import { reportAssetUrl, reportResponse } from './api';
|
|
8
|
-
import styles from './recording.module.css';
|
|
9
|
-
import { traceStates } from './quint';
|
|
10
8
|
import { ModelTrace } from './model-trace';
|
|
11
|
-
|
|
9
|
+
import { traceStates } from './quint';
|
|
12
10
|
const SCALE = 0.72;
|
|
13
|
-
|
|
14
11
|
async function readBlob(url: string, blob: ReportSnapshotBlob, signal: AbortSignal) {
|
|
15
12
|
const response = await reportResponse(url, signal);
|
|
16
13
|
if (blob.media !== 'png' && blob.media !== 'bytes') return response.text();
|
|
@@ -19,12 +16,23 @@ async function readBlob(url: string, blob: ReportSnapshotBlob, signal: AbortSign
|
|
|
19
16
|
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
20
17
|
return `data:${response.headers.get('content-type') ?? 'application/octet-stream'};base64,${btoa(binary)}`;
|
|
21
18
|
}
|
|
22
|
-
|
|
23
19
|
/**
|
|
24
20
|
* A captured page, placed in this page as it was recorded. It sits in a shadow tree so its
|
|
25
21
|
* stylesheet and the viewer's never restyle each other; there is no frame to load.
|
|
26
22
|
*/
|
|
27
|
-
export function SnapshotPicture({
|
|
23
|
+
export function SnapshotPicture({
|
|
24
|
+
file,
|
|
25
|
+
html,
|
|
26
|
+
css,
|
|
27
|
+
hit,
|
|
28
|
+
who
|
|
29
|
+
}: {
|
|
30
|
+
file: string;
|
|
31
|
+
html: string;
|
|
32
|
+
css: string;
|
|
33
|
+
hit: ReportStepHit | null;
|
|
34
|
+
who: string;
|
|
35
|
+
}) {
|
|
28
36
|
const frame = useRef<HTMLDivElement>(null);
|
|
29
37
|
const [found, setFound] = useState<HitResult>('none');
|
|
30
38
|
useEffect(() => {
|
|
@@ -37,73 +45,121 @@ export function SnapshotPicture({ file, html, css, hit, who }: { file: string; h
|
|
|
37
45
|
}, [file, html, css, hit]);
|
|
38
46
|
return (
|
|
39
47
|
<>
|
|
40
|
-
<
|
|
41
|
-
{found === 'missing' &&
|
|
48
|
+
<Stack ref={frame} role="img" aria-label={`Recorded ${who}`} w={PHONE_W * SCALE} h={PHONE_H * SCALE} maw="100%" gap="sm" />
|
|
49
|
+
{found === 'missing' && (
|
|
50
|
+
<Text component="div" c="dimmed">
|
|
51
|
+
The recorded control cannot be found in this picture.
|
|
52
|
+
</Text>
|
|
53
|
+
)}
|
|
42
54
|
</>
|
|
43
55
|
);
|
|
44
56
|
}
|
|
45
|
-
|
|
46
57
|
/** One captured picture or value, exactly as recorded. */
|
|
47
|
-
export function Snapshot({
|
|
48
|
-
|
|
58
|
+
export function Snapshot({
|
|
59
|
+
url,
|
|
60
|
+
blob,
|
|
61
|
+
css,
|
|
62
|
+
hit,
|
|
63
|
+
earlier = false
|
|
64
|
+
}: {
|
|
65
|
+
url: string;
|
|
66
|
+
blob: ReportSnapshotBlob;
|
|
67
|
+
css: string;
|
|
68
|
+
hit: ReportStepHit | null;
|
|
69
|
+
earlier?: boolean;
|
|
70
|
+
}) {
|
|
71
|
+
const content = useQuery({
|
|
72
|
+
queryKey: ['evidence-blob', url],
|
|
73
|
+
queryFn: ({ signal }) => readBlob(url, blob, signal),
|
|
74
|
+
enabled: blob.media !== 'video',
|
|
75
|
+
retry: false
|
|
76
|
+
});
|
|
49
77
|
const who = blob.nodeId ?? blob.name;
|
|
50
78
|
const picture = blob.media === 'html' || blob.media === 'png' || blob.media === 'video';
|
|
51
79
|
return (
|
|
52
|
-
<
|
|
53
|
-
<figcaption>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
<Box data-picture={picture || undefined} component="figure" m={0}>
|
|
81
|
+
<Box component="figcaption">
|
|
82
|
+
<Text span inherit fw={600}>
|
|
83
|
+
{who}
|
|
84
|
+
</Text>
|
|
85
|
+
{blob.nodeId && blob.name !== blob.nodeId && (
|
|
86
|
+
<Text span inherit>
|
|
87
|
+
{blob.name}
|
|
88
|
+
</Text>
|
|
89
|
+
)}
|
|
90
|
+
{earlier && (
|
|
91
|
+
<Text span inherit>
|
|
92
|
+
Earlier picture
|
|
93
|
+
</Text>
|
|
94
|
+
)}
|
|
95
|
+
</Box>
|
|
96
|
+
{blob.media === 'video' && (
|
|
97
|
+
<Box src={reportAssetUrl(url)} controls playsInline aria-label={`Recorded ${who}`} component="video" maw="100%" w="100%" />
|
|
98
|
+
)}
|
|
99
|
+
{blob.media !== 'video' && content.isPending && (
|
|
100
|
+
<Text role="status" component="div" c="dimmed">
|
|
101
|
+
Loading…
|
|
102
|
+
</Text>
|
|
103
|
+
)}
|
|
60
104
|
{content.isError && (
|
|
61
|
-
<
|
|
105
|
+
<Text role="alert" component="div" c="dimmed">
|
|
62
106
|
{content.error.message}{' '}
|
|
63
|
-
<
|
|
64
|
-
|
|
107
|
+
<Button type="button" onClick={() => void content.refetch()} variant="subtle">
|
|
108
|
+
Try again
|
|
109
|
+
</Button>
|
|
110
|
+
</Text>
|
|
111
|
+
)}
|
|
112
|
+
{content.data !== undefined && blob.media === 'html' && (
|
|
113
|
+
<SnapshotPicture file={blob.file} html={content.data} css={css} hit={hit} who={who} />
|
|
114
|
+
)}
|
|
115
|
+
{content.data !== undefined && blob.media === 'png' && <Image alt={`Recorded ${who}`} src={content.data} fit="contain" maw="100%" />}
|
|
116
|
+
{content.data !== undefined && blob.media === 'bytes' && (
|
|
117
|
+
<Anchor download={blob.file.split('/').at(-1)} href={content.data}>
|
|
118
|
+
Download captured data
|
|
119
|
+
</Anchor>
|
|
65
120
|
)}
|
|
66
|
-
{content.data !== undefined && blob.media === 'html' && <SnapshotPicture file={blob.file} html={content.data} css={css} hit={hit} who={who} />}
|
|
67
|
-
{content.data !== undefined && blob.media === 'png' && <img className={styles.image} alt={`Recorded ${who}`} src={content.data} />}
|
|
68
|
-
{content.data !== undefined && blob.media === 'bytes' && <a download={blob.file.split('/').at(-1)} href={content.data}>Download captured data</a>}
|
|
69
121
|
{content.data !== undefined && (blob.media === 'json' || blob.media === 'text') && <SnapshotData text={content.data} />}
|
|
70
|
-
</
|
|
122
|
+
</Box>
|
|
71
123
|
);
|
|
72
124
|
}
|
|
73
|
-
|
|
74
125
|
/** A recorded value: a model trace when it is one, otherwise its fields. */
|
|
75
126
|
export function SnapshotData({ text }: { text: string }) {
|
|
76
127
|
let value: unknown;
|
|
77
128
|
try {
|
|
78
129
|
value = JSON.parse(text);
|
|
79
130
|
} catch {
|
|
80
|
-
return <
|
|
131
|
+
return <Code block>{text}</Code>;
|
|
81
132
|
}
|
|
82
133
|
const states = traceStates(value);
|
|
83
134
|
return states ? <ModelTrace states={states} /> : <DataValue value={value} />;
|
|
84
135
|
}
|
|
85
|
-
|
|
86
136
|
function DataValue({ value }: { value: unknown }) {
|
|
87
|
-
if (value === null || typeof value !== 'object') return <
|
|
137
|
+
if (value === null || typeof value !== 'object') return <Code>{JSON.stringify(value)}</Code>;
|
|
88
138
|
const entries = Object.entries(value);
|
|
89
|
-
if (!entries.length) return <
|
|
139
|
+
if (!entries.length) return <Code>{Array.isArray(value) ? '[]' : '{}'}</Code>;
|
|
90
140
|
return (
|
|
91
|
-
<
|
|
141
|
+
<Box component="dl">
|
|
92
142
|
{entries.map(([key, item]) => (
|
|
93
|
-
<
|
|
94
|
-
<dt>{key}</
|
|
95
|
-
<dd>
|
|
143
|
+
<Stack key={key} gap="sm">
|
|
144
|
+
<Box component="dt">{key}</Box>
|
|
145
|
+
<Box component="dd" m={0}>
|
|
96
146
|
{item !== null && typeof item === 'object' ? (
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
147
|
+
<Accordion variant="separated" my="sm">
|
|
148
|
+
<Accordion.Item value="content">
|
|
149
|
+
<Accordion.Control>
|
|
150
|
+
{Array.isArray(item) ? `${item.length} items` : `${Object.keys(item).length} fields`}
|
|
151
|
+
</Accordion.Control>
|
|
152
|
+
<Accordion.Panel>
|
|
153
|
+
<DataValue value={item} />
|
|
154
|
+
</Accordion.Panel>
|
|
155
|
+
</Accordion.Item>
|
|
156
|
+
</Accordion>
|
|
101
157
|
) : (
|
|
102
158
|
<DataValue value={item} />
|
|
103
159
|
)}
|
|
104
|
-
</
|
|
105
|
-
</
|
|
160
|
+
</Box>
|
|
161
|
+
</Stack>
|
|
106
162
|
))}
|
|
107
|
-
</
|
|
163
|
+
</Box>
|
|
108
164
|
);
|
|
109
165
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CodeHighlightAdapterProvider, CodeHighlightTabs } from '@mantine/code-highlight';
|
|
2
|
+
import { Box, Button, Group, Text, Title } from '@mantine/core';
|
|
3
|
+
import { specHighlightAdapter, specLanguage } from '../ui/highlight';
|
|
4
|
+
|
|
5
|
+
/** The written specification is primary: complete, expanded and highlighted, with Mantine code controls. */
|
|
6
|
+
export function SourceText({ text, file, withResults = false }: { text: string; file: string; withResults?: boolean }) {
|
|
7
|
+
return (
|
|
8
|
+
<Box component="section" data-source aria-label={`Source of ${file}`} mb="lg" miw={0}>
|
|
9
|
+
<Group justify="space-between" mb="sm">
|
|
10
|
+
<Title order={2}>Source</Title>
|
|
11
|
+
{withResults && (
|
|
12
|
+
<Button
|
|
13
|
+
variant="subtle"
|
|
14
|
+
onClick={() => {
|
|
15
|
+
const results = document.getElementById('spec-results');
|
|
16
|
+
results?.scrollIntoView({ block: 'start' });
|
|
17
|
+
results?.focus({ preventScroll: true });
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
Recorded results
|
|
21
|
+
</Button>
|
|
22
|
+
)}
|
|
23
|
+
</Group>
|
|
24
|
+
{text ? (
|
|
25
|
+
<CodeHighlightAdapterProvider adapter={specHighlightAdapter}>
|
|
26
|
+
<CodeHighlightTabs code={[{ fileName: file, code: text, language: specLanguage(file) }]} copyLabel={`Copy ${file}`} />
|
|
27
|
+
</CodeHighlightAdapterProvider>
|
|
28
|
+
) : (
|
|
29
|
+
<Text c="dimmed">This source file is empty.</Text>
|
|
30
|
+
)}
|
|
31
|
+
</Box>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
import type { LayerHeader } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ALL_SOURCES,
|
|
4
|
+
branchOf,
|
|
5
|
+
contributes,
|
|
6
|
+
sourceFacets,
|
|
7
|
+
sourceSelection,
|
|
8
|
+
type SourceFacet,
|
|
9
|
+
type SourceSelection
|
|
10
|
+
} from '@crvouga/atlas/reports/evidence-view';
|
|
3
11
|
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
4
|
-
import
|
|
12
|
+
import { Alert, Button, Checkbox, Drawer, Group, List, Stack, Text, Title } from '@mantine/core';
|
|
5
13
|
import { useMemo } from 'react';
|
|
6
|
-
|
|
7
|
-
import { CloseIcon } from '../components/icons';
|
|
8
14
|
import { useUiStore } from '../state/ui-store';
|
|
9
15
|
import { useEvidence } from './evidence-context';
|
|
10
|
-
import { ago,
|
|
16
|
+
import { ago, plural, shortSha, useNow } from './format';
|
|
11
17
|
import { Bounded } from './marks';
|
|
12
|
-
import styles from './sources.module.css';
|
|
13
|
-
|
|
14
18
|
function toggled(list: readonly string[], id: string) {
|
|
15
19
|
return list.includes(id) ? list.filter((entry) => entry !== id) : [...list, id];
|
|
16
20
|
}
|
|
17
|
-
|
|
18
21
|
function counts(layer: LayerHeader) {
|
|
19
22
|
const { passed, failed, running } = layer.counts;
|
|
20
|
-
return
|
|
23
|
+
return (
|
|
24
|
+
[failed ? `${failed} failed` : '', passed ? `${passed} passed` : '', running ? `${running} running` : ''].filter(Boolean).join(', ') ||
|
|
25
|
+
'nothing completed'
|
|
26
|
+
);
|
|
21
27
|
}
|
|
22
|
-
|
|
23
|
-
/** How many sources are switched off, for the button that opens the panel. */
|
|
28
|
+
/** Number of disabled source facets, including individually excluded layers. */
|
|
24
29
|
export function excludedCount(excluded: Partial<SourceSelection>) {
|
|
25
|
-
const
|
|
26
|
-
return
|
|
30
|
+
const s = sourceSelection(excluded);
|
|
31
|
+
return s.backends.length + s.branches.length + s.versions.length + s.environments.length + s.layers.length;
|
|
27
32
|
}
|
|
28
|
-
|
|
29
33
|
function versionLabel(id: string) {
|
|
30
34
|
return id === 'unknown' ? 'Unknown version' : id.slice(0, 7);
|
|
31
35
|
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
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.
|
|
36
|
-
*/
|
|
36
|
+
/** Every backend, environment, branch, version and layer contributes unless switched off here. */
|
|
37
37
|
export function SourcesPanel() {
|
|
38
38
|
const evidence = useEvidence();
|
|
39
39
|
const { layers, backends, setExcluded, notices, retry } = evidence;
|
|
@@ -45,114 +45,109 @@ export function SourcesPanel() {
|
|
|
45
45
|
const contributing = layers.filter((layer) => contributes(layer, excluded)).length;
|
|
46
46
|
const layerCount = (id: string) => facets.backends.find((facet) => facet.id === id)?.layers ?? 0;
|
|
47
47
|
return (
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
48
|
+
<Drawer opened={open} onClose={() => setOpen(false)} title="Sources" closeButtonProps={{ 'aria-label': 'Close sources' }}>
|
|
49
|
+
<Stack gap="lg">
|
|
50
|
+
<Text c="dimmed">
|
|
51
|
+
{contributing} of {plural(layers.length, 'layer', 'layers')} contribute. For each check and environment, the newest completed
|
|
52
|
+
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
|
|
53
|
+
earlier result.
|
|
54
|
+
</Text>
|
|
55
|
+
{excludedCount(excluded) > 0 && <Button onClick={() => setExcluded(ALL_SOURCES)}>Use every source</Button>}
|
|
56
|
+
{notices.map((notice) => (
|
|
57
|
+
<Alert key={notice} role="status">
|
|
58
|
+
{notice}{' '}
|
|
59
|
+
<Button variant="subtle" onClick={retry}>
|
|
60
|
+
Try again
|
|
61
|
+
</Button>
|
|
62
|
+
</Alert>
|
|
63
|
+
))}
|
|
64
|
+
<Stack gap="sm">
|
|
65
|
+
<Title order={3}>Backends</Title>
|
|
66
|
+
{!backends.length && <Text c="dimmed">No report store is configured.</Text>}
|
|
67
|
+
{backends.map((backend) => (
|
|
68
|
+
<Checkbox
|
|
69
|
+
key={backend.id}
|
|
70
|
+
label={backend.label}
|
|
71
|
+
description={backend.available ? plural(layerCount(backend.id), 'layer', 'layers') : 'Unavailable'}
|
|
72
|
+
checked={!excluded.backends.includes(backend.id)}
|
|
73
|
+
onChange={() => setExcluded({ ...excluded, backends: toggled(excluded.backends, backend.id) })}
|
|
74
|
+
/>
|
|
71
75
|
))}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{layer.live ? 'Running now' : <time title={exactTime(layer.recordedAtMs)}>{ago(layer.recordedAtMs, now)}</time>} · {branchOf(layer)}
|
|
136
|
-
{layer.environment && <> · {environmentLabel(layer.environment)}</>}
|
|
137
|
-
{shortSha(layer.sha) && <> · <code>{shortSha(layer.sha)}</code></>}
|
|
138
|
-
{layer.freshness === 'modified' && ' · local changes'}
|
|
139
|
-
</span>
|
|
140
|
-
</span>
|
|
141
|
-
<span className={styles.optionMeta} data-failed={layer.counts.failed > 0 || undefined}>{counts(layer)}</span>
|
|
142
|
-
</label>
|
|
143
|
-
</li>
|
|
144
|
-
);
|
|
145
|
-
}}
|
|
146
|
-
</Bounded>
|
|
147
|
-
</ul>
|
|
148
|
-
</section>
|
|
149
|
-
</Dialog.Content>
|
|
150
|
-
</Dialog.Portal>
|
|
151
|
-
</Dialog.Root>
|
|
76
|
+
</Stack>
|
|
77
|
+
<FacetGroup
|
|
78
|
+
title="Environments"
|
|
79
|
+
facets={facets.environments.filter((facet) => facets.environments.length > 1 || facet.id !== 'unspecified')}
|
|
80
|
+
excluded={excluded.environments}
|
|
81
|
+
label={(id) => environmentLabel(id === 'unspecified' ? null : id)}
|
|
82
|
+
empty={null}
|
|
83
|
+
now={now}
|
|
84
|
+
onToggle={(id) => setExcluded({ ...excluded, environments: toggled(excluded.environments, id) })}
|
|
85
|
+
onOnly={(id) =>
|
|
86
|
+
setExcluded({ ...excluded, environments: facets.environments.map((facet) => facet.id).filter((facet) => facet !== id) })
|
|
87
|
+
}
|
|
88
|
+
/>
|
|
89
|
+
<FacetGroup
|
|
90
|
+
title="Branches"
|
|
91
|
+
facets={facets.branches}
|
|
92
|
+
excluded={excluded.branches}
|
|
93
|
+
label={(id) => id}
|
|
94
|
+
empty="No layers yet."
|
|
95
|
+
now={now}
|
|
96
|
+
onToggle={(id) => setExcluded({ ...excluded, branches: toggled(excluded.branches, id) })}
|
|
97
|
+
onOnly={(id) => setExcluded({ ...excluded, branches: facets.branches.map((facet) => facet.id).filter((facet) => facet !== id) })}
|
|
98
|
+
/>
|
|
99
|
+
<FacetGroup
|
|
100
|
+
title="Versions"
|
|
101
|
+
facets={facets.versions}
|
|
102
|
+
excluded={excluded.versions}
|
|
103
|
+
label={versionLabel}
|
|
104
|
+
empty={null}
|
|
105
|
+
now={now}
|
|
106
|
+
onToggle={(id) => setExcluded({ ...excluded, versions: toggled(excluded.versions, id) })}
|
|
107
|
+
onOnly={(id) => setExcluded({ ...excluded, versions: facets.versions.map((facet) => facet.id).filter((facet) => facet !== id) })}
|
|
108
|
+
/>
|
|
109
|
+
<Stack gap="sm">
|
|
110
|
+
<Title order={3}>Layers</Title>
|
|
111
|
+
<List spacing="md" listStyleType="none">
|
|
112
|
+
<Bounded items={layers} size={40} more="Earlier layers">
|
|
113
|
+
{(layer) => {
|
|
114
|
+
const off = !contributes(layer, { ...excluded, layers: [] });
|
|
115
|
+
return (
|
|
116
|
+
<List.Item key={`${layer.backend}|${layer.id}`}>
|
|
117
|
+
<Checkbox
|
|
118
|
+
disabled={off}
|
|
119
|
+
checked={!off && !excluded.layers.includes(layer.id)}
|
|
120
|
+
onChange={() => setExcluded({ ...excluded, layers: toggled(excluded.layers, layer.id) })}
|
|
121
|
+
label={layer.packageDir ?? layer.producer}
|
|
122
|
+
description={
|
|
123
|
+
<>
|
|
124
|
+
{layer.live ? 'Running now' : ago(layer.recordedAtMs, now)} · {branchOf(layer)}
|
|
125
|
+
{layer.environment && ` · ${environmentLabel(layer.environment)}`}
|
|
126
|
+
{shortSha(layer.sha) && ` · ${shortSha(layer.sha)}`}
|
|
127
|
+
{layer.freshness === 'modified' && ' · local changes'} · {counts(layer)}
|
|
128
|
+
</>
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
</List.Item>
|
|
132
|
+
);
|
|
133
|
+
}}
|
|
134
|
+
</Bounded>
|
|
135
|
+
</List>
|
|
136
|
+
</Stack>
|
|
137
|
+
</Stack>
|
|
138
|
+
</Drawer>
|
|
152
139
|
);
|
|
153
140
|
}
|
|
154
|
-
|
|
155
|
-
|
|
141
|
+
function FacetGroup({
|
|
142
|
+
title,
|
|
143
|
+
facets,
|
|
144
|
+
excluded,
|
|
145
|
+
label,
|
|
146
|
+
empty,
|
|
147
|
+
now,
|
|
148
|
+
onToggle,
|
|
149
|
+
onOnly
|
|
150
|
+
}: {
|
|
156
151
|
title: string;
|
|
157
152
|
facets: readonly SourceFacet[];
|
|
158
153
|
excluded: readonly string[];
|
|
@@ -164,25 +159,24 @@ function FacetGroup({ title, facets, excluded, label, empty, now, onToggle, onOn
|
|
|
164
159
|
}) {
|
|
165
160
|
if (!facets.length && empty === null) return null;
|
|
166
161
|
return (
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
{facets.length
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
</section>
|
|
162
|
+
<Stack gap="sm">
|
|
163
|
+
<Title order={3}>{title}</Title>
|
|
164
|
+
{!facets.length && <Text c="dimmed">{empty}</Text>}
|
|
165
|
+
{facets.map((facet) => (
|
|
166
|
+
<Group key={facet.id} justify="space-between" wrap="nowrap">
|
|
167
|
+
<Checkbox
|
|
168
|
+
label={label(facet.id)}
|
|
169
|
+
description={`${plural(facet.layers, 'layer', 'layers')} · ${ago(facet.latestAtMs, now)}`}
|
|
170
|
+
checked={!excluded.includes(facet.id)}
|
|
171
|
+
onChange={() => onToggle(facet.id)}
|
|
172
|
+
/>
|
|
173
|
+
{facets.length > 1 && (
|
|
174
|
+
<Button variant="subtle" onClick={() => onOnly(facet.id)}>
|
|
175
|
+
Only
|
|
176
|
+
</Button>
|
|
177
|
+
)}
|
|
178
|
+
</Group>
|
|
179
|
+
))}
|
|
180
|
+
</Stack>
|
|
187
181
|
);
|
|
188
182
|
}
|