@crvouga/atlas 0.10.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +7 -0
- package/dist/reports/src/catalog-health.d.ts +142 -0
- package/dist/reports/src/catalog-health.js +257 -0
- package/dist/reports/src/test-catalog.d.ts +8 -0
- package/dist/reports/src/test-catalog.js +5 -1
- package/dist/schema/src/api.d.ts +79 -0
- package/dist/schema/src/api.js +50 -0
- package/dist/server/src/app.js +9 -0
- package/dist/server/src/llms.js +1 -0
- package/dist/server/src/openapi.js +18 -2
- package/dist/server/src/settings-page.d.ts +4 -4
- package/dist/server/src/settings-page.js +17 -78
- package/docs/reference/http-api.md +34 -0
- package/docs/reference/reports.md +224 -0
- package/docs/reference/schema.md +10 -0
- package/docs/reference/server.md +4 -4
- package/docs/server.md +10 -0
- package/docs/ui.md +142 -0
- package/docs/workspaces.md +130 -0
- package/llms-full.txt +322 -0
- package/llms.txt +2 -0
- package/package.json +17 -10
- package/packages/schema/schemas/catalog-health.schema.json +400 -0
- package/packages/server/openapi.json +596 -0
- package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
- package/packages/visualizer/src/admin/main.tsx +11 -0
- package/packages/visualizer/src/app/AppShell.tsx +62 -38
- package/packages/visualizer/src/app/gazetteer.tsx +90 -83
- package/packages/visualizer/src/app/router.tsx +33 -15
- package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
- package/packages/visualizer/src/components/DataIssues.tsx +36 -54
- package/packages/visualizer/src/components/Header.tsx +197 -125
- package/packages/visualizer/src/components/Legend.tsx +13 -41
- package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
- package/packages/visualizer/src/components/Screen.tsx +80 -71
- package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
- package/packages/visualizer/src/components/Status.tsx +20 -34
- package/packages/visualizer/src/components/labels.tsx +8 -3
- package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
- package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
- package/packages/visualizer/src/components/spec-logos/README.md +23 -0
- package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
- package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
- package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
- package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
- package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
- package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
- package/packages/visualizer/src/evidence/document-page.tsx +51 -29
- package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
- package/packages/visualizer/src/evidence/marks.tsx +112 -79
- package/packages/visualizer/src/evidence/model-page.tsx +77 -46
- package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
- package/packages/visualizer/src/evidence/overview.tsx +137 -69
- package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
- package/packages/visualizer/src/evidence/recording.tsx +135 -68
- package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
- package/packages/visualizer/src/evidence/source-text.tsx +33 -0
- package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
- package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
- package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
- package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
- package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
- package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
- package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
- package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
- package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
- package/packages/visualizer/src/explorer/parts.tsx +35 -13
- package/packages/visualizer/src/find/Finder.tsx +106 -90
- package/packages/visualizer/src/find/open.ts +17 -20
- package/packages/visualizer/src/live/test-progress.tsx +60 -22
- package/packages/visualizer/src/looks/Stage.tsx +223 -110
- package/packages/visualizer/src/main.tsx +9 -11
- package/packages/visualizer/src/map/ChartMap.tsx +125 -69
- package/packages/visualizer/src/map/EventEdge.tsx +59 -37
- package/packages/visualizer/src/map/map.module.css +12 -546
- package/packages/visualizer/src/map/nodes.tsx +124 -70
- package/packages/visualizer/src/reachable.test.ts +3 -2
- package/packages/visualizer/src/spec-kind.ts +2 -2
- package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
- package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/ui/grammars/README.md +14 -0
- package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
- package/packages/visualizer/src/ui/grammars/quint.json +152 -0
- package/packages/visualizer/src/ui/highlight.ts +50 -0
- package/packages/visualizer/src/ui/theme.tsx +109 -0
- package/packages/visualizer/src/views/ChartPage.tsx +46 -26
- package/packages/visualizer/src/views/ChartTests.tsx +59 -22
- package/packages/visualizer/src/views/ProductPage.tsx +16 -15
- package/packages/visualizer/src/views/SidePanel.tsx +241 -131
- package/packages/visualizer/src/app/gazetteer.module.css +0 -107
- package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
- package/packages/visualizer/src/components/DataIssues.module.css +0 -136
- package/packages/visualizer/src/components/Header.module.css +0 -114
- package/packages/visualizer/src/components/Legend.module.css +0 -84
- package/packages/visualizer/src/components/Screen.module.css +0 -312
- package/packages/visualizer/src/components/Status.module.css +0 -116
- package/packages/visualizer/src/components/icons.tsx +0 -156
- package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
- package/packages/visualizer/src/evidence/evidence.module.css +0 -907
- package/packages/visualizer/src/evidence/recording.module.css +0 -460
- package/packages/visualizer/src/evidence/sources.module.css +0 -168
- package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
- package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
- package/packages/visualizer/src/find/Finder.module.css +0 -168
- package/packages/visualizer/src/looks/stage.module.css +0 -300
- package/packages/visualizer/src/styles/global.css +0 -105
- package/packages/visualizer/src/styles/tokens.css +0 -80
- package/packages/visualizer/src/views/views.module.css +0 -546
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IconX, IconCircleFilled } from '@tabler/icons-react';
|
|
2
|
+
import { Alert, Anchor, Box, Button, Code, Flex, SimpleGrid, Stack, Table, Text, Title, VisuallyHidden } from '@mantine/core';
|
|
1
3
|
/**
|
|
2
4
|
* @module Test detail
|
|
3
5
|
*
|
|
@@ -11,24 +13,18 @@ import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
|
11
13
|
import { formatMs, type TestResultEntry, type TestSummary } from '@crvouga/atlas/reports/test-catalog';
|
|
12
14
|
import { Link } from '@tanstack/react-router';
|
|
13
15
|
import { lazy, Suspense, useState } from 'react';
|
|
14
|
-
|
|
15
16
|
import { CheckDetail, type CheckSelection } from '../evidence/check-detail';
|
|
16
17
|
import { ago, exactTime, plural, shortSha, statusWord, useNow } from '../evidence/format';
|
|
17
18
|
import { StatusMark } from '../evidence/marks';
|
|
18
19
|
import { useTestHistory } from './catalog';
|
|
19
|
-
import styles from './explorer.module.css';
|
|
20
|
-
import { Badges } from './parts';
|
|
21
20
|
import { patched } from './navigate';
|
|
22
|
-
|
|
21
|
+
import { Badges } from './parts';
|
|
23
22
|
const RecordingPage = lazy(() => import('../evidence/recording-page').then((module) => ({ default: module.RecordingPage })));
|
|
24
|
-
|
|
25
23
|
/** Results the history table shows before "Show more". */
|
|
26
24
|
const HISTORY_ROWS = 20;
|
|
27
|
-
|
|
28
25
|
function percent(rate: number | null): string {
|
|
29
26
|
return rate === null ? '—' : `${Math.round(rate * 100)}%`;
|
|
30
27
|
}
|
|
31
|
-
|
|
32
28
|
/** The open test. `slot` is the report's check for it, when the page holds one, for its evidence. */
|
|
33
29
|
export function TestDetail({ test, slot }: { test: TestSummary; slot: CheckSlot | undefined }) {
|
|
34
30
|
const now = useNow();
|
|
@@ -40,96 +36,286 @@ export function TestDetail({ test, slot }: { test: TestSummary; slot: CheckSlot
|
|
|
40
36
|
const entries = history.data?.history ?? [];
|
|
41
37
|
const stats = test.stats;
|
|
42
38
|
const d = stats.durations;
|
|
43
|
-
|
|
44
39
|
if (recording && slot) {
|
|
45
40
|
return (
|
|
46
|
-
<Suspense
|
|
47
|
-
|
|
41
|
+
<Suspense
|
|
42
|
+
fallback={
|
|
43
|
+
<Text role="status" c="dimmed">
|
|
44
|
+
Loading the recording…
|
|
45
|
+
</Text>
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
<RecordingPage
|
|
49
|
+
slot={slot}
|
|
50
|
+
layerId={selection.layer}
|
|
51
|
+
label={test.title}
|
|
52
|
+
back="Test detail"
|
|
53
|
+
moment={moment}
|
|
54
|
+
onMoment={setMoment}
|
|
55
|
+
onClose={() => setRecording(false)}
|
|
56
|
+
/>
|
|
48
57
|
</Suspense>
|
|
49
58
|
);
|
|
50
59
|
}
|
|
51
|
-
|
|
52
60
|
return (
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
<Box aria-label={test.title} component="article">
|
|
62
|
+
<Flex gap="xs" align="center" wrap="wrap" justify="flex-end">
|
|
63
|
+
<Anchor
|
|
64
|
+
aria-label="Close the test"
|
|
65
|
+
renderRoot={(props) => (
|
|
66
|
+
<Link {...props} to="/" search={patched({ test: undefined })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
<IconX size={20} aria-hidden="true" />
|
|
70
|
+
</Anchor>
|
|
71
|
+
</Flex>
|
|
72
|
+
<Box component="header" mb="lg">
|
|
58
73
|
{test.spec && (
|
|
59
|
-
<
|
|
60
|
-
{test.spec
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
<Text component="div" c="dimmed">
|
|
75
|
+
{test.spec!.domain && (
|
|
76
|
+
<Anchor
|
|
77
|
+
renderRoot={(props) => (
|
|
78
|
+
<Link
|
|
79
|
+
{...props}
|
|
80
|
+
to="/"
|
|
81
|
+
search={patched({ domain: test.spec!.domain, spec: undefined })}
|
|
82
|
+
resetScroll={false}
|
|
83
|
+
activeOptions={{ exact: true }}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{test.spec!.domain}
|
|
88
|
+
</Anchor>
|
|
89
|
+
)}
|
|
90
|
+
<Anchor
|
|
91
|
+
renderRoot={(props) => (
|
|
92
|
+
<Link
|
|
93
|
+
{...props}
|
|
94
|
+
to="/"
|
|
95
|
+
search={patched({ spec: test.spec!.path, domain: undefined })}
|
|
96
|
+
resetScroll={false}
|
|
97
|
+
activeOptions={{ exact: true }}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
101
|
+
{test.spec!.title}
|
|
102
|
+
</Anchor>
|
|
103
|
+
</Text>
|
|
63
104
|
)}
|
|
64
|
-
<
|
|
65
|
-
|
|
105
|
+
<Title order={2} mb="sm">
|
|
106
|
+
{test.title}
|
|
107
|
+
</Title>
|
|
108
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
66
109
|
<StatusMark status={test.status} />
|
|
67
|
-
<
|
|
68
|
-
|
|
110
|
+
<Text data-status={test.status} span inherit fw={600}>
|
|
111
|
+
{statusWord(test.status)}
|
|
112
|
+
</Text>
|
|
113
|
+
{stats.flaky && (
|
|
114
|
+
<Text title={stats.flakyReason} span inherit>
|
|
115
|
+
Flaky: {stats.flakyReason}
|
|
116
|
+
</Text>
|
|
117
|
+
)}
|
|
69
118
|
<Badges test={test} />
|
|
70
|
-
</
|
|
71
|
-
{test.spec && <
|
|
72
|
-
<
|
|
73
|
-
|
|
119
|
+
</Flex>
|
|
120
|
+
{test.spec && <Code c="dimmed">{test.spec!.path}</Code>}
|
|
121
|
+
<Code title="Check id" c="dimmed">
|
|
122
|
+
{test.id}
|
|
123
|
+
</Code>
|
|
124
|
+
</Box>
|
|
74
125
|
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
126
|
+
<Box aria-label="Current results" component="section" mb="lg">
|
|
127
|
+
<Title order={3} mb="sm" c="dimmed">
|
|
128
|
+
Current result per environment
|
|
129
|
+
</Title>
|
|
130
|
+
{test.outcomes.length === 0 ? (
|
|
131
|
+
<Text component="div" c="dimmed">
|
|
132
|
+
No completed result recorded yet.
|
|
133
|
+
</Text>
|
|
134
|
+
) : (
|
|
135
|
+
<Table.ScrollContainer minWidth={600}>
|
|
136
|
+
<Table>
|
|
137
|
+
<Table.Thead>
|
|
138
|
+
<Table.Tr>
|
|
139
|
+
<Table.Th scope="col">Environment</Table.Th>
|
|
140
|
+
<Table.Th scope="col">Producer</Table.Th>
|
|
141
|
+
<Table.Th scope="col">Status</Table.Th>
|
|
142
|
+
<Table.Th scope="col">Duration</Table.Th>
|
|
143
|
+
<Table.Th scope="col">Branch</Table.Th>
|
|
144
|
+
<Table.Th scope="col">Commit</Table.Th>
|
|
145
|
+
<Table.Th scope="col">Recorded</Table.Th>
|
|
146
|
+
<Table.Th scope="col">
|
|
147
|
+
<VisuallyHidden>Run</VisuallyHidden>
|
|
148
|
+
</Table.Th>
|
|
149
|
+
</Table.Tr>
|
|
150
|
+
</Table.Thead>
|
|
151
|
+
<Table.Tbody>
|
|
82
152
|
{test.outcomes.map((outcome) => (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
153
|
+
<Table.Tr
|
|
154
|
+
key={`${outcome.layer}|${outcome.producer}|${outcome.environment}|${JSON.stringify(outcome.parameters)}`}
|
|
155
|
+
data-status={outcome.status}
|
|
156
|
+
>
|
|
157
|
+
<Table.Td>
|
|
158
|
+
{environmentLabel(outcome.environment)}
|
|
159
|
+
{Object.entries(outcome.parameters).map(([name, value]) => (
|
|
160
|
+
<Text key={name} span inherit c="dimmed">
|
|
161
|
+
{' '}
|
|
162
|
+
{name}: {value}
|
|
163
|
+
</Text>
|
|
164
|
+
))}
|
|
165
|
+
</Table.Td>
|
|
166
|
+
<Table.Td>{outcome.producer}</Table.Td>
|
|
167
|
+
<Table.Td>
|
|
168
|
+
<Flex component="span" display="inline-flex" gap="xs" align="center" wrap="wrap">
|
|
169
|
+
<StatusMark status={outcome.status} size={12} />
|
|
170
|
+
{statusWord(outcome.status)}
|
|
171
|
+
</Flex>
|
|
172
|
+
</Table.Td>
|
|
173
|
+
<Table.Td>{formatMs(outcome.durationMs)}</Table.Td>
|
|
174
|
+
<Table.Td title={outcome.branch ?? ''} c="dimmed">
|
|
175
|
+
{outcome.branch ?? '—'}
|
|
176
|
+
</Table.Td>
|
|
177
|
+
<Table.Td>
|
|
178
|
+
<Code>{shortSha(outcome.sha) ?? '—'}</Code>
|
|
179
|
+
</Table.Td>
|
|
180
|
+
<Table.Td title={exactTime(outcome.at)}>{ago(outcome.at, now)}</Table.Td>
|
|
181
|
+
<Table.Td>
|
|
182
|
+
{outcome.runUrl ? (
|
|
183
|
+
<Anchor href={outcome.runUrl} target="_blank" rel="noreferrer">
|
|
184
|
+
CI run
|
|
185
|
+
</Anchor>
|
|
186
|
+
) : null}
|
|
187
|
+
</Table.Td>
|
|
188
|
+
</Table.Tr>
|
|
93
189
|
))}
|
|
94
|
-
</
|
|
95
|
-
</
|
|
96
|
-
</
|
|
190
|
+
</Table.Tbody>
|
|
191
|
+
</Table>
|
|
192
|
+
</Table.ScrollContainer>
|
|
97
193
|
)}
|
|
98
|
-
</
|
|
194
|
+
</Box>
|
|
99
195
|
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
196
|
+
<Box aria-label="Statistics" component="section" mb="lg">
|
|
197
|
+
<Title order={3} mb="sm" c="dimmed">
|
|
198
|
+
How it has gone
|
|
199
|
+
</Title>
|
|
200
|
+
<SimpleGrid component="dl" cols={{ base: 2, sm: 3 }}>
|
|
201
|
+
<Stack gap="sm">
|
|
202
|
+
<Box component="dt">Pass rate</Box>
|
|
203
|
+
<Box component="dd" m={0}>
|
|
204
|
+
{percent(stats.passRate)}
|
|
205
|
+
</Box>
|
|
206
|
+
</Stack>
|
|
207
|
+
<Stack gap="sm">
|
|
208
|
+
<Box component="dt">Results</Box>
|
|
209
|
+
<Box component="dd" m={0}>
|
|
210
|
+
{stats.results}{' '}
|
|
211
|
+
<Text span inherit>
|
|
212
|
+
({stats.passed} passed{stats.failed ? `, ${stats.failed} failed` : ''})
|
|
213
|
+
</Text>
|
|
214
|
+
</Box>
|
|
215
|
+
</Stack>
|
|
216
|
+
<Stack gap="sm">
|
|
217
|
+
<Box component="dt">Runs</Box>
|
|
218
|
+
<Box component="dd" m={0}>
|
|
219
|
+
{stats.runs}
|
|
220
|
+
</Box>
|
|
221
|
+
</Stack>
|
|
222
|
+
<Stack gap="sm">
|
|
223
|
+
<Box component="dt">Flips</Box>
|
|
224
|
+
<Box component="dd" m={0}>
|
|
225
|
+
{stats.flips}
|
|
226
|
+
</Box>
|
|
227
|
+
</Stack>
|
|
228
|
+
<Stack gap="sm">
|
|
229
|
+
<Box component="dt">Average</Box>
|
|
230
|
+
<Box component="dd" m={0}>
|
|
231
|
+
{formatMs(d?.avgMs)}
|
|
232
|
+
</Box>
|
|
233
|
+
</Stack>
|
|
234
|
+
<Stack gap="sm">
|
|
235
|
+
<Box component="dt">p50</Box>
|
|
236
|
+
<Box component="dd" m={0}>
|
|
237
|
+
{formatMs(d?.p50Ms)}
|
|
238
|
+
</Box>
|
|
239
|
+
</Stack>
|
|
240
|
+
<Stack gap="sm">
|
|
241
|
+
<Box component="dt">p95</Box>
|
|
242
|
+
<Box component="dd" m={0}>
|
|
243
|
+
{formatMs(d?.p95Ms)}
|
|
244
|
+
</Box>
|
|
245
|
+
</Stack>
|
|
246
|
+
<Stack gap="sm">
|
|
247
|
+
<Box component="dt">Min – max</Box>
|
|
248
|
+
<Box component="dd" m={0}>
|
|
249
|
+
{d ? `${formatMs(d.minMs)} – ${formatMs(d.maxMs)}` : '—'}
|
|
250
|
+
</Box>
|
|
251
|
+
</Stack>
|
|
252
|
+
<Stack gap="sm">
|
|
253
|
+
<Box component="dt">Last</Box>
|
|
254
|
+
<Box component="dd" m={0}>
|
|
255
|
+
{formatMs(d?.lastMs)}
|
|
256
|
+
</Box>
|
|
257
|
+
</Stack>
|
|
258
|
+
<Stack gap="sm">
|
|
259
|
+
<Box component="dt">Last passed</Box>
|
|
260
|
+
<Box component="dd" m={0}>
|
|
261
|
+
{stats.lastPassedAtMs ? ago(stats.lastPassedAtMs, now) : '—'}
|
|
262
|
+
</Box>
|
|
263
|
+
</Stack>
|
|
264
|
+
<Stack gap="sm">
|
|
265
|
+
<Box component="dt">Last failed</Box>
|
|
266
|
+
<Box component="dd" m={0}>
|
|
267
|
+
{stats.lastFailedAtMs ? ago(stats.lastFailedAtMs, now) : '—'}
|
|
268
|
+
</Box>
|
|
269
|
+
</Stack>
|
|
270
|
+
<Stack gap="sm">
|
|
271
|
+
<Box component="dt">First seen</Box>
|
|
272
|
+
<Box component="dd" m={0}>
|
|
273
|
+
{stats.firstSeenMs ? ago(stats.firstSeenMs, now) : '—'}
|
|
274
|
+
</Box>
|
|
275
|
+
</Stack>
|
|
276
|
+
</SimpleGrid>
|
|
277
|
+
{stats.branches.length > 0 && (
|
|
278
|
+
<Text component="div" c="dimmed">
|
|
279
|
+
Branches: {stats.branches.join(', ')}
|
|
280
|
+
</Text>
|
|
281
|
+
)}
|
|
282
|
+
{!d && (
|
|
283
|
+
<Text component="div" c="dimmed">
|
|
284
|
+
No duration recorded. Producers can send <Code>durationMs</Code> with each check.
|
|
285
|
+
</Text>
|
|
286
|
+
)}
|
|
118
287
|
{entries.length > 1 && <Trend entries={entries} />}
|
|
119
|
-
</
|
|
288
|
+
</Box>
|
|
120
289
|
|
|
121
290
|
{(test.tags.length > 0 || Object.keys(test.labels).length > 0) && (
|
|
122
|
-
<
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
291
|
+
<Box aria-label="Tags and labels" component="section" mb="lg">
|
|
292
|
+
<Title order={3} mb="sm" c="dimmed">
|
|
293
|
+
Tags and labels
|
|
294
|
+
</Title>
|
|
295
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
296
|
+
{test.tags.map((tag) => (
|
|
297
|
+
<Anchor
|
|
298
|
+
key={tag}
|
|
299
|
+
renderRoot={(props) => (
|
|
300
|
+
<Link {...props} to="/" search={patched({ tag })} resetScroll={false} activeOptions={{ exact: true }} />
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{tag}
|
|
304
|
+
</Anchor>
|
|
305
|
+
))}
|
|
306
|
+
{Object.entries(test.labels).map(([name, value]) => (
|
|
307
|
+
<Text key={name} span inherit>
|
|
308
|
+
{name}: {value}
|
|
309
|
+
</Text>
|
|
310
|
+
))}
|
|
311
|
+
</Flex>
|
|
312
|
+
</Box>
|
|
129
313
|
)}
|
|
130
314
|
|
|
131
|
-
<
|
|
132
|
-
<
|
|
315
|
+
<Box aria-label="Evidence" component="section" mb="lg">
|
|
316
|
+
<Title order={3} mb="sm" c="dimmed">
|
|
317
|
+
What the current result carries
|
|
318
|
+
</Title>
|
|
133
319
|
{slot ? (
|
|
134
320
|
<CheckDetail
|
|
135
321
|
slot={slot}
|
|
@@ -137,78 +323,167 @@ export function TestDetail({ test, slot }: { test: TestSummary; slot: CheckSlot
|
|
|
137
323
|
reads="steps"
|
|
138
324
|
select={(patch) => {
|
|
139
325
|
setSelection({ check: patch.check ?? slot.id, layer: patch.layer ?? undefined });
|
|
140
|
-
if (patch.view === 'recording') {
|
|
326
|
+
if (patch.view === 'recording') {
|
|
327
|
+
setRecording(true);
|
|
328
|
+
setMoment(null);
|
|
329
|
+
}
|
|
141
330
|
}}
|
|
142
331
|
/>
|
|
143
|
-
) :
|
|
144
|
-
|
|
332
|
+
) : (
|
|
333
|
+
<Text component="div" c="dimmed">
|
|
334
|
+
The evidence of this result is not loaded in this page.
|
|
335
|
+
</Text>
|
|
336
|
+
)}
|
|
337
|
+
</Box>
|
|
145
338
|
|
|
146
|
-
<
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
339
|
+
<Box aria-label="History" component="section" mb="lg">
|
|
340
|
+
<Title order={3} mb="sm" c="dimmed">
|
|
341
|
+
History{entries.length ? ` · ${plural(entries.length, 'result', 'results')}` : ''}
|
|
342
|
+
</Title>
|
|
343
|
+
{history.isPending && (
|
|
344
|
+
<Text role="status" component="div" c="dimmed">
|
|
345
|
+
Reading the history…
|
|
346
|
+
</Text>
|
|
347
|
+
)}
|
|
348
|
+
{history.isError && (
|
|
349
|
+
<Alert role="alert" color={'red'} my="sm">
|
|
350
|
+
The history could not be read. {history.error.message}
|
|
351
|
+
</Alert>
|
|
352
|
+
)}
|
|
150
353
|
{entries.length > 0 && (
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
|
|
354
|
+
<Table.ScrollContainer minWidth={600}>
|
|
355
|
+
<Table>
|
|
356
|
+
<Table.Thead>
|
|
357
|
+
<Table.Tr>
|
|
358
|
+
<Table.Th scope="col">Recorded</Table.Th>
|
|
359
|
+
<Table.Th scope="col">Environment</Table.Th>
|
|
360
|
+
<Table.Th scope="col">Status</Table.Th>
|
|
361
|
+
<Table.Th scope="col">Duration</Table.Th>
|
|
362
|
+
<Table.Th scope="col">Branch</Table.Th>
|
|
363
|
+
<Table.Th scope="col">Commit</Table.Th>
|
|
364
|
+
<Table.Th scope="col">Detail</Table.Th>
|
|
365
|
+
</Table.Tr>
|
|
366
|
+
</Table.Thead>
|
|
367
|
+
<Table.Tbody>
|
|
155
368
|
{entries.slice(0, rows).map((entry) => (
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
369
|
+
<Table.Tr
|
|
370
|
+
key={`${entry.layer}|${entry.kind}|${entry.producer}|${entry.environment}|${JSON.stringify(entry.parameters)}`}
|
|
371
|
+
data-status={entry.status}
|
|
372
|
+
>
|
|
373
|
+
<Table.Td title={exactTime(entry.at)}>
|
|
374
|
+
{entry.runUrl ? (
|
|
375
|
+
<Anchor href={entry.runUrl} target="_blank" rel="noreferrer">
|
|
376
|
+
{ago(entry.at, now)}
|
|
377
|
+
</Anchor>
|
|
378
|
+
) : (
|
|
379
|
+
ago(entry.at, now)
|
|
380
|
+
)}
|
|
381
|
+
</Table.Td>
|
|
382
|
+
<Table.Td>{environmentLabel(entry.environment)}</Table.Td>
|
|
383
|
+
<Table.Td>
|
|
384
|
+
<Flex component="span" display="inline-flex" gap="xs" align="center" wrap="wrap">
|
|
385
|
+
<StatusMark status={entry.status} size={12} />
|
|
386
|
+
{statusWord(entry.status)}
|
|
387
|
+
</Flex>
|
|
388
|
+
</Table.Td>
|
|
389
|
+
<Table.Td>{formatMs(entry.durationMs)}</Table.Td>
|
|
390
|
+
<Table.Td title={entry.branch ?? ''} c="dimmed">
|
|
391
|
+
{entry.branch ?? '—'}
|
|
392
|
+
</Table.Td>
|
|
393
|
+
<Table.Td>
|
|
394
|
+
<Code>{shortSha(entry.sha) ?? '—'}</Code>
|
|
395
|
+
</Table.Td>
|
|
396
|
+
<Table.Td title={entry.detail} c="dimmed">
|
|
397
|
+
{entry.detail.split('\n')[0]}
|
|
398
|
+
</Table.Td>
|
|
399
|
+
</Table.Tr>
|
|
165
400
|
))}
|
|
166
|
-
</
|
|
167
|
-
</
|
|
168
|
-
</
|
|
401
|
+
</Table.Tbody>
|
|
402
|
+
</Table>
|
|
403
|
+
</Table.ScrollContainer>
|
|
169
404
|
)}
|
|
170
|
-
{entries.length > rows &&
|
|
171
|
-
|
|
172
|
-
|
|
405
|
+
{entries.length > rows && (
|
|
406
|
+
<Button type="button" onClick={() => setRows(rows + HISTORY_ROWS)} variant="subtle">
|
|
407
|
+
Show more ({entries.length - rows} more)
|
|
408
|
+
</Button>
|
|
409
|
+
)}
|
|
410
|
+
</Box>
|
|
411
|
+
</Box>
|
|
173
412
|
);
|
|
174
413
|
}
|
|
175
|
-
|
|
176
414
|
/** The duration of each completed result over time, and a strip of every outcome, oldest on the left. */
|
|
177
415
|
function Trend({ entries }: { entries: readonly TestResultEntry[] }) {
|
|
178
416
|
const oldest = [...entries].reverse();
|
|
179
417
|
const timed = oldest.filter((entry) => entry.durationMs !== null && (entry.status === 'passed' || entry.status === 'failed'));
|
|
180
|
-
const width = 320,
|
|
418
|
+
const width = 320,
|
|
419
|
+
height = 56,
|
|
420
|
+
pad = 4;
|
|
181
421
|
const max = Math.max(1, ...timed.map((entry) => entry.durationMs!));
|
|
182
422
|
const x = (index: number) => (timed.length === 1 ? width / 2 : pad + (index * (width - 2 * pad)) / (timed.length - 1));
|
|
183
423
|
const y = (ms: number) => height - pad - (ms / max) * (height - 2 * pad);
|
|
184
424
|
const points = timed.map((entry, index) => `${x(index).toFixed(1)},${y(entry.durationMs!).toFixed(1)}`).join(' ');
|
|
185
425
|
return (
|
|
186
|
-
<
|
|
426
|
+
<Stack gap="sm">
|
|
187
427
|
{timed.length > 1 && (
|
|
188
|
-
<figure
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
428
|
+
<Box component="figure" m={0}>
|
|
429
|
+
<Box component="figcaption">
|
|
430
|
+
Duration over the last {plural(timed.length, 'result', 'results')} · max {formatMs(max)}
|
|
431
|
+
</Box>
|
|
432
|
+
<svg
|
|
433
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
434
|
+
role="img"
|
|
435
|
+
aria-label={`Durations from ${formatMs(timed[0]!.durationMs)} to ${formatMs(timed.at(-1)!.durationMs)}`}
|
|
436
|
+
preserveAspectRatio="none"
|
|
437
|
+
>
|
|
438
|
+
<line x1={pad} x2={width - pad} y1={height - pad} y2={height - pad} stroke="var(--mantine-color-gray-3)" />
|
|
439
|
+
<polyline points={points} fill="none" stroke="var(--mantine-color-blue-7)" strokeWidth={2} />
|
|
193
440
|
{timed.map((entry, index) => (
|
|
194
|
-
<circle
|
|
441
|
+
<circle
|
|
442
|
+
key={index}
|
|
443
|
+
cx={x(index)}
|
|
444
|
+
cy={y(entry.durationMs!)}
|
|
445
|
+
r={entry.status === 'failed' ? 3 : index === timed.length - 1 ? 2.5 : 0}
|
|
446
|
+
data-status={entry.status}
|
|
447
|
+
>
|
|
195
448
|
<title>{`${exactTime(entry.at)} · ${environmentLabel(entry.environment)} · ${statusWord(entry.status)} · ${formatMs(entry.durationMs)}`}</title>
|
|
196
449
|
</circle>
|
|
197
450
|
))}
|
|
198
451
|
{/* Wide invisible targets, so a point is easy to hover. */}
|
|
199
452
|
{timed.map((entry, index) => (
|
|
200
|
-
<rect
|
|
453
|
+
<rect
|
|
454
|
+
key={`hit${index}`}
|
|
455
|
+
x={x(index) - width / timed.length / 2}
|
|
456
|
+
y={0}
|
|
457
|
+
width={width / timed.length}
|
|
458
|
+
height={height}
|
|
459
|
+
fill="transparent"
|
|
460
|
+
>
|
|
201
461
|
<title>{`${exactTime(entry.at)} · ${environmentLabel(entry.environment)} · ${statusWord(entry.status)} · ${formatMs(entry.durationMs)}`}</title>
|
|
202
462
|
</rect>
|
|
203
463
|
))}
|
|
204
464
|
</svg>
|
|
205
|
-
</
|
|
465
|
+
</Box>
|
|
206
466
|
)}
|
|
207
|
-
<
|
|
467
|
+
<Flex
|
|
468
|
+
role="img"
|
|
469
|
+
aria-label={`Outcomes, oldest first: ${oldest.map((entry) => statusWord(entry.status)).join(', ')}`}
|
|
470
|
+
gap="xs"
|
|
471
|
+
align="center"
|
|
472
|
+
wrap="wrap"
|
|
473
|
+
>
|
|
208
474
|
{oldest.map((entry, index) => (
|
|
209
|
-
<
|
|
475
|
+
<Text
|
|
476
|
+
key={index}
|
|
477
|
+
c={entry.status === 'passed' ? 'green' : entry.status === 'failed' ? 'red' : 'gray'}
|
|
478
|
+
data-status={entry.status}
|
|
479
|
+
title={`${exactTime(entry.at)} · ${environmentLabel(entry.environment)} · ${statusWord(entry.status)}${entry.durationMs !== null ? ` · ${formatMs(entry.durationMs)}` : ''}`}
|
|
480
|
+
span
|
|
481
|
+
inherit
|
|
482
|
+
>
|
|
483
|
+
<IconCircleFilled size={12} aria-hidden="true" />
|
|
484
|
+
</Text>
|
|
210
485
|
))}
|
|
211
|
-
</
|
|
212
|
-
</
|
|
486
|
+
</Flex>
|
|
487
|
+
</Stack>
|
|
213
488
|
);
|
|
214
489
|
}
|