@crvouga/atlas 0.10.2 → 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 (99) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -0
  3. package/dist/server/src/settings-page.d.ts +4 -4
  4. package/dist/server/src/settings-page.js +17 -78
  5. package/docs/reference/server.md +4 -4
  6. package/docs/ui.md +142 -0
  7. package/docs/workspaces.md +130 -0
  8. package/llms-full.txt +278 -0
  9. package/llms.txt +2 -0
  10. package/package.json +17 -10
  11. package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
  12. package/packages/visualizer/src/admin/main.tsx +11 -0
  13. package/packages/visualizer/src/app/AppShell.tsx +62 -38
  14. package/packages/visualizer/src/app/gazetteer.tsx +90 -83
  15. package/packages/visualizer/src/app/router.tsx +33 -15
  16. package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
  17. package/packages/visualizer/src/components/DataIssues.tsx +36 -54
  18. package/packages/visualizer/src/components/Header.tsx +197 -125
  19. package/packages/visualizer/src/components/Legend.tsx +13 -41
  20. package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
  21. package/packages/visualizer/src/components/Screen.tsx +80 -71
  22. package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
  23. package/packages/visualizer/src/components/Status.tsx +20 -34
  24. package/packages/visualizer/src/components/labels.tsx +8 -3
  25. package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
  26. package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
  27. package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
  28. package/packages/visualizer/src/components/spec-logos/README.md +23 -0
  29. package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
  30. package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
  31. package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
  32. package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
  33. package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
  34. package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
  35. package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
  36. package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
  37. package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
  38. package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
  39. package/packages/visualizer/src/evidence/document-page.tsx +51 -29
  40. package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
  41. package/packages/visualizer/src/evidence/marks.tsx +112 -79
  42. package/packages/visualizer/src/evidence/model-page.tsx +77 -46
  43. package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
  44. package/packages/visualizer/src/evidence/overview.tsx +137 -69
  45. package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
  46. package/packages/visualizer/src/evidence/recording.tsx +135 -68
  47. package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
  48. package/packages/visualizer/src/evidence/source-text.tsx +33 -0
  49. package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
  50. package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
  51. package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
  52. package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
  53. package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
  54. package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
  55. package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
  56. package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
  57. package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
  58. package/packages/visualizer/src/explorer/parts.tsx +35 -13
  59. package/packages/visualizer/src/find/Finder.tsx +106 -90
  60. package/packages/visualizer/src/find/open.ts +17 -20
  61. package/packages/visualizer/src/live/test-progress.tsx +60 -22
  62. package/packages/visualizer/src/looks/Stage.tsx +223 -110
  63. package/packages/visualizer/src/main.tsx +9 -11
  64. package/packages/visualizer/src/map/ChartMap.tsx +125 -69
  65. package/packages/visualizer/src/map/EventEdge.tsx +59 -37
  66. package/packages/visualizer/src/map/map.module.css +12 -546
  67. package/packages/visualizer/src/map/nodes.tsx +124 -70
  68. package/packages/visualizer/src/reachable.test.ts +3 -2
  69. package/packages/visualizer/src/spec-kind.ts +2 -2
  70. package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
  71. package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
  72. package/packages/visualizer/src/ui/grammars/README.md +14 -0
  73. package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
  74. package/packages/visualizer/src/ui/grammars/quint.json +152 -0
  75. package/packages/visualizer/src/ui/highlight.ts +50 -0
  76. package/packages/visualizer/src/ui/theme.tsx +109 -0
  77. package/packages/visualizer/src/views/ChartPage.tsx +46 -26
  78. package/packages/visualizer/src/views/ChartTests.tsx +59 -22
  79. package/packages/visualizer/src/views/ProductPage.tsx +16 -15
  80. package/packages/visualizer/src/views/SidePanel.tsx +241 -131
  81. package/packages/visualizer/src/app/gazetteer.module.css +0 -107
  82. package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
  83. package/packages/visualizer/src/components/DataIssues.module.css +0 -136
  84. package/packages/visualizer/src/components/Header.module.css +0 -114
  85. package/packages/visualizer/src/components/Legend.module.css +0 -84
  86. package/packages/visualizer/src/components/Screen.module.css +0 -312
  87. package/packages/visualizer/src/components/Status.module.css +0 -116
  88. package/packages/visualizer/src/components/icons.tsx +0 -156
  89. package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
  90. package/packages/visualizer/src/evidence/evidence.module.css +0 -907
  91. package/packages/visualizer/src/evidence/recording.module.css +0 -460
  92. package/packages/visualizer/src/evidence/sources.module.css +0 -168
  93. package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
  94. package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
  95. package/packages/visualizer/src/find/Finder.module.css +0 -168
  96. package/packages/visualizer/src/looks/stage.module.css +0 -300
  97. package/packages/visualizer/src/styles/global.css +0 -105
  98. package/packages/visualizer/src/styles/tokens.css +0 -80
  99. 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
  }