@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,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { ScreenImage } from '../components/Screen';
|
|
1
|
+
import { Badge, Box, Divider, NavLink, Stack, Text, Title } from '@mantine/core';
|
|
2
|
+
import { Link, useParams, useSearch } from '@tanstack/react-router';
|
|
4
3
|
import { STATUS_LABEL, type AtlasView, type StateView } from '../data/model';
|
|
4
|
+
import { statusColor } from '../ui/theme';
|
|
5
5
|
import { useAtlas } from './atlas-context';
|
|
6
|
-
import styles from './gazetteer.module.css';
|
|
7
|
-
|
|
8
6
|
function openingScreen(view: AtlasView, steps: readonly { transitionIds: readonly string[] }[]) {
|
|
9
7
|
for (const step of steps) {
|
|
10
8
|
const transition = view.transitions.get(step.transitionIds[0] ?? '');
|
|
@@ -13,105 +11,114 @@ function openingScreen(view: AtlasView, steps: readonly { transitionIds: readonl
|
|
|
13
11
|
}
|
|
14
12
|
return undefined;
|
|
15
13
|
}
|
|
16
|
-
|
|
17
14
|
function screensOf(view: AtlasView, chartId: string) {
|
|
18
|
-
|
|
19
|
-
if (!chart) return [];
|
|
20
|
-
return chart.states
|
|
15
|
+
return (view.charts.get(chartId)?.states ?? [])
|
|
21
16
|
.map((key) => view.states.get(key))
|
|
22
|
-
.filter((state): state is StateView =>
|
|
17
|
+
.filter((state): state is StateView => !!state && (state.kind === 'screen' || state.kind === 'final'));
|
|
23
18
|
}
|
|
24
|
-
|
|
25
19
|
export function Gazetteer() {
|
|
26
20
|
const { view } = useAtlas();
|
|
27
21
|
const params = useParams({ strict: false });
|
|
28
22
|
const search = useSearch({ strict: false });
|
|
29
|
-
const { pathname } = useLocation();
|
|
30
23
|
const screen = typeof search.screen === 'string' ? search.screen : undefined;
|
|
31
24
|
const journeyId = typeof search.journey === 'string' ? search.journey : undefined;
|
|
32
25
|
const group = typeof search.group === 'string' ? search.group : undefined;
|
|
33
26
|
const journeys = (view?.journeys ?? []).filter((journey) => journey.chartIds.length > 0);
|
|
34
27
|
return (
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
<Box component="nav" aria-label="Places" p="md">
|
|
29
|
+
<Stack gap="lg">
|
|
30
|
+
<Title order={2}>Places</Title>
|
|
31
|
+
{!view && <Text c="dimmed">Loading the spec…</Text>}
|
|
32
|
+
{view?.contexts.map((context) => (
|
|
33
|
+
<Stack key={context.id} gap="xs">
|
|
34
|
+
<Text size="xs" fw={600} c="dimmed">
|
|
35
|
+
{context.name}
|
|
36
|
+
</Text>
|
|
37
|
+
{context.chartIds.map((chartId) => {
|
|
38
|
+
const chart = view.charts.get(chartId);
|
|
39
|
+
if (!chart) return null;
|
|
40
|
+
return (
|
|
41
|
+
<Box key={chartId}>
|
|
42
|
+
<NavLink
|
|
43
|
+
label={chart.name}
|
|
44
|
+
fw={600}
|
|
45
|
+
active={params.chartId === chartId && !screen && !journeyId && !group}
|
|
46
|
+
renderRoot={(props) => (
|
|
53
47
|
<Link
|
|
54
|
-
|
|
55
|
-
data-status={state.status}
|
|
48
|
+
{...props}
|
|
56
49
|
to="/chart/$chartId"
|
|
57
50
|
params={{ chartId }}
|
|
58
|
-
search={(prev) => ({ run: prev.run, screen:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
search={(prev) => ({ run: prev.run, screen: undefined, journey: undefined, group: undefined })}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
{screensOf(view, chartId).map((state) => (
|
|
56
|
+
<NavLink
|
|
57
|
+
key={state.key}
|
|
58
|
+
label={state.name}
|
|
59
|
+
active={params.chartId === chartId && screen === state.name}
|
|
60
|
+
aria-current={params.chartId === chartId && screen === state.name ? 'page' : undefined}
|
|
61
|
+
rightSection={
|
|
62
|
+
<Badge color={statusColor(state.status)} size="xs">
|
|
63
|
+
{STATUS_LABEL[state.status]}
|
|
64
|
+
</Badge>
|
|
65
|
+
}
|
|
66
|
+
renderRoot={(props) => (
|
|
67
|
+
<Link
|
|
68
|
+
{...props}
|
|
69
|
+
to="/chart/$chartId"
|
|
70
|
+
params={{ chartId }}
|
|
71
|
+
search={(prev) => ({ run: prev.run, screen: state.name, journey: prev.journey })}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
68
75
|
))}
|
|
69
|
-
</
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<ul>
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</Stack>
|
|
80
|
+
))}
|
|
81
|
+
{journeys.length > 0 && (
|
|
82
|
+
<Stack gap="xs">
|
|
83
|
+
<Divider />
|
|
84
|
+
<Title order={3}>Journeys</Title>
|
|
79
85
|
{journeys.map((journey) => {
|
|
80
|
-
const
|
|
86
|
+
const chartId = journey.chartIds[0]!;
|
|
87
|
+
const start = view && openingScreen(view, journey.steps);
|
|
81
88
|
return (
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
<NavLink
|
|
90
|
+
key={journey.id}
|
|
91
|
+
label={journey.name}
|
|
92
|
+
active={journeyId === journey.id}
|
|
93
|
+
rightSection={
|
|
94
|
+
<Badge size="xs" color={statusColor(journey.status)}>
|
|
95
|
+
{STATUS_LABEL[journey.status]}
|
|
96
|
+
</Badge>
|
|
97
|
+
}
|
|
98
|
+
renderRoot={(props) => (
|
|
99
|
+
<Link
|
|
100
|
+
{...props}
|
|
101
|
+
to="/chart/$chartId"
|
|
102
|
+
params={{ chartId }}
|
|
103
|
+
search={(prev) => ({ run: prev.run, journey: journey.id, screen: start?.name })}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
100
107
|
);
|
|
101
108
|
})}
|
|
102
|
-
</
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<h2>Not in the spec yet</h2>
|
|
108
|
-
<ul>
|
|
109
|
+
</Stack>
|
|
110
|
+
)}
|
|
111
|
+
{!!view?.excluded.length && (
|
|
112
|
+
<Stack gap="xs">
|
|
113
|
+
<Title order={3}>Not in the spec yet</Title>
|
|
109
114
|
{view.excluded.map((item) => (
|
|
110
|
-
<
|
|
115
|
+
<Text key={item} c="dimmed">
|
|
116
|
+
{item}
|
|
117
|
+
</Text>
|
|
111
118
|
))}
|
|
112
|
-
</
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
</
|
|
119
|
+
</Stack>
|
|
120
|
+
)}
|
|
121
|
+
</Stack>
|
|
122
|
+
</Box>
|
|
116
123
|
);
|
|
117
124
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Alert, Anchor, Button, Container, Stack, Text, Title } from '@mantine/core';
|
|
2
|
+
import {
|
|
3
|
+
Link,
|
|
4
|
+
createBrowserHistory,
|
|
5
|
+
createMemoryHistory,
|
|
6
|
+
createRootRoute,
|
|
7
|
+
createRoute,
|
|
8
|
+
createRouter,
|
|
9
|
+
lazyRouteComponent,
|
|
10
|
+
type RouterHistory
|
|
11
|
+
} from '@tanstack/react-router';
|
|
2
12
|
import { z } from 'zod/v4';
|
|
3
|
-
|
|
4
13
|
import { SpecPage } from '../evidence/spec-page';
|
|
14
|
+
import { ExplorerSearchFields } from '../explorer/search';
|
|
5
15
|
import { ProductPage } from '../views/ProductPage';
|
|
6
16
|
import { AppShell } from './AppShell';
|
|
7
|
-
import { ExplorerSearchFields } from '../explorer/search';
|
|
8
|
-
|
|
9
17
|
const RootSearch = z.object({ run: z.string().min(1).optional().catch(undefined) });
|
|
10
|
-
|
|
11
18
|
const ChartSearch = RootSearch.extend({
|
|
12
19
|
screen: z.string().min(1).optional().catch(undefined),
|
|
13
20
|
event: z.string().min(1).optional().catch(undefined),
|
|
@@ -15,12 +22,9 @@ const ChartSearch = RootSearch.extend({
|
|
|
15
22
|
group: z.string().min(1).optional().catch(undefined),
|
|
16
23
|
t: z.coerce.number().nonnegative().optional().catch(undefined)
|
|
17
24
|
});
|
|
18
|
-
|
|
19
25
|
export type ChartSearch = z.infer<typeof ChartSearch>;
|
|
20
|
-
|
|
21
26
|
/** The project home: the test explorer's filters, grouping, order, open test and open run (see `explorer/search.ts`). */
|
|
22
27
|
const HomeSearch = RootSearch.extend(ExplorerSearchFields);
|
|
23
|
-
|
|
24
28
|
/** A spec document, the check opened in it, one layer's record of that check, and a recorded moment. */
|
|
25
29
|
const SpecSearch = RootSearch.extend({
|
|
26
30
|
spec: z.string().min(1).optional().catch(undefined),
|
|
@@ -29,14 +33,32 @@ const SpecSearch = RootSearch.extend({
|
|
|
29
33
|
view: z.enum(['recording']).optional().catch(undefined),
|
|
30
34
|
moment: z.coerce.number().int().positive().optional().catch(undefined)
|
|
31
35
|
});
|
|
32
|
-
|
|
33
36
|
export type SpecSearch = z.infer<typeof SpecSearch>;
|
|
34
|
-
|
|
35
37
|
/** A fresh route tree per router, so a test can run its own viewer beside the page's. */
|
|
36
38
|
function routeTree() {
|
|
37
39
|
const rootRoute = createRootRoute({
|
|
38
40
|
validateSearch: (search) => RootSearch.parse(search),
|
|
39
|
-
component: AppShell
|
|
41
|
+
component: AppShell,
|
|
42
|
+
errorComponent: ({ error, reset }) => (
|
|
43
|
+
<Container py="xl">
|
|
44
|
+
<Alert color="red" title="The page couldn’t be loaded">
|
|
45
|
+
<Stack>
|
|
46
|
+
<Text>{error instanceof Error ? error.message : String(error)}</Text>
|
|
47
|
+
<Button onClick={reset}>Try again</Button>
|
|
48
|
+
<Anchor renderRoot={(props) => <Link {...props} to="/" search={{}} />}>Back to reports</Anchor>
|
|
49
|
+
</Stack>
|
|
50
|
+
</Alert>
|
|
51
|
+
</Container>
|
|
52
|
+
),
|
|
53
|
+
notFoundComponent: () => (
|
|
54
|
+
<Container py="xl">
|
|
55
|
+
<Stack>
|
|
56
|
+
<Title order={1}>Page not found</Title>
|
|
57
|
+
<Text c="dimmed">This address is not part of the report viewer.</Text>
|
|
58
|
+
<Anchor renderRoot={(props) => <Link {...props} to="/" search={{}} />}>Back to reports</Anchor>
|
|
59
|
+
</Stack>
|
|
60
|
+
</Container>
|
|
61
|
+
)
|
|
40
62
|
});
|
|
41
63
|
const productRoute = createRoute({
|
|
42
64
|
getParentRoute: () => rootRoute,
|
|
@@ -59,11 +81,9 @@ function routeTree() {
|
|
|
59
81
|
});
|
|
60
82
|
return rootRoute.addChildren([productRoute, chartRoute, specRoute]);
|
|
61
83
|
}
|
|
62
|
-
|
|
63
84
|
export function createAtlasRouter(history: RouterHistory) {
|
|
64
85
|
return createRouter({ routeTree: routeTree(), history, defaultPreload: false, scrollRestoration: true });
|
|
65
86
|
}
|
|
66
|
-
|
|
67
87
|
/**
|
|
68
88
|
* Addresses in the hash, so a built site deep-links without server rewrites. The page's own query (`?project=geviti`)
|
|
69
89
|
* selects the project and is not part of the address: TanStack's hash history appends it to the hash, which turned
|
|
@@ -82,9 +102,7 @@ export function createProjectHashHistory(win: Window = window): RouterHistory {
|
|
|
82
102
|
createHref: (href) => `${win.location.pathname}${win.location.search}#${href}`
|
|
83
103
|
});
|
|
84
104
|
}
|
|
85
|
-
|
|
86
105
|
export const router = createAtlasRouter(createProjectHashHistory());
|
|
87
|
-
|
|
88
106
|
declare module '@tanstack/react-router' {
|
|
89
107
|
interface Register {
|
|
90
108
|
router: typeof router;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return <span className={styles.activity} role="status" aria-label={label} title={label}><span className={styles.ring} aria-hidden="true" /></span>;
|
|
1
|
+
import { Loader } from '@mantine/core';
|
|
2
|
+
/** A named, reduced-motion-aware indicator for work in progress. */
|
|
3
|
+
export function ActivityIndicator({ label }: { label: string }) {
|
|
4
|
+
return <Loader size="xs" aria-label={label} role="status" />;
|
|
6
5
|
}
|
|
@@ -1,70 +1,52 @@
|
|
|
1
1
|
import type { DataIssue } from '@crvouga/atlas/schema';
|
|
2
|
-
import
|
|
2
|
+
import { Alert, Button, Code, Drawer, Stack, Text, Title } from '@mantine/core';
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
|
|
5
4
|
import { useUiStore } from '../state/ui-store';
|
|
6
|
-
import styles from './DataIssues.module.css';
|
|
7
|
-
import { CloseIcon, NoteIcon, WarnIcon } from './icons';
|
|
8
|
-
|
|
9
5
|
const SEVERITY_LABEL = { error: 'Problem', warning: 'Warning', info: 'Note' } as const;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A quiet count of what went wrong while reading the data. It never interrupts; the list opens
|
|
13
|
-
* only when asked for.
|
|
14
|
-
*/
|
|
6
|
+
/** File-by-file diagnostics; unreadable data never appears as a passing result. */
|
|
15
7
|
export function DataIssues({ issues }: { issues: DataIssue[] }) {
|
|
16
8
|
const open = useUiStore((s) => s.issuesOpen);
|
|
17
9
|
const setOpen = useUiStore((s) => s.setIssuesOpen);
|
|
18
10
|
const problems = issues.filter((i) => i.severity !== 'info').length;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
for (const issue of issues)
|
|
22
|
-
return [...
|
|
11
|
+
const groups = useMemo(() => {
|
|
12
|
+
const byFile = new Map<string, DataIssue[]>();
|
|
13
|
+
for (const issue of issues) byFile.set(issue.file, [...(byFile.get(issue.file) ?? []), issue]);
|
|
14
|
+
return [...byFile].sort(([, a], [, b]) => severityRank(b) - severityRank(a));
|
|
23
15
|
}, [issues]);
|
|
24
|
-
if (issues.length
|
|
25
|
-
const label = problems
|
|
16
|
+
if (!issues.length) return null;
|
|
17
|
+
const label = problems
|
|
18
|
+
? `${problems} data ${problems === 1 ? 'problem' : 'problems'}`
|
|
19
|
+
: `${issues.length} data ${issues.length === 1 ? 'note' : 'notes'}`;
|
|
26
20
|
return (
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
{problems ? <WarnIcon size={13} /> : <NoteIcon size={13} />}
|
|
21
|
+
<>
|
|
22
|
+
<Button color={problems ? 'orange' : 'gray'} onClick={() => setOpen(true)}>
|
|
30
23
|
{label}
|
|
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
|
-
</span>
|
|
56
|
-
</li>
|
|
57
|
-
))}
|
|
58
|
-
</ul>
|
|
59
|
-
</section>
|
|
60
|
-
))}
|
|
61
|
-
</div>
|
|
62
|
-
</Dialog.Content>
|
|
63
|
-
</Dialog.Portal>
|
|
64
|
-
</Dialog.Root>
|
|
24
|
+
</Button>
|
|
25
|
+
<Drawer opened={open} onClose={() => setOpen(false)} title="What we couldn’t read">
|
|
26
|
+
<Stack>
|
|
27
|
+
<Text c="dimmed">
|
|
28
|
+
Everything else on the map is shown as usual. These are the parts of the spec and the runs that were skipped or guessed, file by
|
|
29
|
+
file.
|
|
30
|
+
</Text>
|
|
31
|
+
{groups.map(([file, list]) => (
|
|
32
|
+
<Stack key={file} gap="xs">
|
|
33
|
+
<Title order={3}>{file || 'General'}</Title>
|
|
34
|
+
{list.map((issue, i) => (
|
|
35
|
+
<Alert
|
|
36
|
+
key={i}
|
|
37
|
+
color={issue.severity === 'error' ? 'red' : issue.severity === 'warning' ? 'orange' : 'blue'}
|
|
38
|
+
title={SEVERITY_LABEL[issue.severity]}
|
|
39
|
+
>
|
|
40
|
+
{issue.path && <Code>{issue.path}</Code>} {issue.message}
|
|
41
|
+
</Alert>
|
|
42
|
+
))}
|
|
43
|
+
</Stack>
|
|
44
|
+
))}
|
|
45
|
+
</Stack>
|
|
46
|
+
</Drawer>
|
|
47
|
+
</>
|
|
65
48
|
);
|
|
66
49
|
}
|
|
67
|
-
|
|
68
50
|
function severityRank(list: DataIssue[]) {
|
|
69
51
|
return Math.max(...list.map((i) => (i.severity === 'error' ? 2 : i.severity === 'warning' ? 1 : 0)));
|
|
70
52
|
}
|