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