@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
|
@@ -1,17 +1,16 @@
|
|
|
1
|
+
import { IconArrowLeft } from '@tabler/icons-react';
|
|
1
2
|
import { checkHistory, type CheckSlot, type HistoryEntry } from '@crvouga/atlas/reports/evidence-view';
|
|
2
3
|
import type { RunReport } from '@crvouga/atlas/reports/report-types';
|
|
4
|
+
import { Alert, Box, Button, Container, Flex, Text, Title } from '@mantine/core';
|
|
3
5
|
import { useQuery } from '@tanstack/react-query';
|
|
4
6
|
import { useMemo } from 'react';
|
|
5
|
-
|
|
7
|
+
import { TestProgress, useTestProgress } from '../live/test-progress';
|
|
6
8
|
import { reportText } from './api';
|
|
7
9
|
import { useRecordedReport } from './check-detail';
|
|
8
10
|
import { useEvidence } from './evidence-context';
|
|
9
|
-
import styles from './evidence.module.css';
|
|
10
11
|
import { duration, statusWord, useNow } from './format';
|
|
11
12
|
import { Origin, StatusMark } from './marks';
|
|
12
13
|
import { Timeline } from './recording';
|
|
13
|
-
import { TestProgress, useTestProgress } from '../live/test-progress';
|
|
14
|
-
|
|
15
14
|
type Props = {
|
|
16
15
|
/** The check whose recording is open, once the combined report holds it. */
|
|
17
16
|
slot: CheckSlot | undefined;
|
|
@@ -22,7 +21,6 @@ type Props = {
|
|
|
22
21
|
onMoment: (moment: number, replace: boolean) => void;
|
|
23
22
|
onClose: () => void;
|
|
24
23
|
};
|
|
25
|
-
|
|
26
24
|
/** HTML snapshots are the only pictures that paint with the run's stylesheet. */
|
|
27
25
|
function needsStylesheet(report: RunReport | undefined): boolean {
|
|
28
26
|
if (!report) return false;
|
|
@@ -37,7 +35,6 @@ function needsStylesheet(report: RunReport | undefined): boolean {
|
|
|
37
35
|
};
|
|
38
36
|
return visit(report.steps) || (report.artifacts ?? []).some((artifact) => artifact.media === 'html');
|
|
39
37
|
}
|
|
40
|
-
|
|
41
38
|
/** One recorded test, opened in place of its spec. */
|
|
42
39
|
export function RecordingPage({ slot, layerId, label, back, moment, onMoment, onClose }: Props) {
|
|
43
40
|
const progress = useTestProgress(slot ?? null);
|
|
@@ -45,45 +42,87 @@ export function RecordingPage({ slot, layerId, label, back, moment, onMoment, on
|
|
|
45
42
|
const now = useNow();
|
|
46
43
|
const history = useMemo(() => (slot ? checkHistory(evidence, slot.specPath, slot.id) : []), [evidence, slot]);
|
|
47
44
|
const winner = slot?.outcomes[0];
|
|
48
|
-
const chosen: HistoryEntry | null =
|
|
45
|
+
const chosen: HistoryEntry | null =
|
|
46
|
+
(layerId ? history.find((entry) => entry.layer.id === layerId) : undefined) ??
|
|
47
|
+
(winner ? { layer: winner.layer, row: winner.row } : null);
|
|
49
48
|
const { paths, report } = useRecordedReport(chosen);
|
|
50
49
|
const wantsCss = needsStylesheet(report.data);
|
|
51
|
-
const css = useQuery({
|
|
50
|
+
const css = useQuery({
|
|
51
|
+
queryKey: ['evidence-css', paths?.css ?? ''],
|
|
52
|
+
queryFn: ({ signal }) => reportText(paths!.css, signal),
|
|
53
|
+
enabled: paths !== null && wantsCss,
|
|
54
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
55
|
+
retry: false
|
|
56
|
+
});
|
|
52
57
|
const waiting = !chosen && !unavailable && (!ready || pending > 0);
|
|
53
58
|
return (
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
<Container data-wide="true" aria-label="Recorded test" component="article" size="xl" w="100%" py="xl">
|
|
60
|
+
<Button type="button" onClick={onClose} variant="subtle">
|
|
61
|
+
<Text aria-hidden="true" span inherit>
|
|
62
|
+
<IconArrowLeft size={16} aria-hidden="true" />
|
|
63
|
+
</Text>{' '}
|
|
64
|
+
{back}
|
|
65
|
+
</Button>
|
|
66
|
+
<Box component="header" mb="lg">
|
|
67
|
+
<Title order={1} mb="sm">
|
|
68
|
+
{label}
|
|
69
|
+
</Title>
|
|
60
70
|
{chosen && (
|
|
61
|
-
<
|
|
71
|
+
<Flex gap="xs" align="center" wrap="wrap" c="dimmed">
|
|
62
72
|
<StatusMark status={chosen.row.status} />
|
|
63
|
-
<
|
|
64
|
-
|
|
73
|
+
<Text data-status={chosen.row.status} span inherit fw={600}>
|
|
74
|
+
{statusWord(chosen.row.status)}
|
|
75
|
+
</Text>
|
|
76
|
+
{chosen.row.durationMs !== null && (
|
|
77
|
+
<Text span inherit>
|
|
78
|
+
{duration(chosen.row.durationMs)}
|
|
79
|
+
</Text>
|
|
80
|
+
)}
|
|
65
81
|
<Origin layer={chosen.layer} backends={backends} now={now} />
|
|
66
|
-
</
|
|
82
|
+
</Flex>
|
|
67
83
|
)}
|
|
68
|
-
</
|
|
84
|
+
</Box>
|
|
69
85
|
<TestProgress {...progress} />
|
|
70
|
-
{waiting &&
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
{waiting && (
|
|
87
|
+
<Text role="status" component="div" c="dimmed">
|
|
88
|
+
Loading the recording…
|
|
89
|
+
</Text>
|
|
90
|
+
)}
|
|
91
|
+
{!chosen && !waiting && !progress.tests.length && (
|
|
92
|
+
<Alert color={'blue'} my="sm">
|
|
93
|
+
{unavailable ??
|
|
94
|
+
'This result is not in the combined report any more. A newer run may have replaced it, or its source is switched off.'}
|
|
95
|
+
</Alert>
|
|
96
|
+
)}
|
|
97
|
+
{chosen && !paths && (
|
|
98
|
+
<Alert color={'blue'} my="sm">
|
|
99
|
+
This result has an outcome and no recorded timeline.
|
|
100
|
+
</Alert>
|
|
101
|
+
)}
|
|
102
|
+
{report.isLoading && (
|
|
103
|
+
<Text role="status" component="div" c="dimmed">
|
|
104
|
+
Loading the recording…
|
|
105
|
+
</Text>
|
|
106
|
+
)}
|
|
74
107
|
{report.isError && (
|
|
75
|
-
<
|
|
108
|
+
<Alert role="alert" color={'red'} my="sm">
|
|
76
109
|
The recording could not be read. {report.error.message}{' '}
|
|
77
|
-
<
|
|
78
|
-
|
|
110
|
+
<Button type="button" onClick={() => void report.refetch()} variant="subtle">
|
|
111
|
+
Try again
|
|
112
|
+
</Button>
|
|
113
|
+
</Alert>
|
|
79
114
|
)}
|
|
80
115
|
{wantsCss && css.isError && (
|
|
81
|
-
<
|
|
116
|
+
<Alert role="alert" color={'red'} my="sm">
|
|
82
117
|
The recorded stylesheet could not be read, so pictures appear unstyled.{' '}
|
|
83
|
-
<
|
|
84
|
-
|
|
118
|
+
<Button type="button" onClick={() => void css.refetch()} variant="subtle">
|
|
119
|
+
Try again
|
|
120
|
+
</Button>
|
|
121
|
+
</Alert>
|
|
122
|
+
)}
|
|
123
|
+
{report.data && paths && (
|
|
124
|
+
<Timeline report={report.data} fileUrl={paths.file} css={css.data ?? ''} moment={moment} onMoment={onMoment} />
|
|
85
125
|
)}
|
|
86
|
-
|
|
87
|
-
</article>
|
|
126
|
+
</Container>
|
|
88
127
|
);
|
|
89
128
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { flattenSteps, neighborFailure, orderedUiBlobs, selectedFrame, testFrames } from '@crvouga/atlas/reports/frames';
|
|
2
2
|
import { performanceFrameIndex, performanceRows, type PerformanceRow } from '@crvouga/atlas/reports/performance';
|
|
3
3
|
import type { ReportStep, RunReport } from '@crvouga/atlas/reports/report-types';
|
|
4
|
+
import { Accordion, Box, Button, Code, List, NativeSelect, Progress, Stack, Tabs, Text, VisuallyHidden } from '@mantine/core';
|
|
4
5
|
import { useMemo, useState } from 'react';
|
|
5
|
-
|
|
6
6
|
import { reportLooks } from '../looks/collect';
|
|
7
7
|
import { Stage, type LookMode } from '../looks/Stage';
|
|
8
8
|
import { reportAssetUrl } from './api';
|
|
9
9
|
import { duration } from './format';
|
|
10
10
|
import { StatusMark } from './marks';
|
|
11
|
-
import styles from './recording.module.css';
|
|
12
11
|
import { Snapshot, SnapshotData } from './snapshot';
|
|
13
|
-
|
|
14
12
|
type Props = {
|
|
15
13
|
report: RunReport;
|
|
16
14
|
/** Where a captured file of this test is read from. */
|
|
@@ -20,7 +18,6 @@ type Props = {
|
|
|
20
18
|
moment: number | null;
|
|
21
19
|
onMoment: (moment: number, replace: boolean) => void;
|
|
22
20
|
};
|
|
23
|
-
|
|
24
21
|
/** A recorded test, one captured moment at a time. */
|
|
25
22
|
export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
|
|
26
23
|
const [actor, setActor] = useState('all');
|
|
@@ -30,7 +27,11 @@ export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
|
|
|
30
27
|
const frames = useMemo(() => testFrames(test), [test]);
|
|
31
28
|
const looks = useMemo(() => reportLooks(report, (file) => reportAssetUrl(fileUrl(file))), [report, fileUrl]);
|
|
32
29
|
const extras = (report.artifacts ?? []).filter((artifact) => artifact.media !== 'png' && artifact.media !== 'video');
|
|
33
|
-
const frame = selectedFrame(frames, {
|
|
30
|
+
const frame = selectedFrame(frames, {
|
|
31
|
+
view: 'test',
|
|
32
|
+
testId: report.meta.testId,
|
|
33
|
+
params: { frame: moment === null ? '' : String(moment) }
|
|
34
|
+
});
|
|
34
35
|
const index = frame?.index ?? 0;
|
|
35
36
|
const last = frames.length - 1;
|
|
36
37
|
const go = (target: number) => {
|
|
@@ -53,53 +54,67 @@ export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
|
|
|
53
54
|
go(failure.index);
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
|
-
|
|
57
57
|
return (
|
|
58
|
-
<
|
|
58
|
+
<Stack gap="sm">
|
|
59
59
|
{report.meta.error && (
|
|
60
|
-
<
|
|
60
|
+
<Stack role="alert" gap="sm" c="red.7">
|
|
61
61
|
{report.meta.error}
|
|
62
62
|
{report.meta.errorStack && (
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
<Accordion variant="separated" my="sm">
|
|
64
|
+
<Accordion.Item value="content">
|
|
65
|
+
<Accordion.Control>Where it failed</Accordion.Control>
|
|
66
|
+
<Accordion.Panel>{report.meta.errorStack}</Accordion.Panel>
|
|
67
|
+
</Accordion.Item>
|
|
68
|
+
</Accordion>
|
|
67
69
|
)}
|
|
68
|
-
</
|
|
70
|
+
</Stack>
|
|
69
71
|
)}
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
<Tabs value={tab} onChange={(value) => setTab(value as typeof tab)} mb="md">
|
|
73
|
+
<Tabs.List aria-label="Recording">
|
|
74
|
+
<Tabs.Tab value="moments">Moments</Tabs.Tab>
|
|
75
|
+
<Tabs.Tab value="timing">Timing</Tabs.Tab>
|
|
76
|
+
</Tabs.List>
|
|
77
|
+
</Tabs>
|
|
74
78
|
{tab === 'timing' ? (
|
|
75
79
|
<Timing rows={rows} canOpen={(step) => byStep.has(step)} open={jump} />
|
|
76
80
|
) : (
|
|
77
|
-
<
|
|
78
|
-
<
|
|
81
|
+
<Stack gap="sm">
|
|
82
|
+
<List aria-label="Recorded steps" spacing="xs" listStyleType="none" type="ordered">
|
|
79
83
|
{steps.map((entry) => {
|
|
80
84
|
const current = frame?.step === entry.step;
|
|
81
85
|
return (
|
|
82
|
-
<
|
|
83
|
-
<
|
|
86
|
+
<List.Item key={entry.index} pl={entry.depth ? 'md' : 0}>
|
|
87
|
+
<Button
|
|
88
|
+
type="button"
|
|
89
|
+
data-status={entry.step.status}
|
|
90
|
+
aria-current={current ? 'step' : undefined}
|
|
91
|
+
disabled={!byStep.has(entry.step)}
|
|
92
|
+
onClick={() => jump(entry.step)}
|
|
93
|
+
variant="light"
|
|
94
|
+
>
|
|
84
95
|
<StatusMark status={entry.step.status} size={14} />
|
|
85
|
-
<span
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
<Text span inherit>
|
|
97
|
+
{entry.step.title}
|
|
98
|
+
</Text>
|
|
99
|
+
<Text span inherit c="dimmed">
|
|
100
|
+
{duration(entry.step.durationMs)}
|
|
101
|
+
</Text>
|
|
102
|
+
</Button>
|
|
103
|
+
</List.Item>
|
|
89
104
|
);
|
|
90
105
|
})}
|
|
91
|
-
</
|
|
92
|
-
<
|
|
106
|
+
</List>
|
|
107
|
+
<Box aria-label="Recorded moment" component="section" mb="lg">
|
|
93
108
|
{report.meta.actorIds.length > 1 && (
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
<
|
|
109
|
+
<Box component="label">
|
|
110
|
+
<VisuallyHidden>Person</VisuallyHidden>
|
|
111
|
+
<NativeSelect value={actor} onChange={(event) => setActor(event.target.value)}>
|
|
97
112
|
<option value="all">Everyone</option>
|
|
98
113
|
{report.meta.actorIds.map((id) => (
|
|
99
114
|
<option key={id}>{id}</option>
|
|
100
115
|
))}
|
|
101
|
-
</
|
|
102
|
-
</
|
|
116
|
+
</NativeSelect>
|
|
117
|
+
</Box>
|
|
103
118
|
)}
|
|
104
119
|
<Stage
|
|
105
120
|
looks={looks}
|
|
@@ -110,79 +125,131 @@ export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
|
|
|
110
125
|
onMoment={go}
|
|
111
126
|
failure={frames.some((entry) => entry.step.status === 'failed')}
|
|
112
127
|
onFailure={nextFailure}
|
|
113
|
-
empty={
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
empty={
|
|
129
|
+
extras.length > 0 ? (
|
|
130
|
+
<Text c="dimmed">This runner attached files instead of a step-by-step timeline. They are below.</Text>
|
|
131
|
+
) : (
|
|
132
|
+
<Text c="dimmed">This test recorded no picture, video, or timeline. Its steps and outcome are on the left.</Text>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
116
135
|
renderMoment={(at) => {
|
|
117
136
|
const current = frames[at];
|
|
118
137
|
if (!current) return null;
|
|
119
138
|
const ui = orderedUiBlobs(current, test);
|
|
120
|
-
const pictures = ui.filter(
|
|
139
|
+
const pictures = ui.filter(
|
|
140
|
+
(entry) => entry.blob && entry.blob.media !== 'video' && (actor === 'all' || entry.nodeId === actor)
|
|
141
|
+
);
|
|
121
142
|
const absent = ui.filter((entry) => !entry.blob && (actor === 'all' || !entry.nodeId || entry.nodeId === actor));
|
|
122
|
-
const observations = current.frame.snapshots.filter(
|
|
143
|
+
const observations = current.frame.snapshots.filter(
|
|
144
|
+
(blob) =>
|
|
145
|
+
blob.media !== 'html' &&
|
|
146
|
+
blob.media !== 'png' &&
|
|
147
|
+
blob.media !== 'video' &&
|
|
148
|
+
(actor === 'all' || !blob.nodeId || blob.nodeId === actor)
|
|
149
|
+
);
|
|
123
150
|
return (
|
|
124
151
|
<>
|
|
125
|
-
<
|
|
126
|
-
{current.step.status === 'failed' ? 'Failed' : 'Passed'} · at
|
|
152
|
+
<Text c="dimmed">
|
|
153
|
+
{current.step.status === 'failed' ? 'Failed' : 'Passed'} · at{' '}
|
|
154
|
+
{duration(Math.max(0, current.frame.capturedAtMs - report.meta.startedAtMs)) || '0 ms'}
|
|
127
155
|
{current.frame.hit && <> · acted on {current.frame.hit.path.join(' › ')}</>}
|
|
128
|
-
</
|
|
129
|
-
{current.step.error &&
|
|
156
|
+
</Text>
|
|
157
|
+
{current.step.error && (
|
|
158
|
+
<Code role="alert" c="dimmed">
|
|
159
|
+
{current.step.error}
|
|
160
|
+
</Code>
|
|
161
|
+
)}
|
|
130
162
|
{current.step.detail && (
|
|
131
|
-
<
|
|
163
|
+
<Stack>
|
|
132
164
|
<SnapshotData text={current.step.detail} />
|
|
133
|
-
</
|
|
165
|
+
</Stack>
|
|
134
166
|
)}
|
|
135
|
-
<
|
|
167
|
+
<Stack aria-label="Recorded snapshots">
|
|
136
168
|
{pictures.map((entry) => (
|
|
137
|
-
<Snapshot
|
|
169
|
+
<Snapshot
|
|
170
|
+
key={entry.blob!.file}
|
|
171
|
+
url={fileUrl(entry.blob!.file)}
|
|
172
|
+
blob={entry.blob!}
|
|
173
|
+
css={css}
|
|
174
|
+
hit={entry.stale ? null : (current.frame.hit ?? null)}
|
|
175
|
+
earlier={entry.stale}
|
|
176
|
+
/>
|
|
138
177
|
))}
|
|
139
178
|
{absent.map((entry) => (
|
|
140
|
-
<
|
|
179
|
+
<Text key={entry.nodeId} c="dimmed">
|
|
180
|
+
No picture of {entry.nodeId} had been recorded by this moment.
|
|
181
|
+
</Text>
|
|
141
182
|
))}
|
|
142
183
|
{observations.map((blob) => (
|
|
143
184
|
<Snapshot key={blob.file} url={fileUrl(blob.file)} blob={blob} css={css} hit={null} />
|
|
144
185
|
))}
|
|
145
|
-
</
|
|
186
|
+
</Stack>
|
|
146
187
|
</>
|
|
147
188
|
);
|
|
148
189
|
}}
|
|
149
190
|
/>
|
|
150
191
|
{extras.length > 0 && (
|
|
151
|
-
<
|
|
192
|
+
<Stack aria-label="Other attachments" gap="sm">
|
|
152
193
|
{extras.map((artifact) => (
|
|
153
|
-
<Snapshot
|
|
194
|
+
<Snapshot
|
|
195
|
+
key={artifact.file}
|
|
196
|
+
url={fileUrl(artifact.file)}
|
|
197
|
+
blob={{ name: artifact.name, media: artifact.media, file: artifact.file }}
|
|
198
|
+
css={css}
|
|
199
|
+
hit={null}
|
|
200
|
+
/>
|
|
154
201
|
))}
|
|
155
|
-
</
|
|
202
|
+
</Stack>
|
|
156
203
|
)}
|
|
157
|
-
</
|
|
158
|
-
</
|
|
204
|
+
</Box>
|
|
205
|
+
</Stack>
|
|
159
206
|
)}
|
|
160
|
-
</
|
|
207
|
+
</Stack>
|
|
161
208
|
);
|
|
162
209
|
}
|
|
163
|
-
|
|
164
210
|
/** Where the recorded time went. A step without a measurement is never shown as quick. */
|
|
165
|
-
function Timing({
|
|
211
|
+
function Timing({
|
|
212
|
+
rows,
|
|
213
|
+
canOpen,
|
|
214
|
+
open
|
|
215
|
+
}: {
|
|
216
|
+
rows: readonly PerformanceRow[];
|
|
217
|
+
canOpen: (step: ReportStep) => boolean;
|
|
218
|
+
open: (step: ReportStep) => void;
|
|
219
|
+
}) {
|
|
166
220
|
const measured = rows.filter((row) => row.band !== 'unmeasured');
|
|
167
|
-
if (!measured.length)
|
|
221
|
+
if (!measured.length)
|
|
222
|
+
return (
|
|
223
|
+
<Text component="div" c="dimmed">
|
|
224
|
+
Measurements are unavailable for this recording.
|
|
225
|
+
</Text>
|
|
226
|
+
);
|
|
168
227
|
const longest = Math.max(...measured.map((row) => row.step.performance?.selfMs ?? 0), 1);
|
|
169
228
|
return (
|
|
170
|
-
<
|
|
229
|
+
<List aria-label="Recorded time per step" spacing="xs" listStyleType="none" type="ordered">
|
|
171
230
|
{rows.map((row) => {
|
|
172
231
|
const own = row.step.performance?.selfMs;
|
|
173
232
|
return (
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
<span
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
233
|
+
<List.Item key={row.index} data-band={row.band}>
|
|
234
|
+
<Button type="button" disabled={!canOpen(row.step)} onClick={() => open(row.step)} pl={row.depth ? 'lg' : 'sm'} variant="light">
|
|
235
|
+
<Text span inherit>
|
|
236
|
+
{row.step.title}
|
|
237
|
+
</Text>
|
|
238
|
+
{row.band === 'slow' && (
|
|
239
|
+
<Text span inherit c="red.7">
|
|
240
|
+
Slow
|
|
241
|
+
</Text>
|
|
242
|
+
)}
|
|
243
|
+
<Text aria-hidden="true" span inherit>
|
|
244
|
+
<Progress value={own === undefined ? 0 : (own / longest) * 100} w="6rem" size="sm" />
|
|
245
|
+
</Text>
|
|
246
|
+
<Text span inherit c="dimmed">
|
|
247
|
+
{own === undefined ? 'Not measured' : `${own.toFixed(1)} ms`}
|
|
248
|
+
</Text>
|
|
249
|
+
</Button>
|
|
250
|
+
</List.Item>
|
|
184
251
|
);
|
|
185
252
|
})}
|
|
186
|
-
</
|
|
253
|
+
</List>
|
|
187
254
|
);
|
|
188
255
|
}
|