@crvouga/atlas 1.14.0 → 1.16.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 +14 -1
- package/dist/reports/src/catalog-health.d.ts +15 -2
- package/dist/reports/src/catalog-health.js +23 -2
- package/dist/reports/src/evidence-detail.d.ts +19 -0
- package/dist/reports/src/evidence-detail.js +31 -4
- package/dist/reports/src/evidence-types.d.ts +2 -2
- package/dist/reports/src/layer-merge.js +4 -3
- package/dist/reports/src/layer-store.d.ts +1 -1
- package/dist/reports/src/layer-store.js +4 -2
- package/dist/reports/src/layer-types.d.ts +2 -0
- package/dist/reports/src/run-compare.d.ts +182 -0
- package/dist/reports/src/run-compare.js +329 -0
- package/dist/reports/src/test-catalog.d.ts +8 -0
- package/dist/reports/src/test-catalog.js +14 -1
- package/dist/schema/src/api.d.ts +10 -0
- package/dist/schema/src/api.js +15 -5
- package/dist/schema/src/server-config.d.ts +3 -0
- package/dist/schema/src/server-config.js +5 -0
- package/dist/server/src/app.d.ts +2 -0
- package/dist/server/src/app.js +6 -2
- package/dist/server/src/config.js +4 -0
- package/dist/server/src/main.js +1 -0
- package/dist/server/src/openapi.js +2 -2
- package/docs/extending.md +1 -1
- package/docs/reference/http-api.md +4 -4
- package/docs/reference/reports.md +378 -5
- package/docs/reference/server.md +2 -0
- package/docs/server.md +11 -2
- package/docs/ui.md +49 -1
- package/llms-full.txt +65 -8
- package/package.json +1 -1
- package/packages/schema/schemas/catalog-health.schema.json +20 -0
- package/packages/schema/schemas/effective-delta.schema.json +2 -2
- package/packages/schema/schemas/effective-evidence.schema.json +2 -2
- package/packages/schema/schemas/evidence-page.schema.json +1 -1
- package/packages/schema/schemas/layer-rows.schema.json +1 -1
- package/packages/schema/schemas/report-run.schema.json +25 -3
- package/packages/schema/schemas/report-runs.schema.json +1 -1
- package/packages/schema/schemas/review-collection.schema.json +1 -1
- package/packages/schema/schemas/review-manifest.schema.json +1 -1
- package/packages/schema/schemas/server-config.schema.json +17 -0
- package/packages/schema/schemas/test-catalog.schema.json +1 -1
- package/packages/schema/schemas/test-history.schema.json +2 -2
- package/packages/server/openapi.json +63 -21
- package/packages/visualizer/src/explorer/ComparePair.tsx +597 -0
- package/packages/visualizer/src/explorer/CompareView.tsx +340 -0
- package/packages/visualizer/src/explorer/Explorer.tsx +13 -2
- package/packages/visualizer/src/explorer/HealthView.tsx +402 -0
- package/packages/visualizer/src/explorer/RunsView.tsx +10 -0
- package/packages/visualizer/src/explorer/catalog.tsx +33 -0
- package/packages/visualizer/src/explorer/playlist.tsx +1 -1
- package/packages/visualizer/src/explorer/search.ts +18 -3
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Compare view
|
|
3
|
+
*
|
|
4
|
+
* The Compare tab: two pinned runs, a baseline and a candidate, set side by side. Both are named in the address (`base`
|
|
5
|
+
* and `cand`), so a link reproduces the comparison and a newer run never replaces either; a pinned run that is no longer
|
|
6
|
+
* in the report is said to be missing. Behaviors are paired by the check they record, never by title, and each pair is
|
|
7
|
+
* in one group (newly failing, recovered, still failing, promise or conditions differ, not run, not in one run, same
|
|
8
|
+
* outcome), so a result that did not run is unknown rather than regressed or passed. Opening a behavior shows its promise,
|
|
9
|
+
* outcomes, recordings and figures (`ComparePair`).
|
|
10
|
+
*/
|
|
11
|
+
import { COMPARE_GROUP_TEXT, COMPARE_GROUPS, compareRuns, type CompareGroup, type ComparedResult } from '@crvouga/atlas/reports/run-compare';
|
|
12
|
+
import { formatMs, type RunDetail, type RunSummary } from '@crvouga/atlas/reports/test-catalog';
|
|
13
|
+
import { Alert, Anchor, Badge, Box, Button, Code, Drawer, Flex, Group, List, NativeSelect, Paper, SimpleGrid, Stack, Table, Text, Title, VisuallyHidden } from '@mantine/core';
|
|
14
|
+
import { IconArrowsExchange } from '@tabler/icons-react';
|
|
15
|
+
import { Link } from '@tanstack/react-router';
|
|
16
|
+
import { useMemo, useState } from 'react';
|
|
17
|
+
|
|
18
|
+
import { ago, duration, exactTime, plural, shortSha, statusWord, useNow } from '../evidence/format';
|
|
19
|
+
import { StatusMark } from '../evidence/marks';
|
|
20
|
+
import { useRunDetail, useRunList } from './catalog';
|
|
21
|
+
import { ComparePair } from './ComparePair';
|
|
22
|
+
import { patched, useExplorerNavigate, useExplorerSearch } from './navigate';
|
|
23
|
+
import { runName } from './parts';
|
|
24
|
+
|
|
25
|
+
const ROWS = 100;
|
|
26
|
+
|
|
27
|
+
const GROUP_COLOR: Record<CompareGroup, string> = {
|
|
28
|
+
regressed: 'red',
|
|
29
|
+
recovered: 'green',
|
|
30
|
+
'still-failing': 'red',
|
|
31
|
+
'not-comparable': 'orange',
|
|
32
|
+
unknown: 'gray',
|
|
33
|
+
removed: 'gray',
|
|
34
|
+
added: 'blue',
|
|
35
|
+
unchanged: 'gray'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** What a run is called: the title its producer gave it, else its CI run number or `branch@sha`. */
|
|
39
|
+
function nameOf(run: RunSummary): string {
|
|
40
|
+
return run.title ?? runName(run.key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function RunPicker({ label, value, runs, onChange }: { label: string; value: string | undefined; runs: readonly RunSummary[]; onChange: (key: string | undefined) => void }) {
|
|
44
|
+
const now = useNow();
|
|
45
|
+
const known = runs.some((run) => run.key === value);
|
|
46
|
+
return (
|
|
47
|
+
<NativeSelect
|
|
48
|
+
label={label}
|
|
49
|
+
value={value ?? ''}
|
|
50
|
+
onChange={(event) => onChange(event.currentTarget.value || undefined)}
|
|
51
|
+
data-compare-picker={label}
|
|
52
|
+
>
|
|
53
|
+
<option value="">Choose a run</option>
|
|
54
|
+
{value && !known && <option value={value}>{runName(value)} (not among the newest runs listed)</option>}
|
|
55
|
+
{runs.map((run) => (
|
|
56
|
+
<option key={run.key} value={run.key}>
|
|
57
|
+
{[nameOf(run), run.branch, shortSha(run.sha), ago(run.endedAtMs, now)].filter(Boolean).join(' · ')}
|
|
58
|
+
</option>
|
|
59
|
+
))}
|
|
60
|
+
</NativeSelect>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function RunCard({ role, run }: { role: string; run: RunSummary }) {
|
|
65
|
+
return (
|
|
66
|
+
<Paper withBorder p="md" component="section" aria-label={`${role} run`} data-compare-run={role.toLowerCase()}>
|
|
67
|
+
<Stack gap="xs">
|
|
68
|
+
<Title order={3}>
|
|
69
|
+
{role}: {nameOf(run)}
|
|
70
|
+
</Title>
|
|
71
|
+
<Text component="div" size="sm">
|
|
72
|
+
{[run.branch, run.sha ? shortSha(run.sha) : null].filter(Boolean).join(' @ ') || 'No branch or commit recorded'}
|
|
73
|
+
{run.freshness && (
|
|
74
|
+
<Text span inherit c="dimmed">
|
|
75
|
+
{' '}
|
|
76
|
+
{run.freshness === 'modified' ? 'with local changes' : 'origin not captured'}
|
|
77
|
+
</Text>
|
|
78
|
+
)}
|
|
79
|
+
</Text>
|
|
80
|
+
<Text component="div" size="sm" c="dimmed" title={exactTime(run.endedAtMs)}>
|
|
81
|
+
Ended {exactTime(run.endedAtMs)} · took {formatMs(run.spanMs)} · {plural(run.layers, 'layer', 'layers')}
|
|
82
|
+
</Text>
|
|
83
|
+
<Text component="div" size="sm">
|
|
84
|
+
{plural(run.tests, 'test', 'tests')}: {run.counts.passed} passed, {run.counts.failed} failed, {run.counts['not-run']} not run
|
|
85
|
+
{run.counts.running > 0 ? `, ${run.counts.running} running` : ''}
|
|
86
|
+
</Text>
|
|
87
|
+
<Anchor renderRoot={(props) => <Link {...props} to="/" search={patched({ tab: 'runs', openRun: run.key, pair: undefined })} resetScroll={false} activeOptions={{ exact: true }} />}>
|
|
88
|
+
Open this run
|
|
89
|
+
</Anchor>
|
|
90
|
+
</Stack>
|
|
91
|
+
</Paper>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Outcome({ result }: { result: ComparedResult['baseline'] }) {
|
|
96
|
+
if (!result)
|
|
97
|
+
return (
|
|
98
|
+
<Text span inherit c="dimmed" data-outcome="none">
|
|
99
|
+
No result
|
|
100
|
+
</Text>
|
|
101
|
+
);
|
|
102
|
+
return (
|
|
103
|
+
<Group gap="xs" wrap="nowrap" data-outcome={result.status}>
|
|
104
|
+
<StatusMark status={result.status} size={14} />
|
|
105
|
+
<Text span inherit>
|
|
106
|
+
{statusWord(result.status)}
|
|
107
|
+
</Text>
|
|
108
|
+
<Text span inherit c="dimmed" size="xs">
|
|
109
|
+
{duration(result.durationMs)}
|
|
110
|
+
</Text>
|
|
111
|
+
</Group>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function Row({ item }: { item: ComparedResult }) {
|
|
116
|
+
return (
|
|
117
|
+
<Table.Tr data-group={item.group}>
|
|
118
|
+
<Table.Td>
|
|
119
|
+
<Text component="div">{item.title}</Text>
|
|
120
|
+
{item.specPath && (
|
|
121
|
+
<Text component="div" size="xs" c="dimmed">
|
|
122
|
+
<Code>{item.specPath}</Code>
|
|
123
|
+
</Text>
|
|
124
|
+
)}
|
|
125
|
+
{item.conditions.length > 0 && (
|
|
126
|
+
<Badge variant="light" color="orange" tt="none" mt="xs" title={item.conditions.join('; ')}>
|
|
127
|
+
Conditions differ
|
|
128
|
+
</Badge>
|
|
129
|
+
)}
|
|
130
|
+
{item.promise.state === 'changed' && (
|
|
131
|
+
<Badge variant="light" color="orange" tt="none" mt="xs">
|
|
132
|
+
Promise changed
|
|
133
|
+
</Badge>
|
|
134
|
+
)}
|
|
135
|
+
</Table.Td>
|
|
136
|
+
<Table.Td>
|
|
137
|
+
<Outcome result={item.baseline} />
|
|
138
|
+
</Table.Td>
|
|
139
|
+
<Table.Td>
|
|
140
|
+
<Outcome result={item.candidate} />
|
|
141
|
+
</Table.Td>
|
|
142
|
+
<Table.Td>
|
|
143
|
+
<Badge color={GROUP_COLOR[item.group]} variant="light" tt="none">
|
|
144
|
+
{COMPARE_GROUP_TEXT[item.group].label}
|
|
145
|
+
</Badge>
|
|
146
|
+
<Text component="div" size="xs" c="dimmed" mt="xs">
|
|
147
|
+
{item.summary}
|
|
148
|
+
</Text>
|
|
149
|
+
</Table.Td>
|
|
150
|
+
<Table.Td>
|
|
151
|
+
<Button
|
|
152
|
+
variant="light"
|
|
153
|
+
aria-label={`Compare ${item.title}`}
|
|
154
|
+
renderRoot={(props) => <Link {...props} to="/" search={patched({ pair: item.id })} resetScroll={false} activeOptions={{ exact: true }} />}
|
|
155
|
+
>
|
|
156
|
+
Compare
|
|
157
|
+
</Button>
|
|
158
|
+
</Table.Td>
|
|
159
|
+
</Table.Tr>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function Missing({ side, keyed, onRetry }: { side: string; keyed: string; onRetry?: () => void }) {
|
|
164
|
+
return (
|
|
165
|
+
<Alert color="blue" role="status" title={`The ${side} run is not in the report`} data-compare-missing={side}>
|
|
166
|
+
<Stack gap="xs" align="flex-start">
|
|
167
|
+
<Text component="div">
|
|
168
|
+
<Code>{keyed}</Code> is pinned in the address, but the report holds no run with that key any more. Its source may be switched off or have expired. It is not replaced by another run.
|
|
169
|
+
</Text>
|
|
170
|
+
{onRetry && (
|
|
171
|
+
<Button type="button" variant="light" onClick={onRetry}>
|
|
172
|
+
Try again
|
|
173
|
+
</Button>
|
|
174
|
+
)}
|
|
175
|
+
</Stack>
|
|
176
|
+
</Alert>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function Comparison({ baseline, candidate }: { baseline: RunDetail; candidate: RunDetail }) {
|
|
181
|
+
const search = useExplorerSearch();
|
|
182
|
+
const go = useExplorerNavigate();
|
|
183
|
+
const comparison = useMemo(() => compareRuns(baseline, candidate), [baseline, candidate]);
|
|
184
|
+
const [shown, setShown] = useState(ROWS);
|
|
185
|
+
const group = COMPARE_GROUPS.find((name) => name === search.change);
|
|
186
|
+
const listed = group ? comparison.results.filter((item) => item.group === group) : comparison.results;
|
|
187
|
+
const opened = search.pair ? comparison.results.find((item) => item.id === search.pair) : undefined;
|
|
188
|
+
const total = comparison.results.length;
|
|
189
|
+
return (
|
|
190
|
+
<Stack gap="md">
|
|
191
|
+
{comparison.notes.length > 0 && (
|
|
192
|
+
<Alert color="blue" role="status" title="Before reading this comparison">
|
|
193
|
+
<List size="sm">
|
|
194
|
+
{comparison.notes.map((note) => (
|
|
195
|
+
<List.Item key={note}>{note}</List.Item>
|
|
196
|
+
))}
|
|
197
|
+
</List>
|
|
198
|
+
</Alert>
|
|
199
|
+
)}
|
|
200
|
+
<Text role="status" component="div" data-compare-summary>
|
|
201
|
+
{plural(total, 'behavior', 'behaviors')} across the two runs:{' '}
|
|
202
|
+
{COMPARE_GROUPS.filter((name) => comparison.counts[name] > 0)
|
|
203
|
+
.map((name) => `${comparison.counts[name]} ${COMPARE_GROUP_TEXT[name].label.toLowerCase()}`)
|
|
204
|
+
.join(', ') || 'none'}
|
|
205
|
+
.
|
|
206
|
+
</Text>
|
|
207
|
+
<Flex role="group" aria-label="Kind of change" gap="xs" align="center" wrap="wrap">
|
|
208
|
+
<Button type="button" variant="light" aria-pressed={!group} onClick={() => go({ change: undefined })}>
|
|
209
|
+
All {total}
|
|
210
|
+
</Button>
|
|
211
|
+
{COMPARE_GROUPS.filter((name) => comparison.counts[name] > 0).map((name) => (
|
|
212
|
+
<Button key={name} type="button" variant="light" aria-pressed={group === name} title={COMPARE_GROUP_TEXT[name].meaning} onClick={() => go({ change: group === name ? undefined : name })}>
|
|
213
|
+
{COMPARE_GROUP_TEXT[name].label} {comparison.counts[name]}
|
|
214
|
+
</Button>
|
|
215
|
+
))}
|
|
216
|
+
</Flex>
|
|
217
|
+
{group && (
|
|
218
|
+
<Text component="div" size="sm" c="dimmed">
|
|
219
|
+
{COMPARE_GROUP_TEXT[group].meaning}
|
|
220
|
+
</Text>
|
|
221
|
+
)}
|
|
222
|
+
{total === 0 ? (
|
|
223
|
+
<Text component="div" c="dimmed">
|
|
224
|
+
Neither run recorded a result, so there is nothing to compare.
|
|
225
|
+
</Text>
|
|
226
|
+
) : (
|
|
227
|
+
<Table.ScrollContainer minWidth={720}>
|
|
228
|
+
<Table data-compare-results>
|
|
229
|
+
<Table.Thead>
|
|
230
|
+
<Table.Tr>
|
|
231
|
+
<Table.Th scope="col">Behavior</Table.Th>
|
|
232
|
+
<Table.Th scope="col">Baseline</Table.Th>
|
|
233
|
+
<Table.Th scope="col">Candidate</Table.Th>
|
|
234
|
+
<Table.Th scope="col">Change</Table.Th>
|
|
235
|
+
<Table.Th scope="col">
|
|
236
|
+
<VisuallyHidden>Open</VisuallyHidden>
|
|
237
|
+
</Table.Th>
|
|
238
|
+
</Table.Tr>
|
|
239
|
+
</Table.Thead>
|
|
240
|
+
<Table.Tbody>
|
|
241
|
+
{listed.slice(0, shown).map((item) => (
|
|
242
|
+
<Row key={item.id} item={item} />
|
|
243
|
+
))}
|
|
244
|
+
</Table.Tbody>
|
|
245
|
+
</Table>
|
|
246
|
+
</Table.ScrollContainer>
|
|
247
|
+
)}
|
|
248
|
+
{listed.length > shown && (
|
|
249
|
+
<Button type="button" variant="subtle" onClick={() => setShown(shown + ROWS)}>
|
|
250
|
+
Show more ({listed.length - shown} more)
|
|
251
|
+
</Button>
|
|
252
|
+
)}
|
|
253
|
+
<Drawer opened={!!search.pair} onClose={() => go({ pair: undefined })} title="Compare a behavior" size="xl">
|
|
254
|
+
{opened ? (
|
|
255
|
+
<ComparePair item={opened} comparison={comparison} layers={{ baseline: baseline.layers, candidate: candidate.layers }} onClose={() => go({ pair: undefined })} />
|
|
256
|
+
) : (
|
|
257
|
+
<Alert color="blue" role="status">
|
|
258
|
+
That behavior is not in the two compared runs.
|
|
259
|
+
</Alert>
|
|
260
|
+
)}
|
|
261
|
+
</Drawer>
|
|
262
|
+
</Stack>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The Compare tab. */
|
|
267
|
+
export function CompareView() {
|
|
268
|
+
const search = useExplorerSearch();
|
|
269
|
+
const go = useExplorerNavigate();
|
|
270
|
+
const runs = useRunList(true, {});
|
|
271
|
+
const baseline = useRunDetail(search.base);
|
|
272
|
+
const candidate = useRunDetail(search.cand);
|
|
273
|
+
const picked = search.base && search.cand;
|
|
274
|
+
return (
|
|
275
|
+
<Box aria-label="Compare runs" component="section" mb="lg">
|
|
276
|
+
<Title order={2} mb="sm">
|
|
277
|
+
Compare two runs
|
|
278
|
+
</Title>
|
|
279
|
+
<Text component="div" c="dimmed" mb="md">
|
|
280
|
+
Choose a baseline and a candidate run. Behaviors are paired by the check they record, not by title. Both runs are pinned in the address, so a newer run never replaces either, and a link reproduces the comparison.
|
|
281
|
+
</Text>
|
|
282
|
+
<Group align="flex-end" gap="md" wrap="wrap" mb="md">
|
|
283
|
+
<RunPicker label="Baseline run" value={search.base} runs={runs.data?.runs ?? []} onChange={(key) => go({ base: key, pair: undefined, change: undefined })} />
|
|
284
|
+
<RunPicker label="Candidate run" value={search.cand} runs={runs.data?.runs ?? []} onChange={(key) => go({ cand: key, pair: undefined, change: undefined })} />
|
|
285
|
+
<Button
|
|
286
|
+
type="button"
|
|
287
|
+
variant="default"
|
|
288
|
+
leftSection={<IconArrowsExchange size={16} aria-hidden="true" />}
|
|
289
|
+
disabled={!search.base && !search.cand}
|
|
290
|
+
onClick={() => go({ base: search.cand, cand: search.base, pair: undefined, change: undefined })}
|
|
291
|
+
>
|
|
292
|
+
Swap baseline and candidate
|
|
293
|
+
</Button>
|
|
294
|
+
</Group>
|
|
295
|
+
{runs.isError && (
|
|
296
|
+
<Alert role="alert" color="red" my="sm">
|
|
297
|
+
The runs could not be listed. {runs.error.message}
|
|
298
|
+
</Alert>
|
|
299
|
+
)}
|
|
300
|
+
{!picked && (
|
|
301
|
+
<Text component="div" c="dimmed" role="status">
|
|
302
|
+
{runs.isPending ? 'Reading the runs…' : (runs.data?.runs.length ?? 0) < 1 ? 'No runs recorded yet, so there is nothing to compare.' : 'Choose both runs to compare them. You can also open a run in the Runs tab and use it as the baseline or the candidate.'}
|
|
303
|
+
</Text>
|
|
304
|
+
)}
|
|
305
|
+
{picked && (baseline.isPending || candidate.isPending) && (
|
|
306
|
+
<Text role="status" component="div" c="dimmed">
|
|
307
|
+
Reading the two runs…
|
|
308
|
+
</Text>
|
|
309
|
+
)}
|
|
310
|
+
{picked && (baseline.isError || candidate.isError) && (
|
|
311
|
+
<Alert role="alert" color="red" my="sm">
|
|
312
|
+
A run could not be read. {baseline.error?.message ?? candidate.error?.message}
|
|
313
|
+
<Group mt="xs">
|
|
314
|
+
<Button
|
|
315
|
+
type="button"
|
|
316
|
+
variant="light"
|
|
317
|
+
onClick={() => {
|
|
318
|
+
void baseline.refetch();
|
|
319
|
+
void candidate.refetch();
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
Try again
|
|
323
|
+
</Button>
|
|
324
|
+
</Group>
|
|
325
|
+
</Alert>
|
|
326
|
+
)}
|
|
327
|
+
{picked && baseline.isSuccess && baseline.data === null && <Missing side="baseline" keyed={search.base!} />}
|
|
328
|
+
{picked && candidate.isSuccess && candidate.data === null && <Missing side="candidate" keyed={search.cand!} />}
|
|
329
|
+
{picked && baseline.data && candidate.data && (
|
|
330
|
+
<Stack gap="md">
|
|
331
|
+
<SimpleGrid cols={{ base: 1, sm: 2 }}>
|
|
332
|
+
<RunCard role="Baseline" run={baseline.data.run} />
|
|
333
|
+
<RunCard role="Candidate" run={candidate.data.run} />
|
|
334
|
+
</SimpleGrid>
|
|
335
|
+
<Comparison baseline={baseline.data} candidate={candidate.data} />
|
|
336
|
+
</Stack>
|
|
337
|
+
)}
|
|
338
|
+
</Box>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
@@ -58,6 +58,8 @@ import { groupTests, tally, type TestGroupView } from './groups';
|
|
|
58
58
|
import { patched, useExplorerNavigate, useExplorerSearch } from './navigate';
|
|
59
59
|
import { Badges, EnvironmentChips, projectTitle, runName } from './parts';
|
|
60
60
|
import { ArtifactsView } from './ArtifactsView';
|
|
61
|
+
import { CompareView } from './CompareView';
|
|
62
|
+
import { HealthView } from './HealthView';
|
|
61
63
|
import { CollectionsView } from './collections';
|
|
62
64
|
import { RunsView } from './RunsView';
|
|
63
65
|
import { SearchNotes, TypedFilters } from './search-notes';
|
|
@@ -169,7 +171,7 @@ export function Explorer() {
|
|
|
169
171
|
const model = catalog.model;
|
|
170
172
|
const selected = search.test ? (model?.byKey.get(search.test) ?? null) : null;
|
|
171
173
|
const empty = model !== null && model.tests.length === 0;
|
|
172
|
-
const split = !!search.test && search.tab !== 'runs' && search.tab !== 'artifacts' && search.tab !== 'collections';
|
|
174
|
+
const split = !!search.test && search.tab !== 'runs' && search.tab !== 'artifacts' && search.tab !== 'collections' && search.tab !== 'compare' && search.tab !== 'health';
|
|
173
175
|
return (
|
|
174
176
|
<Container size="xl" py="xl">
|
|
175
177
|
<Box aria-label="Test explorer" component="article" miw={0}>
|
|
@@ -180,7 +182,7 @@ export function Explorer() {
|
|
|
180
182
|
{model && (model.tests.length > 0 || model.summary.runs > 0) && (
|
|
181
183
|
<Tabs
|
|
182
184
|
value={search.tab ?? 'tests'}
|
|
183
|
-
onChange={(value) => go({ tab: value === 'runs' || value === 'artifacts' || value === 'collections' ? value : undefined, test: undefined, openRun: undefined, layer: undefined, artifact: undefined, collection: undefined, rev: undefined })}
|
|
185
|
+
onChange={(value) => go({ tab: value === 'runs' || value === 'artifacts' || value === 'collections' || value === 'compare' || value === 'health' ? value : undefined, test: undefined, openRun: undefined, layer: undefined, artifact: undefined, collection: undefined, rev: undefined, pair: undefined })}
|
|
184
186
|
>
|
|
185
187
|
<Tabs.List aria-label="Explorer view">
|
|
186
188
|
<Tabs.Tab
|
|
@@ -204,6 +206,8 @@ export function Explorer() {
|
|
|
204
206
|
Runs
|
|
205
207
|
</Tabs.Tab>
|
|
206
208
|
<Tabs.Tab value="artifacts">Artifacts</Tabs.Tab>
|
|
209
|
+
<Tabs.Tab value="compare">Compare</Tabs.Tab>
|
|
210
|
+
<Tabs.Tab value="health">Catalog health</Tabs.Tab>
|
|
207
211
|
{!staticEvidence && <Tabs.Tab value="collections">Collections</Tabs.Tab>}
|
|
208
212
|
</Tabs.List>
|
|
209
213
|
</Tabs>
|
|
@@ -241,6 +245,9 @@ export function Explorer() {
|
|
|
241
245
|
</List.Item>
|
|
242
246
|
))}
|
|
243
247
|
</List>
|
|
248
|
+
<Anchor mt="xs" display="inline-block" size="sm" renderRoot={(props) => <Link {...props} to="/" search={patched({ tab: 'health', test: undefined })} resetScroll={false} activeOptions={{ exact: true }} />}>
|
|
249
|
+
Why do these counts differ from the Tests?
|
|
250
|
+
</Anchor>
|
|
244
251
|
</Box>
|
|
245
252
|
)}
|
|
246
253
|
{!model && !evidence.unavailable && (
|
|
@@ -269,6 +276,10 @@ export function Explorer() {
|
|
|
269
276
|
<RunsView />
|
|
270
277
|
) : search.tab === 'artifacts' ? (
|
|
271
278
|
<ArtifactsView />
|
|
279
|
+
) : search.tab === 'compare' ? (
|
|
280
|
+
<CompareView />
|
|
281
|
+
) : search.tab === 'health' ? (
|
|
282
|
+
<HealthView />
|
|
272
283
|
) : search.tab === 'collections' ? (
|
|
273
284
|
<CollectionsView />
|
|
274
285
|
) : empty ? null : (
|