@crvouga/atlas 0.10.1 → 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 +12 -0
- package/README.md +7 -0
- package/dist/reports/src/layer-merge.d.ts +5 -1
- package/dist/reports/src/layer-merge.js +17 -2
- package/dist/server/src/settings-page.d.ts +4 -4
- package/dist/server/src/settings-page.js +17 -78
- package/docs/agents.md +25 -0
- package/docs/reference/reports.md +2 -0
- package/docs/reference/server.md +4 -4
- package/docs/report-contract.md +3 -0
- package/docs/ui.md +142 -0
- package/docs/workspaces.md +130 -0
- package/llms-full.txt +306 -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,64 +1,95 @@
|
|
|
1
1
|
import type { SpecEvidence } from '@crvouga/atlas/reports/evidence-view';
|
|
2
|
+
import { Anchor, Box, Code, Container, List, Title } from '@mantine/core';
|
|
2
3
|
import { useMemo } from 'react';
|
|
3
|
-
|
|
4
4
|
import type { SpecKind } from '../spec-kind';
|
|
5
5
|
import { CheckDetail, ResultRow, type CheckSelection, type SelectCheck } from './check-detail';
|
|
6
|
-
import styles from './evidence.module.css';
|
|
7
6
|
import { Bounded } from './marks';
|
|
8
7
|
import { QUINT_GROUPS, quintOutline } from './quint';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { SpecHeader } from './spec-header';
|
|
9
|
+
import { SourceText } from './source-text';
|
|
11
10
|
import { TestFilter, useRunningFilter } from './test-filter';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
type Props = {
|
|
12
|
+
file: string;
|
|
13
|
+
text: string;
|
|
14
|
+
kind: SpecKind;
|
|
15
|
+
entry: SpecEvidence | undefined;
|
|
16
|
+
selection: CheckSelection;
|
|
17
|
+
select: SelectCheck;
|
|
18
|
+
};
|
|
15
19
|
/** A model read as its state and actions, with every trace that was replayed against the app. */
|
|
16
20
|
export function ModelPage({ file, text, kind, entry, selection, select }: Props) {
|
|
17
21
|
const items = useMemo(() => quintOutline(text), [text]);
|
|
18
22
|
const moduleName = items.find((item) => item.kind === 'module')?.name;
|
|
19
|
-
const slots = useMemo(
|
|
23
|
+
const slots = useMemo(
|
|
24
|
+
() => [...(entry?.slots ?? [])].sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true })),
|
|
25
|
+
[entry]
|
|
26
|
+
);
|
|
20
27
|
const runningOnly = useRunningFilter();
|
|
21
|
-
const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
|
|
28
|
+
const visible = slots.filter((slot) => !runningOnly || slot.status === 'running');
|
|
22
29
|
const toggle = (id: string) => select({ check: selection.check === id ? null : id, layer: null });
|
|
23
30
|
const label = (title: string) => (moduleName && title.startsWith(`${moduleName} · `) ? title.slice(moduleName.length + 3) : title);
|
|
24
31
|
return (
|
|
25
|
-
<article
|
|
32
|
+
<Container component="article" size="xl" w="100%" py="xl">
|
|
26
33
|
<SpecHeader title={moduleName ?? file.split('/').at(-1)!} file={file} kind={kind} entry={entry} />
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
34
|
+
<SourceText text={text} file={file} withResults />
|
|
35
|
+
<Box component="section" id="spec-results" aria-label="Recorded results" tabIndex={-1}>
|
|
36
|
+
<TestFilter slots={slots} />
|
|
37
|
+
{visible.length > 0 && (
|
|
38
|
+
<Box component="section" mb="lg">
|
|
39
|
+
<Title order={2} mb="sm">
|
|
40
|
+
Traces
|
|
41
|
+
</Title>
|
|
42
|
+
<List spacing="xs" listStyleType="none">
|
|
43
|
+
<Bounded items={visible} size={40} more="More traces">
|
|
44
|
+
{(slot) => (
|
|
45
|
+
<ResultRow
|
|
46
|
+
key={slot.key}
|
|
47
|
+
label={label(slot.title)}
|
|
48
|
+
slot={slot}
|
|
49
|
+
open={selection.check === slot.id}
|
|
50
|
+
onToggle={() => toggle(slot.id)}
|
|
51
|
+
>
|
|
52
|
+
<CheckDetail
|
|
53
|
+
slot={slot}
|
|
54
|
+
layerId={selection.check === slot.id ? selection.layer : undefined}
|
|
55
|
+
select={(patch) => select({ check: slot.id, ...patch })}
|
|
56
|
+
reads="trace"
|
|
57
|
+
/>
|
|
58
|
+
</ResultRow>
|
|
59
|
+
)}
|
|
60
|
+
</Bounded>
|
|
61
|
+
</List>
|
|
62
|
+
</Box>
|
|
63
|
+
)}
|
|
64
|
+
{QUINT_GROUPS.map((group) => {
|
|
65
|
+
const declared = items.filter((item) => item.kind === group.kind);
|
|
66
|
+
if (!declared.length) return null;
|
|
67
|
+
return (
|
|
68
|
+
<Box key={group.kind} component="section" mb="lg">
|
|
69
|
+
<Title order={2} mb="sm">
|
|
70
|
+
{group.label}
|
|
71
|
+
</Title>
|
|
72
|
+
<List spacing="xs" listStyleType="none">
|
|
73
|
+
{declared.map((item) => (
|
|
74
|
+
<List.Item key={item.line}>
|
|
75
|
+
<Anchor
|
|
76
|
+
href={`#line-${item.line}`}
|
|
77
|
+
onClick={(event) => {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
const leading = (text.match(/^(?:[^\S\r\n]*\r?\n)+/)?.[0].match(/\n/g) ?? []).length;
|
|
80
|
+
const line = document.getElementById(`line-${item.line - leading}`) ?? document.querySelector('[data-source]');
|
|
81
|
+
line?.scrollIntoView({ block: 'center' });
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Code>{item.text}</Code>
|
|
85
|
+
</Anchor>
|
|
86
|
+
</List.Item>
|
|
87
|
+
))}
|
|
88
|
+
</List>
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</Box>
|
|
93
|
+
</Container>
|
|
63
94
|
);
|
|
64
95
|
}
|
|
@@ -1,57 +1,77 @@
|
|
|
1
|
+
import { Button, Code, List, Stack, Table, Text } from '@mantine/core';
|
|
1
2
|
import { useState } from 'react';
|
|
2
|
-
|
|
3
3
|
import type { ModelState } from './quint';
|
|
4
|
-
import styles from './recording.module.css';
|
|
5
|
-
|
|
6
4
|
function shown(value: unknown) {
|
|
7
5
|
if (value === undefined) return '—';
|
|
8
6
|
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
9
7
|
}
|
|
10
|
-
|
|
11
8
|
/** The states a model run went through, one at a time, with what each step changed. */
|
|
12
9
|
export function ModelTrace({ states }: { states: readonly ModelState[] }) {
|
|
13
10
|
const [selected, setSelected] = useState(0);
|
|
14
|
-
if (!states.length)
|
|
11
|
+
if (!states.length)
|
|
12
|
+
return (
|
|
13
|
+
<Text component="div" c="dimmed">
|
|
14
|
+
No model states were recorded.
|
|
15
|
+
</Text>
|
|
16
|
+
);
|
|
15
17
|
const at = Math.min(selected, states.length - 1);
|
|
16
18
|
const state = states[at]!;
|
|
17
19
|
const previous = states[at - 1];
|
|
18
20
|
const variables = [...new Set([...Object.keys(previous ?? {}), ...Object.keys(state)])].filter((name) => !name.startsWith('#'));
|
|
19
21
|
const changed = (name: string) => previous !== undefined && JSON.stringify(previous[name]) !== JSON.stringify(state[name]);
|
|
20
22
|
return (
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
+
<Stack gap="sm" miw={0}>
|
|
24
|
+
<List aria-label="Model trace states" spacing="xs" listStyleType="none" type="ordered">
|
|
23
25
|
{states.map((entry, index) => (
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
<List.Item key={index}>
|
|
27
|
+
<Button
|
|
28
|
+
type="button"
|
|
29
|
+
aria-label={`State ${index + 1}`}
|
|
30
|
+
aria-pressed={at === index}
|
|
31
|
+
onClick={() => setSelected(index)}
|
|
32
|
+
variant="light"
|
|
33
|
+
>
|
|
34
|
+
<Text span inherit>
|
|
35
|
+
{index + 1}
|
|
36
|
+
</Text>
|
|
37
|
+
<Text span inherit>
|
|
38
|
+
{typeof entry['#action'] === 'string' ? entry['#action'] : index === 0 ? 'Start' : 'Step'}
|
|
39
|
+
</Text>
|
|
40
|
+
</Button>
|
|
41
|
+
</List.Item>
|
|
30
42
|
))}
|
|
31
|
-
</
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
</
|
|
40
|
-
</
|
|
41
|
-
<
|
|
43
|
+
</List>
|
|
44
|
+
<Table.ScrollContainer minWidth={600}>
|
|
45
|
+
<Table>
|
|
46
|
+
<Table.Thead>
|
|
47
|
+
<Table.Tr>
|
|
48
|
+
<Table.Th scope="col">Variable</Table.Th>
|
|
49
|
+
<Table.Th scope="col">Before</Table.Th>
|
|
50
|
+
<Table.Th scope="col">State {at + 1}</Table.Th>
|
|
51
|
+
</Table.Tr>
|
|
52
|
+
</Table.Thead>
|
|
53
|
+
<Table.Tbody>
|
|
42
54
|
{variables.map((name) => (
|
|
43
|
-
<
|
|
44
|
-
<
|
|
55
|
+
<Table.Tr key={name} data-changed={changed(name) || undefined}>
|
|
56
|
+
<Table.Th scope="row">
|
|
45
57
|
{name}
|
|
46
|
-
{changed(name) &&
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
{changed(name) && (
|
|
59
|
+
<Text span inherit>
|
|
60
|
+
Changed
|
|
61
|
+
</Text>
|
|
62
|
+
)}
|
|
63
|
+
</Table.Th>
|
|
64
|
+
<Table.Td>
|
|
65
|
+
<Code block>{previous ? shown(previous[name]) : '—'}</Code>
|
|
66
|
+
</Table.Td>
|
|
67
|
+
<Table.Td>
|
|
68
|
+
<Code block>{shown(state[name])}</Code>
|
|
69
|
+
</Table.Td>
|
|
70
|
+
</Table.Tr>
|
|
51
71
|
))}
|
|
52
|
-
</
|
|
53
|
-
</
|
|
54
|
-
</
|
|
55
|
-
</
|
|
72
|
+
</Table.Tbody>
|
|
73
|
+
</Table>
|
|
74
|
+
</Table.ScrollContainer>
|
|
75
|
+
</Stack>
|
|
56
76
|
);
|
|
57
77
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { SpecLogo } from '../components/SpecLogo';
|
|
1
2
|
import { addSummaries, NO_EVIDENCE, type SpecSummary } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import { Alert, Anchor, Box, Button, Container, Flex, Group, List, Stack, Text, Title } from '@mantine/core';
|
|
2
4
|
import { Link } from '@tanstack/react-router';
|
|
3
5
|
import { useMemo } from 'react';
|
|
4
|
-
|
|
5
6
|
import { folderOf, outlinePath, type SpecKind } from '../spec-kind';
|
|
6
7
|
import { useUiStore } from '../state/ui-store';
|
|
7
8
|
import { useEvidence } from './evidence-context';
|
|
8
|
-
import styles from './evidence.module.css';
|
|
9
9
|
import { ago, plural, useNow } from './format';
|
|
10
10
|
import { backendLabel, Bounded, CountLine, Meter, StatusMark, Tally } from './marks';
|
|
11
|
-
|
|
12
11
|
/** A kind's specs at a glance: how many have results, which are failing, and what just ran. */
|
|
13
12
|
export function Overview({ kind }: { kind: SpecKind }) {
|
|
14
13
|
const evidence = useEvidence();
|
|
@@ -16,119 +15,188 @@ export function Overview({ kind }: { kind: SpecKind }) {
|
|
|
16
15
|
const setFilter = useUiStore((s) => s.setResultFilter);
|
|
17
16
|
const setSourcesOpen = useUiStore((s) => s.setSourcesOpen);
|
|
18
17
|
const files = useMemo(() => evidence.files.filter(kind.accepts), [evidence.files, kind]);
|
|
19
|
-
const rows = useMemo(
|
|
18
|
+
const rows = useMemo(
|
|
19
|
+
() => files.map((file) => ({ file, summary: evidence.view.specs.get(outlinePath(file))?.summary ?? NO_EVIDENCE })),
|
|
20
|
+
[files, evidence.view]
|
|
21
|
+
);
|
|
20
22
|
const total = useMemo(() => addSummaries(rows.map((row) => row.summary)), [rows]);
|
|
21
23
|
const withResults = rows.filter((row) => row.summary.status !== 'none');
|
|
22
24
|
const failing = rows.filter((row) => row.summary.counts.failed > 0).sort((a, b) => b.summary.counts.failed - a.summary.counts.failed);
|
|
23
25
|
const recent = [...withResults].sort((a, b) => (b.summary.latestAtMs ?? 0) - (a.summary.latestAtMs ?? 0)).slice(0, 8);
|
|
24
26
|
const empty = rows.length - withResults.length;
|
|
25
27
|
const specRow = (row: { file: string; summary: SpecSummary }) => (
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
<List.Item key={row.file}>
|
|
29
|
+
<Anchor
|
|
30
|
+
display="block"
|
|
31
|
+
p="sm"
|
|
32
|
+
underline="never"
|
|
33
|
+
renderRoot={(props) => <Link {...props} to="/specs/$kind" params={{ kind: kind.id }} search={{ spec: row.file }} />}
|
|
34
|
+
>
|
|
35
|
+
<Group gap="md" align="center" wrap="nowrap">
|
|
36
|
+
<StatusMark status={row.summary.status} />
|
|
37
|
+
<Stack gap={0} flex={1} miw={0}>
|
|
38
|
+
<Text span inherit fw={500} truncate title={evidence.titleOf(row.file)}>
|
|
39
|
+
{evidence.titleOf(row.file)}
|
|
40
|
+
</Text>
|
|
41
|
+
{folderOf(row.file) && (
|
|
42
|
+
<Text span inherit c="dimmed" truncate title={folderOf(row.file)}>
|
|
43
|
+
{folderOf(row.file)}
|
|
44
|
+
</Text>
|
|
45
|
+
)}
|
|
46
|
+
</Stack>
|
|
47
|
+
<Flex direction={{ base: 'column', sm: 'row' }} align={{ base: 'flex-end', sm: 'center' }} gap={{ base: 'xs', sm: 'md' }}>
|
|
48
|
+
<Tally summary={row.summary} />
|
|
49
|
+
{row.summary.latestAtMs !== null && (
|
|
50
|
+
<Text span inherit c="dimmed" textWrap="nowrap">
|
|
51
|
+
{ago(row.summary.latestAtMs, now)}
|
|
52
|
+
</Text>
|
|
53
|
+
)}
|
|
54
|
+
</Flex>
|
|
55
|
+
</Group>
|
|
56
|
+
</Anchor>
|
|
57
|
+
</List.Item>
|
|
37
58
|
);
|
|
38
59
|
return (
|
|
39
|
-
<article
|
|
40
|
-
<header
|
|
41
|
-
<
|
|
42
|
-
|
|
60
|
+
<Container component="article" size="xl" w="100%" py="xl">
|
|
61
|
+
<Box component="header" mb="lg">
|
|
62
|
+
<Flex gap="sm" align="center" mb="sm">
|
|
63
|
+
<SpecLogo kind={kind} />
|
|
64
|
+
<Title order={1}>{kind.label}</Title>
|
|
65
|
+
</Flex>
|
|
66
|
+
<Stack gap="sm" miw={0}>
|
|
43
67
|
<Meter counts={total.counts} />
|
|
44
|
-
<
|
|
45
|
-
<span
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
69
|
+
<Text span inherit>
|
|
70
|
+
{plural(files.length, kind.one, kind.many)}
|
|
71
|
+
</Text>
|
|
72
|
+
{evidence.ready && (
|
|
73
|
+
<Text span inherit>
|
|
74
|
+
{withResults.length} with results
|
|
75
|
+
</Text>
|
|
76
|
+
)}
|
|
77
|
+
{evidence.ready && empty > 0 && (
|
|
78
|
+
<Text span inherit>
|
|
79
|
+
{empty} without
|
|
80
|
+
</Text>
|
|
81
|
+
)}
|
|
82
|
+
</Flex>
|
|
49
83
|
{total.status !== 'none' && (
|
|
50
|
-
<
|
|
84
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
51
85
|
<CountLine summary={total} noun={kind.check} nouns={`${kind.check}s`} />
|
|
52
|
-
</
|
|
86
|
+
</Flex>
|
|
53
87
|
)}
|
|
54
|
-
</
|
|
55
|
-
</
|
|
88
|
+
</Stack>
|
|
89
|
+
</Box>
|
|
56
90
|
|
|
57
91
|
{evidence.unavailable && (
|
|
58
|
-
<
|
|
92
|
+
<Alert role="alert" color={'red'} my="sm">
|
|
59
93
|
Recorded results are unavailable. {evidence.unavailable}{' '}
|
|
60
|
-
<
|
|
61
|
-
|
|
94
|
+
<Button type="button" onClick={evidence.retry} variant="subtle">
|
|
95
|
+
Try again
|
|
96
|
+
</Button>
|
|
97
|
+
</Alert>
|
|
62
98
|
)}
|
|
63
99
|
{evidence.notices.map((notice) => (
|
|
64
|
-
<
|
|
100
|
+
<Alert key={notice} role="status" color={'blue'} my="sm">
|
|
65
101
|
{notice}{' '}
|
|
66
|
-
<
|
|
67
|
-
|
|
102
|
+
<Button type="button" onClick={evidence.retry} variant="subtle">
|
|
103
|
+
Try again
|
|
104
|
+
</Button>
|
|
105
|
+
</Alert>
|
|
68
106
|
))}
|
|
69
|
-
{!evidence.ready && !evidence.unavailable &&
|
|
70
|
-
|
|
107
|
+
{!evidence.ready && !evidence.unavailable && (
|
|
108
|
+
<Text role="status" component="div" c="dimmed">
|
|
109
|
+
Loading recorded results…
|
|
110
|
+
</Text>
|
|
111
|
+
)}
|
|
112
|
+
{evidence.ready && evidence.pending > 0 && (
|
|
113
|
+
<Text role="status" component="div" c="dimmed">
|
|
114
|
+
Reading {plural(evidence.pending, 'more layer', 'more layers')}…
|
|
115
|
+
</Text>
|
|
116
|
+
)}
|
|
71
117
|
|
|
72
118
|
{evidence.active.length > 0 && (
|
|
73
|
-
<section
|
|
74
|
-
<
|
|
75
|
-
|
|
119
|
+
<Box component="section" mb="lg">
|
|
120
|
+
<Title order={2} mb="sm">
|
|
121
|
+
Running now
|
|
122
|
+
</Title>
|
|
123
|
+
<List spacing="xs" listStyleType="none">
|
|
76
124
|
{evidence.active.map((run) => (
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
125
|
+
<List.Item key={run.source}>
|
|
126
|
+
<Group gap="md" align="center">
|
|
127
|
+
<StatusMark status="running" />
|
|
128
|
+
<Text span inherit>
|
|
129
|
+
{run.packageDir}
|
|
130
|
+
</Text>
|
|
131
|
+
<Text data-status={run.failed ? 'failed' : 'running'} span inherit>
|
|
132
|
+
{plural(run.completed, 'result', 'results')} so far{run.failed ? `, ${run.failed} failed` : ''}
|
|
133
|
+
</Text>
|
|
134
|
+
<Text span inherit c="dimmed">
|
|
135
|
+
started {ago(run.startedAtMs, now)}
|
|
136
|
+
</Text>
|
|
137
|
+
</Group>
|
|
138
|
+
</List.Item>
|
|
85
139
|
))}
|
|
86
|
-
</
|
|
87
|
-
</
|
|
140
|
+
</List>
|
|
141
|
+
</Box>
|
|
88
142
|
)}
|
|
89
143
|
|
|
90
144
|
{failing.length > 0 && (
|
|
91
|
-
<section
|
|
92
|
-
<
|
|
93
|
-
Failing
|
|
94
|
-
|
|
95
|
-
|
|
145
|
+
<Box component="section" mb="lg">
|
|
146
|
+
<Title order={2} mb="sm">
|
|
147
|
+
Failing{' '}
|
|
148
|
+
<Text span inherit c="dimmed">
|
|
149
|
+
{plural(failing.length, kind.one, kind.many)}
|
|
150
|
+
</Text>
|
|
151
|
+
</Title>
|
|
152
|
+
<List spacing="xs" listStyleType="none">
|
|
96
153
|
<Bounded items={failing} size={12} more="More failing">
|
|
97
154
|
{specRow}
|
|
98
155
|
</Bounded>
|
|
99
|
-
</
|
|
100
|
-
</
|
|
156
|
+
</List>
|
|
157
|
+
</Box>
|
|
101
158
|
)}
|
|
102
159
|
|
|
103
160
|
{recent.length > 0 && (
|
|
104
|
-
<section
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
161
|
+
<Box component="section" mb="lg">
|
|
162
|
+
<Title order={2} mb="sm">
|
|
163
|
+
Latest results
|
|
164
|
+
</Title>
|
|
165
|
+
<List spacing="xs" listStyleType="none">
|
|
166
|
+
{recent.map(specRow)}
|
|
167
|
+
</List>
|
|
168
|
+
</Box>
|
|
108
169
|
)}
|
|
109
170
|
|
|
110
171
|
{evidence.ready && evidence.pending === 0 && withResults.length === 0 && !evidence.unavailable && (
|
|
111
|
-
<
|
|
172
|
+
<Text component="div" c="dimmed">
|
|
112
173
|
{evidence.layers.length === 0
|
|
113
174
|
? `Nothing has been recorded yet. Results appear here as a spec run finishes each test.`
|
|
114
175
|
: `None of the ${plural(evidence.layers.length, 'layer', 'layers')} in the combined report checked a ${kind.one}.`}
|
|
115
|
-
</
|
|
176
|
+
</Text>
|
|
116
177
|
)}
|
|
117
178
|
|
|
118
179
|
{evidence.ready && (
|
|
119
|
-
<
|
|
120
|
-
{plural(evidence.layers.length, 'layer', 'layers')} from
|
|
121
|
-
|
|
180
|
+
<Text component="div" c="dimmed">
|
|
181
|
+
{plural(evidence.layers.length, 'layer', 'layers')} from{' '}
|
|
182
|
+
{evidence.backends
|
|
183
|
+
.filter((backend) => backend.available)
|
|
184
|
+
.map((backend) => backendLabel(evidence.backends, backend.id))
|
|
185
|
+
.join(' and ') || 'no report store'}
|
|
186
|
+
.{' '}
|
|
187
|
+
<Button type="button" onClick={() => setSourcesOpen(true)} variant="subtle">
|
|
188
|
+
Choose sources
|
|
189
|
+
</Button>
|
|
122
190
|
{empty > 0 && (
|
|
123
191
|
<>
|
|
124
192
|
{' · '}
|
|
125
|
-
<
|
|
193
|
+
<Button type="button" onClick={() => setFilter('none')} variant="subtle">
|
|
126
194
|
Show the {empty} without results
|
|
127
|
-
</
|
|
195
|
+
</Button>
|
|
128
196
|
</>
|
|
129
197
|
)}
|
|
130
|
-
</
|
|
198
|
+
</Text>
|
|
131
199
|
)}
|
|
132
|
-
</
|
|
200
|
+
</Container>
|
|
133
201
|
);
|
|
134
202
|
}
|