@crvouga/atlas 0.10.1 → 0.11.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -0
  3. package/dist/reports/src/layer-merge.d.ts +5 -1
  4. package/dist/reports/src/layer-merge.js +17 -2
  5. package/dist/server/src/settings-page.d.ts +4 -4
  6. package/dist/server/src/settings-page.js +17 -78
  7. package/docs/agents.md +25 -0
  8. package/docs/reference/reports.md +2 -0
  9. package/docs/reference/server.md +4 -4
  10. package/docs/report-contract.md +3 -0
  11. package/docs/ui.md +142 -0
  12. package/docs/workspaces.md +130 -0
  13. package/llms-full.txt +306 -0
  14. package/llms.txt +2 -0
  15. package/package.json +17 -10
  16. package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
  17. package/packages/visualizer/src/admin/main.tsx +11 -0
  18. package/packages/visualizer/src/app/AppShell.tsx +62 -38
  19. package/packages/visualizer/src/app/gazetteer.tsx +90 -83
  20. package/packages/visualizer/src/app/router.tsx +33 -15
  21. package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
  22. package/packages/visualizer/src/components/DataIssues.tsx +36 -54
  23. package/packages/visualizer/src/components/Header.tsx +197 -125
  24. package/packages/visualizer/src/components/Legend.tsx +13 -41
  25. package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
  26. package/packages/visualizer/src/components/Screen.tsx +80 -71
  27. package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
  28. package/packages/visualizer/src/components/Status.tsx +20 -34
  29. package/packages/visualizer/src/components/labels.tsx +8 -3
  30. package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
  31. package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
  32. package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
  33. package/packages/visualizer/src/components/spec-logos/README.md +23 -0
  34. package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
  35. package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
  36. package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
  37. package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
  38. package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
  39. package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
  40. package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
  41. package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
  42. package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
  43. package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
  44. package/packages/visualizer/src/evidence/document-page.tsx +51 -29
  45. package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
  46. package/packages/visualizer/src/evidence/marks.tsx +112 -79
  47. package/packages/visualizer/src/evidence/model-page.tsx +77 -46
  48. package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
  49. package/packages/visualizer/src/evidence/overview.tsx +137 -69
  50. package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
  51. package/packages/visualizer/src/evidence/recording.tsx +135 -68
  52. package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
  53. package/packages/visualizer/src/evidence/source-text.tsx +33 -0
  54. package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
  55. package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
  56. package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
  57. package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
  58. package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
  59. package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
  60. package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
  61. package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
  62. package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
  63. package/packages/visualizer/src/explorer/parts.tsx +35 -13
  64. package/packages/visualizer/src/find/Finder.tsx +106 -90
  65. package/packages/visualizer/src/find/open.ts +17 -20
  66. package/packages/visualizer/src/live/test-progress.tsx +60 -22
  67. package/packages/visualizer/src/looks/Stage.tsx +223 -110
  68. package/packages/visualizer/src/main.tsx +9 -11
  69. package/packages/visualizer/src/map/ChartMap.tsx +125 -69
  70. package/packages/visualizer/src/map/EventEdge.tsx +59 -37
  71. package/packages/visualizer/src/map/map.module.css +12 -546
  72. package/packages/visualizer/src/map/nodes.tsx +124 -70
  73. package/packages/visualizer/src/reachable.test.ts +3 -2
  74. package/packages/visualizer/src/spec-kind.ts +2 -2
  75. package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
  76. package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
  77. package/packages/visualizer/src/ui/grammars/README.md +14 -0
  78. package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
  79. package/packages/visualizer/src/ui/grammars/quint.json +152 -0
  80. package/packages/visualizer/src/ui/highlight.ts +50 -0
  81. package/packages/visualizer/src/ui/theme.tsx +109 -0
  82. package/packages/visualizer/src/views/ChartPage.tsx +46 -26
  83. package/packages/visualizer/src/views/ChartTests.tsx +59 -22
  84. package/packages/visualizer/src/views/ProductPage.tsx +16 -15
  85. package/packages/visualizer/src/views/SidePanel.tsx +241 -131
  86. package/packages/visualizer/src/app/gazetteer.module.css +0 -107
  87. package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
  88. package/packages/visualizer/src/components/DataIssues.module.css +0 -136
  89. package/packages/visualizer/src/components/Header.module.css +0 -114
  90. package/packages/visualizer/src/components/Legend.module.css +0 -84
  91. package/packages/visualizer/src/components/Screen.module.css +0 -312
  92. package/packages/visualizer/src/components/Status.module.css +0 -116
  93. package/packages/visualizer/src/components/icons.tsx +0 -156
  94. package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
  95. package/packages/visualizer/src/evidence/evidence.module.css +0 -907
  96. package/packages/visualizer/src/evidence/recording.module.css +0 -460
  97. package/packages/visualizer/src/evidence/sources.module.css +0 -168
  98. package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
  99. package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
  100. package/packages/visualizer/src/find/Finder.module.css +0 -168
  101. package/packages/visualizer/src/looks/stage.module.css +0 -300
  102. package/packages/visualizer/src/styles/global.css +0 -105
  103. package/packages/visualizer/src/styles/tokens.css +0 -80
  104. package/packages/visualizer/src/views/views.module.css +0 -546
