@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.
- package/CHANGELOG.md +6 -0
- package/README.md +7 -0
- package/dist/server/src/settings-page.d.ts +4 -4
- package/dist/server/src/settings-page.js +17 -78
- package/docs/reference/server.md +4 -4
- package/docs/ui.md +142 -0
- package/docs/workspaces.md +130 -0
- package/llms-full.txt +278 -0
- package/llms.txt +2 -0
- package/package.json +17 -10
- 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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Alert, Button, Text } from '@mantine/core';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { keys, specKey, useAdapter } from '../data/queries';
|
|
4
|
+
import { SourceText } from './source-text';
|
|
5
|
+
|
|
6
|
+
/** Reuse the chart source already held by the viewer's spec query, including live updates. */
|
|
7
|
+
export function ChartSource({ file }: { file: string }) {
|
|
8
|
+
const adapter = useAdapter();
|
|
9
|
+
const source = useQuery({ queryKey: keys.specFile(specKey(adapter), file), queryFn: () => adapter.specFile(file), retry: false });
|
|
10
|
+
if (source.isPending) return <Text role="status">Loading source…</Text>;
|
|
11
|
+
if (source.isError)
|
|
12
|
+
return (
|
|
13
|
+
<Alert color="red" title="Source unavailable">
|
|
14
|
+
{source.error.message} <Button onClick={() => void source.refetch()}>Try again</Button>
|
|
15
|
+
</Alert>
|
|
16
|
+
);
|
|
17
|
+
return <SourceText file={file} text={source.data} />;
|
|
18
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { recordingPaths } from '@crvouga/atlas/reports/evidence-types';
|
|
2
2
|
import { checkHistory, type CheckSlot, type HistoryEntry } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import { flattenFrames } from '@crvouga/atlas/reports/frames';
|
|
3
4
|
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
4
5
|
import type { EvidenceResult } from '@crvouga/atlas/reports/layer-merge';
|
|
5
|
-
import { flattenFrames } from '@crvouga/atlas/reports/frames';
|
|
6
6
|
import type { ReportStep, RunReport } from '@crvouga/atlas/reports/report-types';
|
|
7
|
+
import { Accordion, Alert, Anchor, Box, Button, Code, Flex, Group, List, Stack, Table, Text, Title } from '@mantine/core';
|
|
7
8
|
import { useQuery } from '@tanstack/react-query';
|
|
8
9
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
9
|
-
|
|
10
|
+
import { TestProgress, useTestProgress } from '../live/test-progress';
|
|
10
11
|
import { reportJson } from './api';
|
|
11
12
|
import { ResultEvidence } from './artifacts';
|
|
12
13
|
import { useEvidence } from './evidence-context';
|
|
13
|
-
import styles from './evidence.module.css';
|
|
14
14
|
import { ago, duration, plural, statusWord, useNow } from './format';
|
|
15
15
|
import type { FeatureExamples, FeatureStep } from './gherkin';
|
|
16
16
|
import { Bounded, Origin, StatusMark } from './marks';
|
|
17
17
|
import { ModelTrace } from './model-trace';
|
|
18
18
|
import { recordedModelStates } from './quint';
|
|
19
|
-
import { TestProgress, useTestProgress } from '../live/test-progress';
|
|
20
|
-
|
|
21
19
|
/** What the address says is open: a check, and optionally one particular layer's record of it. */
|
|
22
|
-
export type CheckSelection = {
|
|
20
|
+
export type CheckSelection = {
|
|
21
|
+
readonly check?: string;
|
|
22
|
+
readonly layer?: string;
|
|
23
|
+
};
|
|
23
24
|
export type SelectCheck = (patch: { check?: string | null; layer?: string | null; view?: 'recording' | null }) => void;
|
|
24
|
-
|
|
25
25
|
/** The recorded timeline of one result, read only when someone opens it. */
|
|
26
26
|
export function useRecordedReport(entry: HistoryEntry | null, enabled = true) {
|
|
27
27
|
const address = entry?.layer.recording ?? null;
|
|
@@ -36,10 +36,8 @@ export function useRecordedReport(entry: HistoryEntry | null, enabled = true) {
|
|
|
36
36
|
});
|
|
37
37
|
return { paths, report };
|
|
38
38
|
}
|
|
39
|
-
|
|
40
39
|
/** One check in a list: its outcome at a glance, and everything recorded for it when opened. */
|
|
41
40
|
const ENV_RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 } as const;
|
|
42
|
-
|
|
43
41
|
/** One mark per client when the same check was recorded on more than one. */
|
|
44
42
|
function environmentMarks(slot: CheckSlot) {
|
|
45
43
|
const seen = new Map<string, CheckSlot['status']>();
|
|
@@ -51,147 +49,216 @@ function environmentMarks(slot: CheckSlot) {
|
|
|
51
49
|
if (seen.size < 2) return [];
|
|
52
50
|
return [...seen].map(([id, status]) => ({ id, status }));
|
|
53
51
|
}
|
|
54
|
-
|
|
55
52
|
function outcomeName(outcome: EvidenceResult) {
|
|
56
53
|
const environment = outcome.layer.environment ? environmentLabel(outcome.layer.environment) : '';
|
|
57
|
-
const parameters = Object.entries(outcome.parameters)
|
|
54
|
+
const parameters = Object.entries(outcome.parameters)
|
|
55
|
+
.map(([name, value]) => `${name}: ${value}`)
|
|
56
|
+
.join(', ');
|
|
58
57
|
return [environment, parameters].filter(Boolean).join(' · ') || outcome.layer.producer;
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
export function ResultRow({
|
|
60
|
+
label,
|
|
61
|
+
slot,
|
|
62
|
+
open,
|
|
63
|
+
onToggle,
|
|
64
|
+
children
|
|
65
|
+
}: {
|
|
66
|
+
label: string;
|
|
67
|
+
slot: CheckSlot | null;
|
|
68
|
+
open: boolean;
|
|
69
|
+
onToggle: () => void;
|
|
70
|
+
children: ReactNode;
|
|
71
|
+
}) {
|
|
62
72
|
const now = useNow();
|
|
63
73
|
const progress = useTestProgress(slot);
|
|
64
74
|
const current = progress.tests.find(({ run, check }) => run.status === 'running' && check.status === 'running');
|
|
65
75
|
const interrupted = !current && progress.tests.find(({ run, check }) => run.status === 'interrupted' && check.status === 'not-run');
|
|
66
76
|
const newest = slot?.outcomes[0];
|
|
67
77
|
const marks = slot && !current ? environmentMarks(slot) : [];
|
|
68
|
-
const failure =
|
|
78
|
+
const failure =
|
|
79
|
+
slot?.status === 'failed' ? slot.outcomes.find((outcome) => outcome.row.status === 'failed')?.row.detail.split('\n')[0] : '';
|
|
69
80
|
return (
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
<List.Item data-result-row data-status={slot?.status ?? 'none'} data-open={open || undefined}>
|
|
82
|
+
<Accordion variant="separated" value={open ? 'result' : null} onChange={onToggle}>
|
|
83
|
+
<Accordion.Item value="result">
|
|
84
|
+
<Accordion.Control>
|
|
85
|
+
<Group gap="sm" wrap="nowrap">
|
|
86
|
+
<StatusMark status={slot?.status ?? 'none'} />
|
|
87
|
+
<Text data-result-label span inherit>
|
|
88
|
+
{label}
|
|
89
|
+
{marks.length > 0 && (
|
|
90
|
+
<Flex component="span" display="inline-flex" gap="xs" align="center" wrap="wrap">
|
|
91
|
+
{marks.map((mark) => (
|
|
92
|
+
<Text key={mark.id || 'unspecified'} data-status={mark.status} span inherit>
|
|
93
|
+
{environmentLabel(mark.id || null)}
|
|
94
|
+
</Text>
|
|
95
|
+
))}
|
|
96
|
+
</Flex>
|
|
97
|
+
)}
|
|
98
|
+
</Text>
|
|
99
|
+
{slot?.live && (
|
|
100
|
+
<Text span inherit>
|
|
101
|
+
Running now
|
|
102
|
+
</Text>
|
|
103
|
+
)}
|
|
104
|
+
{interrupted && (
|
|
105
|
+
<Text span inherit>
|
|
106
|
+
Interrupted
|
|
107
|
+
</Text>
|
|
108
|
+
)}
|
|
109
|
+
<Text span inherit c="dimmed">
|
|
110
|
+
{current
|
|
111
|
+
? duration(current.check.durationMs ?? null) || 'In progress'
|
|
112
|
+
: interrupted
|
|
113
|
+
? 'No completed result'
|
|
114
|
+
: newest
|
|
115
|
+
? [
|
|
116
|
+
newest.row.evidence?.attachments ? plural(newest.row.evidence.attachments, 'attachment', 'attachments') : '',
|
|
117
|
+
duration(newest.row.durationMs),
|
|
118
|
+
slot.live ? '' : ago(slot.latestAtMs, now)
|
|
119
|
+
]
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join(' · ')
|
|
122
|
+
: 'No result'}
|
|
123
|
+
</Text>
|
|
124
|
+
<Text aria-hidden="true" span />
|
|
125
|
+
</Group>
|
|
126
|
+
</Accordion.Control>
|
|
127
|
+
<Accordion.Panel>{open && children}</Accordion.Panel>
|
|
128
|
+
</Accordion.Item>
|
|
129
|
+
</Accordion>
|
|
130
|
+
{!open && current && (
|
|
131
|
+
<Text component="p" data-progress="true" role="status" c="dimmed">
|
|
132
|
+
{current.check.detail || current.check.steps?.at(-1)?.name || 'Test in progress…'}
|
|
133
|
+
{progress.error ? ` · ${progress.error}` : ''}
|
|
134
|
+
</Text>
|
|
135
|
+
)}
|
|
136
|
+
{!open && interrupted && (
|
|
137
|
+
<Text component="p" data-progress="true" role="status" c="dimmed">
|
|
138
|
+
{interrupted.run.reason ?? 'The test run was interrupted.'}
|
|
139
|
+
</Text>
|
|
140
|
+
)}
|
|
141
|
+
{!open && !current && failure && (
|
|
142
|
+
<Text component="p" c="red.7">
|
|
143
|
+
{failure}
|
|
144
|
+
</Text>
|
|
145
|
+
)}
|
|
146
|
+
</List.Item>
|
|
95
147
|
);
|
|
96
148
|
}
|
|
97
|
-
|
|
98
149
|
const KEYWORD = /^(Given|When|Then|And|But|\*)\s+/;
|
|
99
|
-
|
|
100
150
|
function StepLine({ title }: { title: string }) {
|
|
101
151
|
const keyword = KEYWORD.exec(title)?.[1];
|
|
102
152
|
return keyword ? (
|
|
103
|
-
<span
|
|
104
|
-
<span
|
|
105
|
-
|
|
153
|
+
<Text span inherit>
|
|
154
|
+
<Text span inherit>
|
|
155
|
+
{keyword}
|
|
156
|
+
</Text>{' '}
|
|
157
|
+
{title.slice(keyword.length + 1)}
|
|
158
|
+
</Text>
|
|
106
159
|
) : (
|
|
107
|
-
<span
|
|
160
|
+
<Text span inherit>
|
|
161
|
+
{title}
|
|
162
|
+
</Text>
|
|
108
163
|
);
|
|
109
164
|
}
|
|
110
|
-
|
|
111
165
|
/** The steps a test recorded, each with what happened to it. */
|
|
112
166
|
export function RecordedSteps({ steps, nested = false }: { steps: readonly ReportStep[]; nested?: boolean }) {
|
|
113
167
|
return (
|
|
114
|
-
<
|
|
168
|
+
<List data-nested={nested || undefined} spacing="xs" listStyleType="none" type="ordered">
|
|
115
169
|
{steps.map((step) => (
|
|
116
|
-
<
|
|
117
|
-
<
|
|
170
|
+
<List.Item key={step.index} data-status={step.status}>
|
|
171
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
118
172
|
<StatusMark status={step.status} size={14} />
|
|
119
173
|
<StepLine title={step.title} />
|
|
120
|
-
<span
|
|
121
|
-
|
|
122
|
-
|
|
174
|
+
<Text span inherit c="dimmed">
|
|
175
|
+
{duration(step.durationMs)}
|
|
176
|
+
</Text>
|
|
177
|
+
</Flex>
|
|
178
|
+
{step.error && !step.steps.some((inner) => inner.status === 'failed' && inner.error) && (
|
|
179
|
+
<Code block c="red.7">
|
|
180
|
+
{step.error}
|
|
181
|
+
</Code>
|
|
182
|
+
)}
|
|
123
183
|
{step.steps.length > 0 && (
|
|
124
|
-
<
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
184
|
+
<Accordion variant="separated" my="sm" defaultValue={step.status === 'failed' ? 'content' : null}>
|
|
185
|
+
<Accordion.Item value="content">
|
|
186
|
+
<Accordion.Control>{plural(step.steps.length, 'recorded action', 'recorded actions')}</Accordion.Control>
|
|
187
|
+
<Accordion.Panel>
|
|
188
|
+
<RecordedSteps steps={step.steps} nested />
|
|
189
|
+
</Accordion.Panel>
|
|
190
|
+
</Accordion.Item>
|
|
191
|
+
</Accordion>
|
|
128
192
|
)}
|
|
129
|
-
</
|
|
193
|
+
</List.Item>
|
|
130
194
|
))}
|
|
131
|
-
</
|
|
195
|
+
</List>
|
|
132
196
|
);
|
|
133
197
|
}
|
|
134
|
-
|
|
135
198
|
/** A scenario as written, for when nothing has been recorded against it. */
|
|
136
199
|
export function PlainSteps({ steps, examples = [] }: { steps: readonly FeatureStep[]; examples?: readonly FeatureExamples[] }) {
|
|
137
200
|
return (
|
|
138
201
|
<>
|
|
139
|
-
<
|
|
202
|
+
<List spacing="xs" listStyleType="none" type="ordered">
|
|
140
203
|
{steps.map((step, index) => (
|
|
141
|
-
<
|
|
142
|
-
<
|
|
143
|
-
<span
|
|
144
|
-
<span
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
204
|
+
<List.Item key={index}>
|
|
205
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
206
|
+
<Text span inherit>
|
|
207
|
+
<Text span inherit>
|
|
208
|
+
{step.keyword}
|
|
209
|
+
</Text>{' '}
|
|
210
|
+
{step.text}
|
|
211
|
+
</Text>
|
|
212
|
+
</Flex>
|
|
213
|
+
{step.docString !== null && <Code block>{step.docString}</Code>}
|
|
148
214
|
{step.table.length > 0 && <Cells rows={step.table} />}
|
|
149
|
-
</
|
|
215
|
+
</List.Item>
|
|
150
216
|
))}
|
|
151
|
-
</
|
|
217
|
+
</List>
|
|
152
218
|
{examples.map((example, index) => (
|
|
153
|
-
<
|
|
154
|
-
<
|
|
219
|
+
<Stack key={index} gap="sm" miw={0}>
|
|
220
|
+
<Text component="div" c="dimmed">
|
|
221
|
+
Examples{example.name ? `: ${example.name}` : ''}
|
|
222
|
+
</Text>
|
|
155
223
|
<Cells header={example.header} rows={example.rows} />
|
|
156
|
-
</
|
|
224
|
+
</Stack>
|
|
157
225
|
))}
|
|
158
226
|
</>
|
|
159
227
|
);
|
|
160
228
|
}
|
|
161
|
-
|
|
162
229
|
function Cells({ header, rows }: { header?: readonly string[]; rows: readonly (readonly string[])[] }) {
|
|
163
230
|
return (
|
|
164
|
-
<
|
|
165
|
-
<
|
|
231
|
+
<Table.ScrollContainer minWidth={600}>
|
|
232
|
+
<Table>
|
|
166
233
|
{header && (
|
|
167
|
-
<
|
|
168
|
-
<
|
|
234
|
+
<Table.Thead>
|
|
235
|
+
<Table.Tr>
|
|
169
236
|
{header.map((cell, index) => (
|
|
170
|
-
<
|
|
237
|
+
<Table.Th key={index} scope="col">
|
|
238
|
+
{cell}
|
|
239
|
+
</Table.Th>
|
|
171
240
|
))}
|
|
172
|
-
</
|
|
173
|
-
</
|
|
241
|
+
</Table.Tr>
|
|
242
|
+
</Table.Thead>
|
|
174
243
|
)}
|
|
175
|
-
<
|
|
244
|
+
<Table.Tbody>
|
|
176
245
|
{rows.map((row, index) => (
|
|
177
|
-
<
|
|
246
|
+
<Table.Tr key={index}>
|
|
178
247
|
{row.map((cell, at) => (
|
|
179
|
-
<
|
|
248
|
+
<Table.Td key={at}>{cell}</Table.Td>
|
|
180
249
|
))}
|
|
181
|
-
</
|
|
250
|
+
</Table.Tr>
|
|
182
251
|
))}
|
|
183
|
-
</
|
|
184
|
-
</
|
|
185
|
-
</
|
|
252
|
+
</Table.Tbody>
|
|
253
|
+
</Table>
|
|
254
|
+
</Table.ScrollContainer>
|
|
186
255
|
);
|
|
187
256
|
}
|
|
188
|
-
|
|
189
257
|
function qualification(warning: string | null) {
|
|
190
258
|
// The origin line already says a checkout had local changes or an unknown origin.
|
|
191
259
|
if (!warning || warning.includes('modified checkout') || warning.includes('origin was not captured')) return null;
|
|
192
260
|
return warning;
|
|
193
261
|
}
|
|
194
|
-
|
|
195
262
|
type Props = {
|
|
196
263
|
slot: CheckSlot;
|
|
197
264
|
/** The layer whose record to read. Without it, the result that won the combined report. */
|
|
@@ -202,16 +269,35 @@ type Props = {
|
|
|
202
269
|
/** The check as written, shown until a recording is read or when there is none. */
|
|
203
270
|
written?: ReactNode;
|
|
204
271
|
};
|
|
205
|
-
|
|
206
272
|
/** Everything recorded for one check: the chosen result, how it went, and every other run of it. */
|
|
207
273
|
export function CheckDetail(props: Props) {
|
|
208
274
|
const progress = useTestProgress(props.slot);
|
|
209
|
-
const inProgress = progress.tests.some(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
275
|
+
const inProgress = progress.tests.some(
|
|
276
|
+
({ run, check }) =>
|
|
277
|
+
(check.status === 'running' && run.status === 'running') || (check.status === 'not-run' && run.status === 'interrupted')
|
|
278
|
+
);
|
|
279
|
+
return (
|
|
280
|
+
<>
|
|
281
|
+
<TestProgress {...progress} />
|
|
282
|
+
{props.slot.outcomes.length > 0 ? (
|
|
283
|
+
inProgress && !props.layerId ? (
|
|
284
|
+
<Accordion variant="separated" my="sm">
|
|
285
|
+
<Accordion.Item value="content">
|
|
286
|
+
<Accordion.Control>Previous recorded result</Accordion.Control>
|
|
287
|
+
<Accordion.Panel>
|
|
288
|
+
<RecordedCheckDetail {...props} />
|
|
289
|
+
</Accordion.Panel>
|
|
290
|
+
</Accordion.Item>
|
|
291
|
+
</Accordion>
|
|
292
|
+
) : (
|
|
293
|
+
<RecordedCheckDetail {...props} />
|
|
294
|
+
)
|
|
295
|
+
) : (
|
|
296
|
+
props.written
|
|
297
|
+
)}
|
|
298
|
+
</>
|
|
299
|
+
);
|
|
213
300
|
}
|
|
214
|
-
|
|
215
301
|
function RecordedCheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
216
302
|
const { evidence, backends } = useEvidence();
|
|
217
303
|
const now = useNow();
|
|
@@ -228,79 +314,137 @@ function RecordedCheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
|
228
314
|
const states = reads === 'trace' && report.data ? recordedModelStates(report.data.steps).map((entry) => entry.state) : [];
|
|
229
315
|
const stepsShown = report.data !== undefined && report.data.steps.length > 0 && (reads === 'steps' || states.length === 0);
|
|
230
316
|
// The failed step carries its own message when the steps are on screen.
|
|
231
|
-
const failure =
|
|
232
|
-
|
|
317
|
+
const failure =
|
|
318
|
+
chosen.row.status === 'failed' && !(stepsShown && report.data?.steps.some((step) => step.error))
|
|
319
|
+
? (report.data?.meta.error ?? chosen.row.detail)
|
|
320
|
+
: '';
|
|
233
321
|
return (
|
|
234
|
-
<
|
|
322
|
+
<Stack gap="sm" miw={0}>
|
|
235
323
|
{slot.outcomes.length > 1 && !earlier && (
|
|
236
|
-
<
|
|
324
|
+
<Flex role="group" aria-label="Environments" gap="xs" align="center" wrap="wrap">
|
|
237
325
|
{slot.outcomes.map((outcome, index) => (
|
|
238
|
-
<
|
|
326
|
+
<Button key={outcome.key} type="button" aria-pressed={index === variant} onClick={() => setVariant(index)} variant="light">
|
|
239
327
|
<StatusMark status={outcome.row.status} size={12} />
|
|
240
328
|
{outcomeName(outcome)}
|
|
241
|
-
</
|
|
329
|
+
</Button>
|
|
242
330
|
))}
|
|
243
|
-
</
|
|
331
|
+
</Flex>
|
|
244
332
|
)}
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
|
|
333
|
+
<Flex gap="xs" align="center" wrap="wrap" c="dimmed">
|
|
334
|
+
<Text data-status={chosen.row.status} span inherit fw={600}>
|
|
335
|
+
{statusWord(chosen.row.status)}
|
|
336
|
+
</Text>
|
|
337
|
+
{chosen.row.durationMs !== null && (
|
|
338
|
+
<Text span inherit>
|
|
339
|
+
{duration(chosen.row.durationMs)}
|
|
340
|
+
</Text>
|
|
341
|
+
)}
|
|
248
342
|
<Origin layer={chosen.layer} backends={backends} now={now} />
|
|
249
|
-
</
|
|
250
|
-
{note &&
|
|
251
|
-
|
|
252
|
-
|
|
343
|
+
</Flex>
|
|
344
|
+
{note && (
|
|
345
|
+
<Alert color={'blue'} my="sm">
|
|
346
|
+
{note}
|
|
347
|
+
</Alert>
|
|
348
|
+
)}
|
|
349
|
+
{failure && (
|
|
350
|
+
<Code block c="red.7">
|
|
351
|
+
{failure}
|
|
352
|
+
</Code>
|
|
353
|
+
)}
|
|
354
|
+
{report.isLoading && (
|
|
355
|
+
<Text role="status" component="div" c="dimmed">
|
|
356
|
+
Loading what was recorded…
|
|
357
|
+
</Text>
|
|
358
|
+
)}
|
|
253
359
|
{report.isError && (
|
|
254
|
-
<
|
|
360
|
+
<Alert role="alert" color={'red'} my="sm">
|
|
255
361
|
The recording could not be read. {report.error.message}{' '}
|
|
256
|
-
<
|
|
257
|
-
|
|
362
|
+
<Button type="button" onClick={() => void report.refetch()} variant="subtle">
|
|
363
|
+
Try again
|
|
364
|
+
</Button>
|
|
365
|
+
</Alert>
|
|
258
366
|
)}
|
|
259
367
|
{report.data && stepsShown && <RecordedSteps steps={report.data.steps} />}
|
|
260
368
|
{states.length > 0 && <ModelTrace states={states} />}
|
|
261
369
|
{!report.data && written}
|
|
262
|
-
{!paths && !written &&
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
370
|
+
{!paths && !written && (
|
|
371
|
+
<Text component="div" c="dimmed">
|
|
372
|
+
This check has an outcome and no recorded timeline.
|
|
373
|
+
</Text>
|
|
374
|
+
)}
|
|
375
|
+
{timings.flatMap((entry) =>
|
|
376
|
+
entry.row.measurements.map((measurement, index) => {
|
|
377
|
+
const over = measurement.limit !== undefined && measurement.value > measurement.limit;
|
|
378
|
+
return (
|
|
379
|
+
<Text key={`${entry.key}:${index}`} data-over={over || undefined} component="div">
|
|
380
|
+
{measurement.name}:{' '}
|
|
381
|
+
<Text span inherit fw={600}>
|
|
382
|
+
{Number(measurement.value.toFixed(1))} {measurement.unit}
|
|
383
|
+
</Text>
|
|
384
|
+
{measurement.limit !== undefined && (
|
|
385
|
+
<Text span inherit>
|
|
386
|
+
{' '}
|
|
387
|
+
against an allowance of {measurement.limit} {measurement.unit}
|
|
388
|
+
</Text>
|
|
389
|
+
)}
|
|
390
|
+
</Text>
|
|
391
|
+
);
|
|
392
|
+
})
|
|
393
|
+
)}
|
|
272
394
|
<ResultEvidence layer={chosen.layer} row={chosen.row} />
|
|
273
395
|
{(moments > 0 || chosen.layer.runUrl) && (
|
|
274
|
-
<
|
|
396
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
275
397
|
{moments > 0 && (
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
398
|
+
<Button
|
|
399
|
+
type="button"
|
|
400
|
+
onClick={() => select({ view: 'recording', check: slot.id, layer: earlier ? chosen.layer.id : null })}
|
|
401
|
+
variant="filled"
|
|
402
|
+
>
|
|
403
|
+
Open recording{' '}
|
|
404
|
+
<Text span inherit>
|
|
405
|
+
{plural(moments, 'moment', 'moments')}
|
|
406
|
+
</Text>
|
|
407
|
+
</Button>
|
|
408
|
+
)}
|
|
409
|
+
{chosen.layer.runUrl && (
|
|
410
|
+
<Anchor href={chosen.layer.runUrl} target="_blank" rel="noreferrer">
|
|
411
|
+
Open the run
|
|
412
|
+
</Anchor>
|
|
279
413
|
)}
|
|
280
|
-
|
|
281
|
-
</p>
|
|
414
|
+
</Flex>
|
|
282
415
|
)}
|
|
283
416
|
{history.length > 1 && (
|
|
284
|
-
<
|
|
285
|
-
<
|
|
286
|
-
|
|
417
|
+
<Box aria-label="Other runs of this check" component="section" mb="lg">
|
|
418
|
+
<Title order={4} mb="sm">
|
|
419
|
+
Recorded in {plural(history.length, 'run', 'runs')}
|
|
420
|
+
</Title>
|
|
421
|
+
<List spacing="xs" listStyleType="none">
|
|
287
422
|
<Bounded items={history} size={6} more="Earlier runs">
|
|
288
423
|
{(entry) => {
|
|
289
424
|
const current = entry.layer.id === chosen.layer.id && entry.row.kind === chosen.row.kind;
|
|
290
425
|
return (
|
|
291
|
-
<
|
|
292
|
-
<
|
|
426
|
+
<List.Item key={`${entry.layer.backend}|${entry.layer.id}|${entry.row.kind}`}>
|
|
427
|
+
<Button
|
|
428
|
+
type="button"
|
|
429
|
+
aria-current={current || undefined}
|
|
430
|
+
onClick={() =>
|
|
431
|
+
select({ layer: slot.outcomes.some((outcome) => outcome.layer.id === entry.layer.id) ? null : entry.layer.id })
|
|
432
|
+
}
|
|
433
|
+
variant="light"
|
|
434
|
+
>
|
|
293
435
|
<StatusMark status={entry.row.status} size={14} />
|
|
294
|
-
<span>
|
|
436
|
+
<Text span inherit>
|
|
437
|
+
{statusWord(entry.row.status)}
|
|
438
|
+
</Text>
|
|
295
439
|
<Origin layer={entry.layer} backends={backends} now={now} />
|
|
296
|
-
</
|
|
297
|
-
</
|
|
440
|
+
</Button>
|
|
441
|
+
</List.Item>
|
|
298
442
|
);
|
|
299
443
|
}}
|
|
300
444
|
</Bounded>
|
|
301
|
-
</
|
|
302
|
-
</
|
|
445
|
+
</List>
|
|
446
|
+
</Box>
|
|
303
447
|
)}
|
|
304
|
-
</
|
|
448
|
+
</Stack>
|
|
305
449
|
);
|
|
306
450
|
}
|