@crvouga/atlas 0.10.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +7 -0
- package/dist/reports/src/catalog-health.d.ts +142 -0
- package/dist/reports/src/catalog-health.js +257 -0
- package/dist/reports/src/test-catalog.d.ts +8 -0
- package/dist/reports/src/test-catalog.js +5 -1
- package/dist/schema/src/api.d.ts +79 -0
- package/dist/schema/src/api.js +50 -0
- package/dist/server/src/app.js +9 -0
- package/dist/server/src/llms.js +1 -0
- package/dist/server/src/openapi.js +18 -2
- package/dist/server/src/settings-page.d.ts +4 -4
- package/dist/server/src/settings-page.js +17 -78
- package/docs/reference/http-api.md +34 -0
- package/docs/reference/reports.md +224 -0
- package/docs/reference/schema.md +10 -0
- package/docs/reference/server.md +4 -4
- package/docs/server.md +10 -0
- package/docs/ui.md +142 -0
- package/docs/workspaces.md +130 -0
- package/llms-full.txt +322 -0
- package/llms.txt +2 -0
- package/package.json +17 -10
- package/packages/schema/schemas/catalog-health.schema.json +400 -0
- package/packages/server/openapi.json +596 -0
- package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
- package/packages/visualizer/src/admin/main.tsx +11 -0
- package/packages/visualizer/src/app/AppShell.tsx +62 -38
- package/packages/visualizer/src/app/gazetteer.tsx +90 -83
- package/packages/visualizer/src/app/router.tsx +33 -15
- package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
- package/packages/visualizer/src/components/DataIssues.tsx +36 -54
- package/packages/visualizer/src/components/Header.tsx +197 -125
- package/packages/visualizer/src/components/Legend.tsx +13 -41
- package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
- package/packages/visualizer/src/components/Screen.tsx +80 -71
- package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
- package/packages/visualizer/src/components/Status.tsx +20 -34
- package/packages/visualizer/src/components/labels.tsx +8 -3
- package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
- package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
- package/packages/visualizer/src/components/spec-logos/README.md +23 -0
- package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
- package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
- package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
- package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
- package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
- package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
- package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
- package/packages/visualizer/src/evidence/document-page.tsx +51 -29
- package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
- package/packages/visualizer/src/evidence/marks.tsx +112 -79
- package/packages/visualizer/src/evidence/model-page.tsx +77 -46
- package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
- package/packages/visualizer/src/evidence/overview.tsx +137 -69
- package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
- package/packages/visualizer/src/evidence/recording.tsx +135 -68
- package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
- package/packages/visualizer/src/evidence/source-text.tsx +33 -0
- package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
- package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
- package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
- package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
- package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
- package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
- package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
- package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
- package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
- package/packages/visualizer/src/explorer/parts.tsx +35 -13
- package/packages/visualizer/src/find/Finder.tsx +106 -90
- package/packages/visualizer/src/find/open.ts +17 -20
- package/packages/visualizer/src/live/test-progress.tsx +60 -22
- package/packages/visualizer/src/looks/Stage.tsx +223 -110
- package/packages/visualizer/src/main.tsx +9 -11
- package/packages/visualizer/src/map/ChartMap.tsx +125 -69
- package/packages/visualizer/src/map/EventEdge.tsx +59 -37
- package/packages/visualizer/src/map/map.module.css +12 -546
- package/packages/visualizer/src/map/nodes.tsx +124 -70
- package/packages/visualizer/src/reachable.test.ts +3 -2
- package/packages/visualizer/src/spec-kind.ts +2 -2
- package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
- package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
- package/packages/visualizer/src/ui/grammars/README.md +14 -0
- package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
- package/packages/visualizer/src/ui/grammars/quint.json +152 -0
- package/packages/visualizer/src/ui/highlight.ts +50 -0
- package/packages/visualizer/src/ui/theme.tsx +109 -0
- package/packages/visualizer/src/views/ChartPage.tsx +46 -26
- package/packages/visualizer/src/views/ChartTests.tsx +59 -22
- package/packages/visualizer/src/views/ProductPage.tsx +16 -15
- package/packages/visualizer/src/views/SidePanel.tsx +241 -131
- package/packages/visualizer/src/app/gazetteer.module.css +0 -107
- package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
- package/packages/visualizer/src/components/DataIssues.module.css +0 -136
- package/packages/visualizer/src/components/Header.module.css +0 -114
- package/packages/visualizer/src/components/Legend.module.css +0 -84
- package/packages/visualizer/src/components/Screen.module.css +0 -312
- package/packages/visualizer/src/components/Status.module.css +0 -116
- package/packages/visualizer/src/components/icons.tsx +0 -156
- package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
- package/packages/visualizer/src/evidence/evidence.module.css +0 -907
- package/packages/visualizer/src/evidence/recording.module.css +0 -460
- package/packages/visualizer/src/evidence/sources.module.css +0 -168
- package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
- package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
- package/packages/visualizer/src/find/Finder.module.css +0 -168
- package/packages/visualizer/src/looks/stage.module.css +0 -300
- package/packages/visualizer/src/styles/global.css +0 -105
- package/packages/visualizer/src/styles/tokens.css +0 -80
- package/packages/visualizer/src/views/views.module.css +0 -546
|
@@ -1,40 +1,46 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ActionIcon, Alert, Box, Button, Center, Group, Loader, Paper, Stack, Text, useMantineTheme } from '@mantine/core';
|
|
2
|
+
import {
|
|
3
|
+
Background,
|
|
4
|
+
MiniMap,
|
|
5
|
+
Panel,
|
|
6
|
+
ReactFlow,
|
|
7
|
+
ReactFlowProvider,
|
|
8
|
+
useReactFlow,
|
|
9
|
+
type Node,
|
|
10
|
+
type ReactFlowInstance,
|
|
11
|
+
type Viewport
|
|
12
|
+
} from '@xyflow/react';
|
|
2
13
|
import { useCallback, useMemo, useRef } from 'react';
|
|
3
|
-
|
|
4
|
-
import type { AtlasView, ItemStatus } from '../data/model';
|
|
5
|
-
import { FitIcon, MinusIcon, PlusIcon } from '../components/icons';
|
|
14
|
+
import { IconArrowsMaximize as FitIcon, IconMinus as MinusIcon, IconPlus as PlusIcon } from '@tabler/icons-react';
|
|
6
15
|
import { Legend } from '../components/Legend';
|
|
16
|
+
import type { AtlasView, ItemStatus } from '../data/model';
|
|
7
17
|
import { chartGraph } from '../layout/graph';
|
|
8
18
|
import type { ChartLayout } from '../layout/layout';
|
|
9
19
|
import { useChartLayout } from '../layout/use-chart-layout';
|
|
10
20
|
import { useUiStore } from '../state/ui-store';
|
|
11
21
|
import { EdgeMarkers, edgeTypes, type EventEdgeType } from './EventEdge';
|
|
12
|
-
import styles from './map.module.css';
|
|
13
|
-
import { nodeTypes, type ChartLinkNodeType, type ContextNodeType, type GroupNodeType, type ScreenNodeType, type SummaryNodeType } from './nodes';
|
|
14
22
|
|
|
23
|
+
import {
|
|
24
|
+
nodeTypes,
|
|
25
|
+
type ChartLinkNodeType,
|
|
26
|
+
type ContextNodeType,
|
|
27
|
+
type GroupNodeType,
|
|
28
|
+
type ScreenNodeType,
|
|
29
|
+
type SummaryNodeType
|
|
30
|
+
} from './nodes';
|
|
15
31
|
type MapNode = ScreenNodeType | GroupNodeType | ChartLinkNodeType | ContextNodeType | SummaryNodeType;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
passed: '#86efac',
|
|
21
|
-
flaky: '#fdba74',
|
|
22
|
-
failed: '#fca5a5',
|
|
23
|
-
'not-reached': '#e2e8f0',
|
|
24
|
-
'not-yet-run': '#e2e8f0',
|
|
25
|
-
'spec-only': '#ddd6fe'
|
|
32
|
+
export type MapSelection = {
|
|
33
|
+
screen?: string;
|
|
34
|
+
event?: string;
|
|
35
|
+
journey?: string;
|
|
26
36
|
};
|
|
27
|
-
|
|
28
37
|
const START_ZOOM = 0.7;
|
|
29
|
-
|
|
30
38
|
function fitPadding() {
|
|
31
|
-
const narrow = typeof window !== 'undefined' && window.matchMedia('(max-width:
|
|
39
|
+
const narrow = typeof window !== 'undefined' && window.matchMedia('(max-width: 62em)').matches;
|
|
32
40
|
if (narrow) return { top: '180px', right: '64px', bottom: '16px', left: '16px' } as const;
|
|
33
41
|
return { top: '16px', right: '72px', bottom: '120px', left: '16px' } as const;
|
|
34
42
|
}
|
|
35
|
-
|
|
36
43
|
const reducedMotion = () => typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
37
|
-
|
|
38
44
|
function useFlowElements(view: AtlasView, chartId: string, layout: ChartLayout | undefined, selection: MapSelection) {
|
|
39
45
|
return useMemo(() => {
|
|
40
46
|
if (!layout) return { nodes: [] as MapNode[], edges: [] as EventEdgeType[] };
|
|
@@ -61,7 +67,6 @@ function useFlowElements(view: AtlasView, chartId: string, layout: ChartLayout |
|
|
|
61
67
|
const state = view.states.get(id);
|
|
62
68
|
return Boolean(state?.children.some((child) => coversJourney(child)));
|
|
63
69
|
};
|
|
64
|
-
|
|
65
70
|
const nodes: MapNode[] = layout.nodes.flatMap((n): MapNode[] => {
|
|
66
71
|
const state = view.states.get(n.id);
|
|
67
72
|
if (!state) return [];
|
|
@@ -81,9 +86,18 @@ function useFlowElements(view: AtlasView, chartId: string, layout: ChartLayout |
|
|
|
81
86
|
case 'parallel':
|
|
82
87
|
return [{ ...base, type: 'group', zIndex: 0, data: { state, variant: n.kind, dimmed: false } }];
|
|
83
88
|
case 'chart-link':
|
|
84
|
-
return [
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
...base,
|
|
92
|
+
type: 'chartLink',
|
|
93
|
+
zIndex: 2,
|
|
94
|
+
data: { state, chart: state.childChartId ? (view.charts.get(state.childChartId) ?? null) : null, dimmed: dim(n.id) }
|
|
95
|
+
}
|
|
96
|
+
];
|
|
85
97
|
case 'context':
|
|
86
|
-
return [
|
|
98
|
+
return [
|
|
99
|
+
{ ...base, type: 'context', zIndex: 2, data: { state, chart: view.charts.get(state.chartId) ?? null, dimmed: dim(n.id) } }
|
|
100
|
+
];
|
|
87
101
|
case 'summary':
|
|
88
102
|
return [
|
|
89
103
|
{
|
|
@@ -97,7 +111,6 @@ function useFlowElements(view: AtlasView, chartId: string, layout: ChartLayout |
|
|
|
97
111
|
return [{ ...base, type: 'screen', zIndex: 2, data: { state, chartId, selected: selection.screen === n.id, dimmed: dim(n.id) } }];
|
|
98
112
|
}
|
|
99
113
|
});
|
|
100
|
-
|
|
101
114
|
const edges: EventEdgeType[] = layout.edges.flatMap((e): EventEdgeType[] => {
|
|
102
115
|
const transition = e.transitionId ? (view.transitions.get(e.transitionId) ?? null) : null;
|
|
103
116
|
if (!transition && !e.bundle) return [];
|
|
@@ -111,33 +124,55 @@ function useFlowElements(view: AtlasView, chartId: string, layout: ChartLayout |
|
|
|
111
124
|
type: 'event',
|
|
112
125
|
zIndex: 1,
|
|
113
126
|
selectable: false,
|
|
114
|
-
data: {
|
|
127
|
+
data: {
|
|
128
|
+
placed: e,
|
|
129
|
+
transition,
|
|
130
|
+
bundle: e.bundle,
|
|
131
|
+
chartId,
|
|
132
|
+
selected: Boolean(e.transitionId) && selection.event === e.transitionId,
|
|
133
|
+
step,
|
|
134
|
+
dimmed
|
|
135
|
+
}
|
|
115
136
|
}
|
|
116
137
|
];
|
|
117
138
|
});
|
|
118
139
|
return { nodes, edges };
|
|
119
140
|
}, [view, chartId, layout, selection.screen, selection.event, selection.journey]);
|
|
120
141
|
}
|
|
121
|
-
|
|
122
142
|
function ZoomControls() {
|
|
123
143
|
const flow = useReactFlow();
|
|
124
144
|
const duration = reducedMotion() ? 0 : 250;
|
|
125
145
|
return (
|
|
126
|
-
<
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
<Paper shadow="xs" p="xs">
|
|
147
|
+
<Group gap="xs" role="group" aria-label="Zoom">
|
|
148
|
+
<ActionIcon variant="default" onClick={() => void flow.zoomIn({ duration })} aria-label="Zoom in">
|
|
149
|
+
<PlusIcon aria-hidden="true" size={16} />
|
|
150
|
+
</ActionIcon>
|
|
151
|
+
<ActionIcon variant="default" onClick={() => void flow.zoomOut({ duration })} aria-label="Zoom out">
|
|
152
|
+
<MinusIcon aria-hidden="true" size={16} />
|
|
153
|
+
</ActionIcon>
|
|
154
|
+
<ActionIcon variant="default" onClick={() => void flow.fitView({ duration, padding: fitPadding() })} aria-label="Fit the whole map">
|
|
155
|
+
<FitIcon aria-hidden="true" size={16} />
|
|
156
|
+
</ActionIcon>
|
|
157
|
+
</Group>
|
|
158
|
+
</Paper>
|
|
137
159
|
);
|
|
138
160
|
}
|
|
139
161
|
|
|
140
|
-
function Flow({
|
|
162
|
+
function Flow({
|
|
163
|
+
view,
|
|
164
|
+
chartId,
|
|
165
|
+
scopeKey,
|
|
166
|
+
layout,
|
|
167
|
+
selection
|
|
168
|
+
}: {
|
|
169
|
+
view: AtlasView;
|
|
170
|
+
chartId: string;
|
|
171
|
+
scopeKey: string;
|
|
172
|
+
layout: ChartLayout;
|
|
173
|
+
selection: MapSelection;
|
|
174
|
+
}) {
|
|
175
|
+
const theme = useMantineTheme();
|
|
141
176
|
const { nodes, edges } = useFlowElements(view, chartId, layout, selection);
|
|
142
177
|
const saved = useRef<Viewport | undefined>(useUiStore.getState().viewports[scopeKey]);
|
|
143
178
|
const saveViewport = useUiStore((s) => s.saveViewport);
|
|
@@ -155,7 +190,11 @@ function Flow({ view, chartId, scopeKey, layout, selection }: { view: AtlasView;
|
|
|
155
190
|
const focus = layout.nodes.find((n) => n.id === selection.screen);
|
|
156
191
|
if (focus) {
|
|
157
192
|
const zoom = START_ZOOM;
|
|
158
|
-
flow.setViewport({
|
|
193
|
+
flow.setViewport({
|
|
194
|
+
x: width / 2 - (focus.absX + focus.width / 2) * zoom,
|
|
195
|
+
y: height / 2 - (focus.absY + focus.height / 2) * zoom,
|
|
196
|
+
zoom
|
|
197
|
+
});
|
|
159
198
|
return;
|
|
160
199
|
}
|
|
161
200
|
const start = layout.nodes.find((n) => n.id === view.charts.get(chartId)?.initial) ?? layout.nodes[0]!;
|
|
@@ -182,71 +221,88 @@ function Flow({ view, chartId, scopeKey, layout, selection }: { view: AtlasView;
|
|
|
182
221
|
edgesFocusable={false}
|
|
183
222
|
elementsSelectable={false}
|
|
184
223
|
onlyRenderVisibleElements
|
|
185
|
-
className={styles.flow}
|
|
186
224
|
aria-label="Map of screens and events. Drag to move, scroll or pinch to zoom."
|
|
187
225
|
>
|
|
188
226
|
<EdgeMarkers />
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
<
|
|
192
|
-
</
|
|
227
|
+
<Background color={theme.colors.gray[3]} />
|
|
228
|
+
<Panel position="top-right">
|
|
229
|
+
<ZoomControls />
|
|
230
|
+
</Panel>
|
|
231
|
+
<Panel position="bottom-left">
|
|
232
|
+
<Paper p="xs" shadow="xs" maw="70vw">
|
|
233
|
+
<Legend showTime={hasTime} />
|
|
234
|
+
</Paper>
|
|
235
|
+
</Panel>
|
|
193
236
|
<MiniMap<MapNode>
|
|
194
237
|
pannable
|
|
195
238
|
zoomable
|
|
196
239
|
ariaLabel="Overview of the whole map"
|
|
197
|
-
className={styles.minimap}
|
|
198
240
|
nodeColor={(n: Node) => {
|
|
199
241
|
if (n.type === 'group') return 'transparent';
|
|
200
|
-
const data = n.data as {
|
|
201
|
-
|
|
242
|
+
const data = n.data as {
|
|
243
|
+
status?: ItemStatus;
|
|
244
|
+
state?: {
|
|
245
|
+
status: ItemStatus;
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
const status = data.status ?? data.state?.status;
|
|
249
|
+
return theme.colors[status === 'passed' ? 'green' : status === 'failed' ? 'red' : status === 'flaky' ? 'orange' : 'gray'][3];
|
|
202
250
|
}}
|
|
203
|
-
nodeStrokeColor={(n: Node) => (n.type === 'group' ?
|
|
251
|
+
nodeStrokeColor={(n: Node) => (n.type === 'group' ? theme.colors.gray[3] : 'transparent')}
|
|
204
252
|
nodeBorderRadius={8}
|
|
205
|
-
maskColor="
|
|
253
|
+
maskColor="var(--mantine-color-gray-0)"
|
|
206
254
|
/>
|
|
207
255
|
</ReactFlow>
|
|
208
256
|
);
|
|
209
257
|
}
|
|
210
|
-
|
|
211
258
|
function MapSkeleton({ label }: { label: string }) {
|
|
212
259
|
return (
|
|
213
|
-
<
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
</
|
|
260
|
+
<Center h="60vh">
|
|
261
|
+
<Stack align="center">
|
|
262
|
+
<Loader />
|
|
263
|
+
<Text role="status" c="dimmed">
|
|
264
|
+
{label}
|
|
265
|
+
</Text>
|
|
266
|
+
</Stack>
|
|
267
|
+
</Center>
|
|
221
268
|
);
|
|
222
269
|
}
|
|
223
|
-
|
|
224
270
|
/** One chart's map. Positions come from the cached layout, statuses and media from the view. */
|
|
225
|
-
export function ChartMap({
|
|
271
|
+
export function ChartMap({
|
|
272
|
+
view,
|
|
273
|
+
chartId,
|
|
274
|
+
selection,
|
|
275
|
+
group
|
|
276
|
+
}: {
|
|
277
|
+
view: AtlasView;
|
|
278
|
+
chartId: string;
|
|
279
|
+
selection: MapSelection;
|
|
280
|
+
group?: string;
|
|
281
|
+
}) {
|
|
226
282
|
const input = useMemo(() => chartGraph(view, chartId, group ?? null), [view, chartId, group]);
|
|
227
283
|
const chart = view.charts.get(chartId);
|
|
228
284
|
const layout = useChartLayout(input, input?.scoped ? null : (chart?.layout ?? null));
|
|
229
285
|
if (layout.error) {
|
|
230
286
|
return (
|
|
231
|
-
<
|
|
287
|
+
<Alert color="red" title="Map unavailable">
|
|
232
288
|
<p>The map for this chart couldn’t be laid out.</p>
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
</button>
|
|
236
|
-
</div>
|
|
289
|
+
<Button onClick={() => void layout.refetch()}>Try again</Button>
|
|
290
|
+
</Alert>
|
|
237
291
|
);
|
|
238
292
|
}
|
|
239
293
|
if (!layout.data) return <MapSkeleton label={`Laying out ${chart?.name ?? 'the map'}…`} />;
|
|
240
294
|
if (layout.data.nodes.length === 0) {
|
|
241
295
|
return (
|
|
242
|
-
<
|
|
296
|
+
<Alert>
|
|
243
297
|
<p>This chart has no states yet.</p>
|
|
244
|
-
</
|
|
298
|
+
</Alert>
|
|
245
299
|
);
|
|
246
300
|
}
|
|
247
301
|
return (
|
|
248
302
|
<ReactFlowProvider>
|
|
249
|
-
<
|
|
303
|
+
<Box h="70vh" mih="24rem">
|
|
304
|
+
<Flow view={view} chartId={chartId} scopeKey={`${chartId}:${input?.focus ?? ''}`} layout={layout.data} selection={selection} />
|
|
305
|
+
</Box>
|
|
250
306
|
</ReactFlowProvider>
|
|
251
307
|
);
|
|
252
308
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { Badge, Text } from '@mantine/core';
|
|
1
2
|
import { Link } from '@tanstack/react-router';
|
|
2
3
|
import { EdgeLabelRenderer, type Edge, type EdgeProps } from '@xyflow/react';
|
|
3
4
|
import { memo } from 'react';
|
|
4
|
-
|
|
5
|
-
import { STATUS_LABEL, type ItemStatus, type TransitionView } from '../data/model';
|
|
6
5
|
import { KIND_LABEL, KindIcon } from '../components/labels';
|
|
6
|
+
import { STATUS_LABEL, type ItemStatus, type TransitionView } from '../data/model';
|
|
7
7
|
import type { LayoutBundle } from '../layout/graph';
|
|
8
8
|
import type { PlacedEdge } from '../layout/layout';
|
|
9
9
|
import styles from './map.module.css';
|
|
10
10
|
import { roundedPath } from './path';
|
|
11
|
-
|
|
12
11
|
export type EventEdgeData = {
|
|
13
12
|
placed: PlacedEdge;
|
|
14
13
|
transition: TransitionView | null;
|
|
@@ -18,17 +17,13 @@ export type EventEdgeData = {
|
|
|
18
17
|
step: number | null;
|
|
19
18
|
dimmed: boolean;
|
|
20
19
|
};
|
|
21
|
-
|
|
22
20
|
export type EventEdgeType = Edge<EventEdgeData, 'event'>;
|
|
23
|
-
|
|
24
21
|
const UNCHECKED = new Set(['not-reached', 'not-yet-run', 'spec-only']);
|
|
25
|
-
|
|
26
22
|
function markerFor(status: ItemStatus, selected: boolean, step: number | null, kind: string) {
|
|
27
23
|
if (selected || step !== null) return 'active';
|
|
28
24
|
if (UNCHECKED.has(status)) return 'muted';
|
|
29
25
|
return kind;
|
|
30
26
|
}
|
|
31
|
-
|
|
32
27
|
export const EventEdge = memo(function EventEdge({ data, id }: EdgeProps<EventEdgeType>) {
|
|
33
28
|
if (!data) return null;
|
|
34
29
|
const { placed, transition: t, bundle, chartId, selected, step, dimmed } = data;
|
|
@@ -47,17 +42,23 @@ export const EventEdge = memo(function EventEdge({ data, id }: EdgeProps<EventEd
|
|
|
47
42
|
markerEnd={`url(#atlas-arrow-${unchecked ? 'muted' : 'user'})`}
|
|
48
43
|
/>
|
|
49
44
|
<EdgeLabelRenderer>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
<Badge
|
|
46
|
+
renderRoot={(props) => (
|
|
47
|
+
<Link
|
|
48
|
+
{...props}
|
|
49
|
+
to="/chart/$chartId"
|
|
50
|
+
params={{ chartId }}
|
|
51
|
+
search={(prev) => ({
|
|
52
|
+
run: prev.run,
|
|
53
|
+
journey: prev.journey,
|
|
54
|
+
event: undefined,
|
|
55
|
+
screen: bundle.screen ?? undefined,
|
|
56
|
+
group: bundle.group ?? undefined
|
|
57
|
+
})}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
60
|
className={`${styles.chip} nodrag nopan`}
|
|
61
|
+
variant="default"
|
|
61
62
|
data-status={bundle.status}
|
|
62
63
|
data-unchecked={unchecked}
|
|
63
64
|
data-dimmed={dimmed}
|
|
@@ -65,8 +66,10 @@ export const EventEdge = memo(function EventEdge({ data, id }: EdgeProps<EventEd
|
|
|
65
66
|
aria-label={`${bundle.label}. ${STATUS_LABEL[bundle.status]}.`}
|
|
66
67
|
title={bundle.label}
|
|
67
68
|
>
|
|
68
|
-
<span
|
|
69
|
-
|
|
69
|
+
<Text span inherit truncate>
|
|
70
|
+
{bundle.label}
|
|
71
|
+
</Text>
|
|
72
|
+
</Badge>
|
|
70
73
|
</EdgeLabelRenderer>
|
|
71
74
|
</>
|
|
72
75
|
);
|
|
@@ -89,11 +92,17 @@ export const EventEdge = memo(function EventEdge({ data, id }: EdgeProps<EventEd
|
|
|
89
92
|
markerEnd={`url(#atlas-arrow-${markerFor(t.status, selected, step, t.kind)})`}
|
|
90
93
|
/>
|
|
91
94
|
<EdgeLabelRenderer>
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
<Badge
|
|
96
|
+
renderRoot={(props) => (
|
|
97
|
+
<Link
|
|
98
|
+
{...props}
|
|
99
|
+
to="/chart/$chartId"
|
|
100
|
+
params={{ chartId }}
|
|
101
|
+
search={(prev) => ({ run: prev.run, event: t.id, journey: prev.journey })}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
96
104
|
className={`${styles.chip} nodrag nopan`}
|
|
105
|
+
variant="default"
|
|
97
106
|
data-kind={t.kind}
|
|
98
107
|
data-status={t.status}
|
|
99
108
|
data-unchecked={unchecked}
|
|
@@ -104,34 +113,47 @@ export const EventEdge = memo(function EventEdge({ data, id }: EdgeProps<EventEd
|
|
|
104
113
|
aria-label={`${KIND_LABEL[t.kind]}: ${t.event}. ${STATUS_LABEL[t.status]}.${step !== null ? ` Step ${step} of the journey.` : ''}`}
|
|
105
114
|
title={t.event}
|
|
106
115
|
>
|
|
107
|
-
{step !== null &&
|
|
108
|
-
|
|
116
|
+
{step !== null && (
|
|
117
|
+
<Text span inherit fw={700}>
|
|
118
|
+
{step}
|
|
119
|
+
</Text>
|
|
120
|
+
)}
|
|
121
|
+
<Text span inherit>
|
|
109
122
|
<KindIcon kind={t.kind} size={11} />
|
|
110
|
-
</
|
|
111
|
-
<span
|
|
112
|
-
|
|
123
|
+
</Text>
|
|
124
|
+
<Text span inherit truncate>
|
|
125
|
+
{t.event}
|
|
126
|
+
</Text>
|
|
127
|
+
</Badge>
|
|
113
128
|
</EdgeLabelRenderer>
|
|
114
129
|
</>
|
|
115
130
|
);
|
|
116
131
|
});
|
|
117
|
-
|
|
118
132
|
export const edgeTypes = { event: EventEdge };
|
|
119
|
-
|
|
120
133
|
export function EdgeMarkers() {
|
|
121
134
|
const marker = (id: string, color: string) => (
|
|
122
|
-
<marker
|
|
135
|
+
<marker
|
|
136
|
+
key={id}
|
|
137
|
+
id={`atlas-arrow-${id}`}
|
|
138
|
+
viewBox="0 0 10 10"
|
|
139
|
+
refX="8.5"
|
|
140
|
+
refY="5"
|
|
141
|
+
markerWidth="7"
|
|
142
|
+
markerHeight="7"
|
|
143
|
+
orient="auto-start-reverse"
|
|
144
|
+
>
|
|
123
145
|
<path d="M0,0.5 L9.5,5 L0,9.5 z" fill={color} />
|
|
124
146
|
</marker>
|
|
125
147
|
);
|
|
126
148
|
return (
|
|
127
149
|
<svg className={styles.markers} aria-hidden="true">
|
|
128
150
|
<defs>
|
|
129
|
-
{marker('user', 'var(--
|
|
130
|
-
{marker('system', 'var(--
|
|
131
|
-
{marker('time', 'var(--
|
|
132
|
-
{marker('hand-off', 'var(--
|
|
133
|
-
{marker('muted', 'var(--
|
|
134
|
-
{marker('active', 'var(--
|
|
151
|
+
{marker('user', 'var(--mantine-color-blue-7)')}
|
|
152
|
+
{marker('system', 'var(--mantine-color-gray-6)')}
|
|
153
|
+
{marker('time', 'var(--mantine-color-gray-6)')}
|
|
154
|
+
{marker('hand-off', 'var(--mantine-color-dark-5)')}
|
|
155
|
+
{marker('muted', 'var(--mantine-color-gray-4)')}
|
|
156
|
+
{marker('active', 'var(--mantine-color-blue-7)')}
|
|
135
157
|
</defs>
|
|
136
158
|
</svg>
|
|
137
159
|
);
|