@@ -1,3 +1,5 @@
1
+ import { IconArrowLeft } from '@tabler/icons-react';
2
+ import { Alert, Anchor, Box, Button, Code, Flex, Group, List, Stack, Table, Text, TextInput, Title, VisuallyHidden } from '@mantine/core';
1
3
  /**
2
4
  * @module Runs view
3
5
  *
@@ -9,73 +11,142 @@ import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
9
11
  import { formatMs, type RunResult, type RunSummary } from '@crvouga/atlas/reports/test-catalog';
10
12
  import { Link } from '@tanstack/react-router';
11
13
  import { useMemo, useState } from 'react';
12
-
13
14
  import { ago, exactTime, plural, shortSha, statusWord, useNow } from '../evidence/format';
14
15
  import { Meter, StatusMark } from '../evidence/marks';
15
16
  import { useRunDetail, useRunList } from './catalog';
16
- import styles from './explorer.module.css';
17
17
  import { patched, useExplorerSearch } from './navigate';
18
18
  import { runName } from './parts';
19
-
20
19
  const RUN_ROWS = 50;
21
20
  const RESULT_ROWS = 200;
22
-
23
21
  /** The Runs tab: the run list, or the run opened in it. */
24
22
  export function RunsView() {
25
23
  const search = useExplorerSearch();
26
24
  return search.openRun ? <RunDetailView runKey={search.openRun} /> : <RunTable />;
27
25
  }
28
-
29
26
  function Counts({ run }: { run: RunSummary }) {
30
27
  return (
31
- <span className={styles.runCounts}>
32
- {run.counts.failed > 0 && <strong>{run.counts.failed} failed</strong>}
33
- <span>{run.counts.passed} passed</span>
34
- {run.counts['not-run'] > 0 && <span>{run.counts['not-run']} not run</span>}
35
- {run.counts.running > 0 && <span data-status="running">{run.counts.running} running</span>}
36
- </span>
28
+ <Text span inherit>
29
+ {run.counts.failed > 0 && (
30
+ <Text span inherit fw={600}>
31
+ {run.counts.failed} failed
32
+ </Text>
33
+ )}
34
+ <Text span inherit>
35
+ {run.counts.passed} passed
36
+ </Text>
37
+ {run.counts['not-run'] > 0 && (
38
+ <Text span inherit>
39
+ {run.counts['not-run']} not run
40
+ </Text>
41
+ )}
42
+ {run.counts.running > 0 && (
43
+ <Text data-status="running" span inherit>
44
+ {run.counts.running} running
45
+ </Text>
46
+ )}
47
+ </Text>
37
48
  );
38
49
  }
39
-
40
50
  function RunTable() {
41
51
  const runs = useRunList();
42
52
  const now = useNow();
43
53
  const [shown, setShown] = useState(RUN_ROWS);
44
- if (runs.isPending) return <p className={styles.quiet} role="status">Reading the runs…</p>;
45
- if (runs.isError) return <p className={styles.notice} role="alert">The runs could not be read. {runs.error.message}</p>;
54
+ if (runs.isPending)
55
+ return (
56
+ <Text role="status" component="div" c="dimmed">
57
+ Reading the runs…
58
+ </Text>
59
+ );
60
+ if (runs.isError)
61
+ return (
62
+ <Alert role="alert" color={'red'} my="sm">
63
+ The runs could not be read. {runs.error.message}
64
+ </Alert>
65
+ );
46
66
  const list = runs.data.runs;
47
- if (!list.length) return <p className={styles.quiet}>No runs recorded yet.</p>;
67
+ if (!list.length)
68
+ return (
69
+ <Text component="div" c="dimmed">
70
+ No runs recorded yet.
71
+ </Text>
72
+ );
48
73
  return (
49
- <section className={styles.runs} aria-label="Runs">
50
- <p className={styles.showing}>{runs.data.total > list.length ? `The newest ${list.length} of ${plural(runs.data.total, 'run', 'runs')}` : plural(list.length, 'run', 'runs')}</p>
51
- <div className={styles.tableScroll}>
52
- <table className={styles.table} data-runs>
53
- <thead>
54
- <tr><th scope="col">Ended</th><th scope="col">Branch</th><th scope="col">Commit</th><th scope="col">Environments</th><th scope="col">Tests</th><th scope="col">Outcomes</th><th scope="col">Span</th><th scope="col"><span className="sr-only">CI run</span></th></tr>
55
- </thead>
56
- <tbody>
74
+ <Box aria-label="Runs" component="section" mb="lg">
75
+ <Flex gap="xs" align="center" wrap="wrap">
76
+ {runs.data.total > list.length
77
+ ? `The newest ${list.length} of ${plural(runs.data.total, 'run', 'runs')}`
78
+ : plural(list.length, 'run', 'runs')}
79
+ </Flex>
80
+ <Table.ScrollContainer minWidth={600}>
81
+ <Table data-runs>
82
+ <Table.Thead>
83
+ <Table.Tr>
84
+ <Table.Th scope="col">Ended</Table.Th>
85
+ <Table.Th scope="col">Branch</Table.Th>
86
+ <Table.Th scope="col">Commit</Table.Th>
87
+ <Table.Th scope="col">Environments</Table.Th>
88
+ <Table.Th scope="col">Tests</Table.Th>
89
+ <Table.Th scope="col">Outcomes</Table.Th>
90
+ <Table.Th scope="col">Span</Table.Th>
91
+ <Table.Th scope="col">
92
+ <VisuallyHidden>CI run</VisuallyHidden>
93
+ </Table.Th>
94
+ </Table.Tr>
95
+ </Table.Thead>
96
+ <Table.Tbody>
57
97
  {list.slice(0, shown).map((run) => (
58
- <tr key={run.key} data-status={run.counts.failed ? 'failed' : 'passed'}>
59
- <td title={exactTime(run.endedAtMs)}><Link to="/" search={patched({ openRun: run.key })} resetScroll={false} activeOptions={{ exact: true }} aria-label={`Open run ${runName(run.key)}`}>{ago(run.endedAtMs, now)}</Link></td>
60
- <td className={styles.ellipsis} title={run.branch ?? ''}>{run.branch ?? '—'}</td>
61
- <td><code>{shortSha(run.sha) ?? '—'}</code></td>
62
- <td>{run.environments.map((env) => environmentLabel(env)).join(', ') || '—'}</td>
63
- <td className={styles.num}>{run.tests}</td>
64
- <td><span className={styles.runMeter}><Meter counts={run.counts} /></span><Counts run={run} /></td>
65
- <td className={styles.num}>{formatMs(run.spanMs)}</td>
66
- <td>{run.runUrl ? <a href={run.runUrl} target="_blank" rel="noreferrer">CI run</a> : null}</td>
67
- </tr>
98
+ <Table.Tr key={run.key} data-status={run.counts.failed ? 'failed' : 'passed'}>
99
+ <Table.Td title={exactTime(run.endedAtMs)}>
100
+ <Anchor
101
+ aria-label={`Open run ${runName(run.key)}`}
102
+ renderRoot={(props) => (
103
+ <Link {...props} to="/" search={patched({ openRun: run.key })} resetScroll={false} activeOptions={{ exact: true }} />
104
+ )}
105
+ >
106
+ {ago(run.endedAtMs, now)}
107
+ </Anchor>
108
+ </Table.Td>
109
+ <Table.Td title={run.branch ?? ''} c="dimmed">
110
+ {run.branch ?? '—'}
111
+ </Table.Td>
112
+ <Table.Td>
113
+ <Code>{shortSha(run.sha) ?? '—'}</Code>
114
+ </Table.Td>
115
+ <Table.Td>{run.environments.map((env) => environmentLabel(env)).join(', ') || '—'}</Table.Td>
116
+ <Table.Td>{run.tests}</Table.Td>
117
+ <Table.Td>
118
+ <Text span inherit>
119
+ <Meter counts={run.counts} />
120
+ </Text>
121
+ <Counts run={run} />
122
+ </Table.Td>
123
+ <Table.Td>{formatMs(run.spanMs)}</Table.Td>
124
+ <Table.Td>
125
+ {run.runUrl ? (
126
+ <Anchor href={run.runUrl} target="_blank" rel="noreferrer">
127
+ CI run
128
+ </Anchor>
129
+ ) : null}
130
+ </Table.Td>
131
+ </Table.Tr>
68
132
  ))}
69
- </tbody>
70
- </table>
71
- </div>
72
- {list.length > shown && <button type="button" className={styles.link} onClick={() => setShown(shown + RUN_ROWS)}>Show more ({list.length - shown} more)</button>}
73
- </section>
133
+ </Table.Tbody>
134
+ </Table>
135
+ </Table.ScrollContainer>
136
+ {list.length > shown && (
137
+ <Button type="button" onClick={() => setShown(shown + RUN_ROWS)} variant="subtle">
138
+ Show more ({list.length - shown} more)
139
+ </Button>
140
+ )}
141
+ </Box>
74
142
  );
75
143
  }
76
-
77
- const RESULT_FILTERS = [{ id: '', label: 'All' }, { id: 'failed', label: 'Failed' }, { id: 'passed', label: 'Passed' }, { id: 'not-run', label: 'Not run' }] as const;
78
-
144
+ const RESULT_FILTERS = [
145
+ { id: '', label: 'All' },
146
+ { id: 'failed', label: 'Failed' },
147
+ { id: 'passed', label: 'Passed' },
148
+ { id: 'not-run', label: 'Not run' }
149
+ ] as const;
79
150
  function RunDetailView({ runKey }: { runKey: string }) {
80
151
  const detail = useRunDetail(runKey);
81
152
  const now = useNow();
@@ -85,7 +156,13 @@ function RunDetailView({ runKey }: { runKey: string }) {
85
156
  const results = detail.data?.results ?? [];
86
157
  const filtered = useMemo(() => {
87
158
  const terms = words.toLowerCase().split(/\s+/).filter(Boolean);
88
- return results.filter((result) => (!status || result.status === status) && terms.every((term) => `${result.title}\n${result.specPath ?? ''}\n${result.environment ?? ''}\n${result.producer}`.toLowerCase().includes(term)));
159
+ return results.filter(
160
+ (result) =>
161
+ (!status || result.status === status) &&
162
+ terms.every((term) =>
163
+ `${result.title}\n${result.specPath ?? ''}\n${result.environment ?? ''}\n${result.producer}`.toLowerCase().includes(term)
164
+ )
165
+ );
89
166
  }, [results, status, words]);
90
167
  const bySpec = useMemo(() => {
91
168
  const groups = new Map<string, RunResult[]>();
@@ -97,68 +174,204 @@ function RunDetailView({ runKey }: { runKey: string }) {
97
174
  }
98
175
  return [...groups];
99
176
  }, [filtered, shown]);
100
- const back = <Link to="/" search={patched({ openRun: undefined })} resetScroll={false} activeOptions={{ exact: true }} className={styles.back}><span aria-hidden="true">←</span> All runs</Link>;
101
- if (detail.isPending) return <>{back}<p className={styles.quiet} role="status">Reading the run…</p></>;
102
- if (detail.isError) return <>{back}<p className={styles.notice} role="alert">The run could not be read. {detail.error.message}</p></>;
103
- if (!detail.data) return <>{back}<p className={styles.notice}>That run is not in the report any more.</p></>;
177
+ const back = (
178
+ <Anchor
179
+ renderRoot={(props) => (
180
+ <Link {...props} to="/" search={patched({ openRun: undefined })} resetScroll={false} activeOptions={{ exact: true }} />
181
+ )}
182
+ >
183
+ <Text aria-hidden="true" span inherit>
184
+ <IconArrowLeft size={16} aria-hidden="true" />
185
+ </Text>{' '}
186
+ All runs
187
+ </Anchor>
188
+ );
189
+ if (detail.isPending)
190
+ return (
191
+ <>
192
+ {back}
193
+ <Text role="status" component="div" c="dimmed">
194
+ Reading the run…
195
+ </Text>
196
+ </>
197
+ );
198
+ if (detail.isError)
199
+ return (
200
+ <>
201
+ {back}
202
+ <Alert role="alert" color={'red'} my="sm">
203
+ The run could not be read. {detail.error.message}
204
+ </Alert>
205
+ </>
206
+ );
207
+ if (!detail.data)
208
+ return (
209
+ <>
210
+ {back}
211
+ <Alert color={'blue'} my="sm">
212
+ That run is not in the report any more.
213
+ </Alert>
214
+ </>
215
+ );
104
216
  const { run } = detail.data;
105
217
  return (
106
- <section className={styles.runs} aria-label={`Run ${runName(run.key)}`}>
218
+ <Box aria-label={`Run ${runName(run.key)}`} component="section" mb="lg">
107
219
  {back}
108
- <h2 className={styles.runTitle}>Run {runName(run.key)}</h2>
109
- <p className={styles.runMeta}>
110
- {run.branch && <span>{run.branch}</span>}
111
- {run.sha && <code>{shortSha(run.sha)}</code>}
112
- <span title={exactTime(run.endedAtMs)}>ended {ago(run.endedAtMs, now)}</span>
113
- <span>took {formatMs(run.spanMs)}</span>
114
- {run.environments.length > 0 && <span>{run.environments.map((env) => environmentLabel(env)).join(', ')}</span>}
115
- <span>{run.producers.join(', ')}</span>
116
- {run.totalDurationMs !== null && <span>{formatMs(run.totalDurationMs)} of test time</span>}
117
- </p>
118
- <div className={styles.runSummary}>
119
- <span className={styles.runMeter}><Meter counts={run.counts} /></span>
120
- <span>{plural(run.tests, 'test', 'tests')}</span>
220
+ <Title order={2} mb="sm">
221
+ Run {runName(run.key)}
222
+ </Title>
223
+ <Text component="div">
224
+ {run.branch && (
225
+ <Text span inherit>
226
+ {run.branch}
227
+ </Text>
228
+ )}
229
+ {run.sha && <Code>{shortSha(run.sha)}</Code>}
230
+ <Text title={exactTime(run.endedAtMs)} span inherit>
231
+ ended {ago(run.endedAtMs, now)}
232
+ </Text>
233
+ <Text span inherit>
234
+ took {formatMs(run.spanMs)}
235
+ </Text>
236
+ {run.environments.length > 0 && (
237
+ <Text span inherit>
238
+ {run.environments.map((env) => environmentLabel(env)).join(', ')}
239
+ </Text>
240
+ )}
241
+ <Text span inherit>
242
+ {run.producers.join(', ')}
243
+ </Text>
244
+ {run.totalDurationMs !== null && (
245
+ <Text span inherit>
246
+ {formatMs(run.totalDurationMs)} of test time
247
+ </Text>
248
+ )}
249
+ </Text>
250
+ <Stack gap="sm">
251
+ <Text span inherit>
252
+ <Meter counts={run.counts} />
253
+ </Text>
254
+ <Text span inherit>
255
+ {plural(run.tests, 'test', 'tests')}
256
+ </Text>
121
257
  <Counts run={run} />
122
- </div>
123
- <p className={styles.actionsLine}>
124
- <Link className={styles.primary} to="/" search={patched({ tab: undefined, openRun: undefined, inRun: run.key, test: undefined })} resetScroll={false} activeOptions={{ exact: true }}>Show these tests in the explorer</Link>
125
- {run.runUrl && <a href={run.runUrl} target="_blank" rel="noreferrer">Open the CI run</a>}
126
- </p>
127
- <div className={styles.searchRow}>
128
- <input type="search" className={styles.searchBox} aria-label="Search this run" placeholder="Search this run…" value={words} onChange={(event) => setWords(event.target.value)} />
129
- </div>
130
- <div className={styles.segments} role="group" aria-label="Result status">
258
+ </Stack>
259
+ <Flex gap="xs" align="center" wrap="wrap">
260
+ <Anchor
261
+ renderRoot={(props) => (
262
+ <Link
263
+ {...props}
264
+ to="/"
265
+ search={patched({ tab: undefined, openRun: undefined, inRun: run.key, test: undefined })}
266
+ resetScroll={false}
267
+ activeOptions={{ exact: true }}
268
+ />
269
+ )}
270
+ >
271
+ Show these tests in the explorer
272
+ </Anchor>
273
+ {run.runUrl && (
274
+ <Anchor href={run.runUrl} target="_blank" rel="noreferrer">
275
+ Open the CI run
276
+ </Anchor>
277
+ )}
278
+ </Flex>
279
+ <Flex gap="xs" align="center" wrap="wrap">
280
+ <TextInput
281
+ type="search"
282
+ aria-label="Search this run"
283
+ placeholder="Search this run…"
284
+ value={words}
285
+ onChange={(event) => setWords(event.target.value)}
286
+ />
287
+ </Flex>
288
+ <Flex role="group" aria-label="Result status" gap="xs" align="center" wrap="wrap">
131
289
  {RESULT_FILTERS.map((filter) => (
132
- <button key={filter.id || 'all'} type="button" aria-pressed={status === filter.id} data-status={filter.id || undefined} onClick={() => setStatus(filter.id)}>
133
- {filter.label} <span>{filter.id ? results.filter((result) => result.status === filter.id).length : results.length}</span>
134
- </button>
290
+ <Button
291
+ key={filter.id || 'all'}
292
+ type="button"
293
+ aria-pressed={status === filter.id}
294
+ data-status={filter.id || undefined}
295
+ onClick={() => setStatus(filter.id)}
296
+ variant="light"
297
+ >
298
+ {filter.label}{' '}
299
+ <Text span inherit>
300
+ {filter.id ? results.filter((result) => result.status === filter.id).length : results.length}
301
+ </Text>
302
+ </Button>
135
303
  ))}
136
- </div>
137
- {!filtered.length && <p className={styles.quiet}>No result matches.</p>}
304
+ </Flex>
305
+ {!filtered.length && (
306
+ <Text component="div" c="dimmed">
307
+ No result matches.
308
+ </Text>
309
+ )}
138
310
  {bySpec.map(([spec, list]) => (
139
- <section key={spec} className={styles.group} data-open aria-label={spec || 'No spec'}>
140
- <div className={styles.groupHead} data-static>
141
- <span className={styles.groupName}><code>{spec || 'No spec'}</code></span>
142
- <span className={styles.groupTally}>{plural(list.length, 'result', 'results')}</span>
143
- </div>
144
- <ul className={styles.rows}>
311
+ <Box key={spec} data-open aria-label={spec || 'No spec'} component="section" mb="lg">
312
+ <Button data-static variant="default" fullWidth h="auto" py="sm" justify="flex-start">
313
+ <Flex component="span" direction="column" gap="xs" miw={0}>
314
+ <Code>{spec || 'No spec'}</Code>
315
+ </Flex>
316
+ <Flex component="span" display="inline-flex" gap="xs" align="center" wrap="wrap">
317
+ {plural(list.length, 'result', 'results')}
318
+ </Flex>
319
+ </Button>
320
+ <List spacing="xs" listStyleType="none">
145
321
  {list.map((result) => (
146
- <li key={`${result.key}|${result.environment}|${result.producer}|${result.layer}`} className={styles.row} data-status={result.status}>
147
- <Link to="/" search={patched({ tab: undefined, openRun: undefined, test: result.key })} resetScroll={false} activeOptions={{ exact: true }} className={styles.rowLink}>
322
+ <List.Item key={`${result.key}|${result.environment}|${result.producer}|${result.layer}`} data-status={result.status}>
323
+ <Anchor
324
+ display="flex"
325
+ p="sm"
326
+ underline="never"
327
+ renderRoot={(props) => (
328
+ <Link
329
+ {...props}
330
+ to="/"
331
+ search={patched({ tab: undefined, openRun: undefined, test: result.key })}
332
+ resetScroll={false}
333
+ activeOptions={{ exact: true }}
334
+ />
335
+ )}
336
+ >
148
337
  <StatusMark status={result.status} size={14} />
149
- <span className={styles.rowMain}>
150
- <span className={styles.rowTitle}>{result.title}</span>
151
- <span className={styles.rowMeta}><span>{environmentLabel(result.environment)}</span><span>{result.producer}</span><span>{statusWord(result.status)}</span></span>
152
- {result.status === 'failed' && result.detail && <span className={styles.rowError}>{result.detail.split('\n')[0]}</span>}
153
- </span>
154
- <span className={styles.columns} data-one><span>{formatMs(result.durationMs)}</span></span>
155
- </Link>
156
- </li>
338
+ <Flex component="span" direction="column" gap="xs" miw={0}>
339
+ <Flex component="span" direction="column" gap="xs" miw={0}>
340
+ {result.title}
341
+ </Flex>
342
+ <Flex component="span" display="inline-flex" gap="xs" align="center" wrap="wrap" c="dimmed">
343
+ <Text span inherit>
344
+ {environmentLabel(result.environment)}
345
+ </Text>
346
+ <Text span inherit>
347
+ {result.producer}
348
+ </Text>
349
+ <Text span inherit>
350
+ {statusWord(result.status)}
351
+ </Text>
352
+ </Flex>
353
+ {result.status === 'failed' && result.detail && (
354
+ <Text span inherit c="red.7" size="xs" lineClamp={2}>
355
+ {result.detail.split('\n')[0]}
356
+ </Text>
357
+ )}
358
+ </Flex>
359
+ <Group data-one gap="md" ml="auto" visibleFrom="sm">
360
+ <Text span inherit>
361
+ {formatMs(result.durationMs)}
362
+ </Text>
363
+ </Group>
364
+ </Anchor>
365
+ </List.Item>
157
366
  ))}
158
- </ul>
159
- </section>
367
+ </List>
368
+ </Box>
160
369
  ))}
161
- {filtered.length > shown && <button type="button" className={styles.link} onClick={() => setShown(shown + RESULT_ROWS)}>Show more ({filtered.length - shown} more)</button>}
162
- </section>
370
+ {filtered.length > shown && (
371
+ <Button type="button" onClick={() => setShown(shown + RESULT_ROWS)} variant="subtle">
372
+ Show more ({filtered.length - shown} more)
373
+ </Button>
374
+ )}
375
+ </Box>
163
376
  );
164
377
  }