@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,3 +1,5 @@
|
|
|
1
|
+
import { Badge, Flex } from '@mantine/core';
|
|
2
|
+
import { statusColor } from '../ui/theme';
|
|
1
3
|
/**
|
|
2
4
|
* @module Explorer parts
|
|
3
5
|
*
|
|
@@ -6,18 +8,19 @@
|
|
|
6
8
|
*/
|
|
7
9
|
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
8
10
|
import type { TestSummary } from '@crvouga/atlas/reports/test-catalog';
|
|
9
|
-
|
|
10
11
|
import { plural } from '../evidence/format';
|
|
11
12
|
import { StatusMark } from '../evidence/marks';
|
|
12
13
|
import { currentProject } from '../runtime';
|
|
13
|
-
import styles from './explorer.module.css';
|
|
14
14
|
import { environmentStatuses } from './groups';
|
|
15
|
-
|
|
16
15
|
/** The project's name for its title: `?project=geviti-app` reads "Geviti App". */
|
|
17
16
|
export function projectTitle(): string {
|
|
18
|
-
return
|
|
17
|
+
return (
|
|
18
|
+
currentProject()
|
|
19
|
+
?.split('-')
|
|
20
|
+
.map((word) => (word[0]?.toUpperCase() ?? '') + word.slice(1))
|
|
21
|
+
.join(' ') || 'Project reports'
|
|
22
|
+
);
|
|
19
23
|
}
|
|
20
|
-
|
|
21
24
|
/** A run key for a person: its CI run number, or `branch@sha` shortened. */
|
|
22
25
|
export function runName(key: string): string {
|
|
23
26
|
const actions = /\/actions\/runs\/(\d+)/.exec(key);
|
|
@@ -26,7 +29,6 @@ export function runName(key: string): string {
|
|
|
26
29
|
const [branch, sha] = key.split('@');
|
|
27
30
|
return [branch, sha?.slice(0, 7)].filter(Boolean).join(' @ ') || key;
|
|
28
31
|
}
|
|
29
|
-
|
|
30
32
|
/** Small badges for what a test's results carry: flaky, video, screenshots, a trace, steps. */
|
|
31
33
|
export function Badges({ test }: { test: TestSummary }) {
|
|
32
34
|
const roles = test.evidence.roles;
|
|
@@ -37,23 +39,43 @@ export function Badges({ test }: { test: TestSummary }) {
|
|
|
37
39
|
roles.includes('trace') ? { id: 'trace', label: 'Trace', title: 'Has a trace' } : null,
|
|
38
40
|
test.evidence.steps > 0 ? { id: 'steps', label: plural(test.evidence.steps, 'step', 'steps'), title: 'Recorded steps' } : null,
|
|
39
41
|
test.live ? { id: 'live', label: 'Running now', title: 'A run is in progress' } : null
|
|
40
|
-
].filter(
|
|
42
|
+
].filter(
|
|
43
|
+
(
|
|
44
|
+
badge
|
|
45
|
+
): badge is {
|
|
46
|
+
id: string;
|
|
47
|
+
label: string;
|
|
48
|
+
title: string;
|
|
49
|
+
} => badge !== null
|
|
50
|
+
);
|
|
41
51
|
if (!badges.length) return null;
|
|
42
|
-
return
|
|
52
|
+
return (
|
|
53
|
+
<Flex component="span" display="inline-flex" gap="xs" align="center" wrap="wrap">
|
|
54
|
+
{badges.map((badge) => (
|
|
55
|
+
<Badge key={badge.id} color={badge.id === 'flaky' ? 'orange' : 'gray'} data-badge={badge.id} title={badge.title}>
|
|
56
|
+
{badge.label}
|
|
57
|
+
</Badge>
|
|
58
|
+
))}
|
|
59
|
+
</Flex>
|
|
60
|
+
);
|
|
43
61
|
}
|
|
44
|
-
|
|
45
62
|
/** Where a test ran and how it went there: one chip per environment. */
|
|
46
63
|
export function EnvironmentChips({ test }: { test: TestSummary }) {
|
|
47
64
|
const environments = environmentStatuses(test);
|
|
48
65
|
if (!environments.length) return null;
|
|
49
66
|
return (
|
|
50
|
-
<span
|
|
67
|
+
<Flex component="span" display="inline-flex" gap="xs" align="center" wrap="wrap">
|
|
51
68
|
{environments.map(({ environment, status }) => (
|
|
52
|
-
<
|
|
69
|
+
<Badge
|
|
70
|
+
key={environment ?? 'none'}
|
|
71
|
+
color={statusColor(status)}
|
|
72
|
+
data-status={status}
|
|
73
|
+
title={`${environmentLabel(environment)}: ${status}`}
|
|
74
|
+
>
|
|
53
75
|
<StatusMark status={status} size={10} />
|
|
54
76
|
{environmentLabel(environment)}
|
|
55
|
-
</
|
|
77
|
+
</Badge>
|
|
56
78
|
))}
|
|
57
|
-
</
|
|
79
|
+
</Flex>
|
|
58
80
|
);
|
|
59
81
|
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Badge, Group, Kbd, List, Modal, Text } from '@mantine/core';
|
|
2
|
+
import { Spotlight, useSpotlight } from '@mantine/spotlight';
|
|
2
3
|
import { useQueryClient } from '@tanstack/react-query';
|
|
3
4
|
import { useNavigate, useSearch } from '@tanstack/react-router';
|
|
4
5
|
import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
5
|
-
|
|
6
6
|
import { useAtlas } from '../app/atlas-context';
|
|
7
7
|
import { keys, specKey, useAdapter } from '../data/queries';
|
|
8
|
-
import { StatusMark } from '../evidence/marks';
|
|
9
8
|
import { useEvidence } from '../evidence/evidence-context';
|
|
9
|
+
import { StatusMark } from '../evidence/marks';
|
|
10
10
|
import { kindOfFile } from '../spec-kind';
|
|
11
11
|
import { corpusFrom } from './corpus';
|
|
12
|
-
import {
|
|
13
|
-
import { FIND_GROUPS, indexCorpus, notationOf, rankIndex, splitQuery, type FindDoc, type FindKind, type FindTarget
|
|
12
|
+
import { finderStore, isHelpOpen, setFinderOpen, setHelpOpen, toggleFinder, useFinderOpen, useHelpOpen } from './open';
|
|
13
|
+
import { FIND_GROUPS, indexCorpus, notationOf, rankIndex, splitQuery, type FindDoc, type FindKind, type FindTarget } from './rank';
|
|
14
14
|
import { applySemantic, createWorkerEmbedder, embeddingsAvailable, embedText, textHash, type Embedder } from './semantic';
|
|
15
|
-
import styles from './Finder.module.css';
|
|
16
15
|
|
|
17
16
|
const KIND_WORD: Record<FindKind, string> = {
|
|
18
17
|
journey: 'Journey',
|
|
@@ -23,8 +22,10 @@ const KIND_WORD: Record<FindKind, string> = {
|
|
|
23
22
|
action: 'Action',
|
|
24
23
|
run: 'Run'
|
|
25
24
|
};
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const SHORTCUTS: readonly {
|
|
26
|
+
readonly keys: string;
|
|
27
|
+
readonly does: string;
|
|
28
|
+
}[] = [
|
|
28
29
|
{ keys: '⌘K or Ctrl+K', does: 'Find a flow' },
|
|
29
30
|
{ keys: '/', does: 'Find a flow' },
|
|
30
31
|
{ keys: '↑ ↓', does: 'Move through the list' },
|
|
@@ -35,13 +36,11 @@ const SHORTCUTS: readonly { readonly keys: string; readonly does: string }[] = [
|
|
|
35
36
|
{ keys: '← →', does: 'Previous and next moment' },
|
|
36
37
|
{ keys: 'Home End', does: 'Start and end of a recording' }
|
|
37
38
|
];
|
|
38
|
-
|
|
39
39
|
function isTypingTarget(target: EventTarget | null) {
|
|
40
40
|
if (!(target instanceof HTMLElement)) return false;
|
|
41
41
|
const tag = target.tagName;
|
|
42
42
|
return target.isContentEditable || tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'BUTTON' || tag === 'A';
|
|
43
43
|
}
|
|
44
|
-
|
|
45
44
|
function useSpecTexts(files: readonly string[]) {
|
|
46
45
|
const adapter = useAdapter();
|
|
47
46
|
const client = useQueryClient();
|
|
@@ -84,10 +83,8 @@ function useSpecTexts(files: readonly string[]) {
|
|
|
84
83
|
}, [adapter, client, key, source]);
|
|
85
84
|
return { texts, pending };
|
|
86
85
|
}
|
|
87
|
-
|
|
88
86
|
/** The vector cache outlives a keystroke, so a live run does not embed the suite again. */
|
|
89
87
|
const vectorCache = new Map<string, Float32Array>();
|
|
90
|
-
|
|
91
88
|
function vectorsFor(docs: readonly FindDoc[]): ReadonlyMap<string, Float32Array> {
|
|
92
89
|
const vectors = new Map<string, Float32Array>();
|
|
93
90
|
for (const doc of docs) {
|
|
@@ -96,7 +93,6 @@ function vectorsFor(docs: readonly FindDoc[]): ReadonlyMap<string, Float32Array>
|
|
|
96
93
|
}
|
|
97
94
|
return vectors;
|
|
98
95
|
}
|
|
99
|
-
|
|
100
96
|
export function Finder() {
|
|
101
97
|
const open = useFinderOpen();
|
|
102
98
|
const help = useHelpOpen();
|
|
@@ -106,12 +102,20 @@ export function Finder() {
|
|
|
106
102
|
const routeSearch = useSearch({ strict: false });
|
|
107
103
|
const readable = useMemo(() => evidence.files.filter((file) => kindOfFile(file)), [evidence.files]);
|
|
108
104
|
const { texts, pending } = useSpecTexts(readable);
|
|
109
|
-
const docs = useMemo(
|
|
105
|
+
const docs = useMemo(
|
|
106
|
+
() => corpusFrom(view, evidence.files, evidence.titleOf, evidence.view, texts),
|
|
107
|
+
[view, evidence.files, evidence.titleOf, evidence.view, texts]
|
|
108
|
+
);
|
|
110
109
|
const index = useMemo(() => indexCorpus(docs), [docs]);
|
|
111
110
|
const [query, setQuery] = useState('');
|
|
112
|
-
const
|
|
111
|
+
const store = finderStore;
|
|
112
|
+
const spotlightState = useSpotlight(store);
|
|
113
|
+
|
|
113
114
|
const lexical = useMemo(() => rankIndex(index, query), [index, query]);
|
|
114
|
-
const [embedded, setEmbedded] = useState<{
|
|
115
|
+
const [embedded, setEmbedded] = useState<{
|
|
116
|
+
readonly query: string;
|
|
117
|
+
readonly vector: Float32Array;
|
|
118
|
+
} | null>(null);
|
|
115
119
|
const [vectors, setVectors] = useState<ReadonlyMap<string, Float32Array>>(() => new Map());
|
|
116
120
|
const queryVector = embedded !== null && embedded.query === query.trim() ? embedded.vector : null;
|
|
117
121
|
const searchable = useMemo(() => {
|
|
@@ -119,18 +123,23 @@ export function Finder() {
|
|
|
119
123
|
if (notations.size === 0) return docs;
|
|
120
124
|
return docs.filter((doc) => notations.has(notationOf(doc)));
|
|
121
125
|
}, [docs, query]);
|
|
122
|
-
const shown =
|
|
126
|
+
const shown = useMemo(
|
|
127
|
+
() => (queryVector ? applySemantic(searchable, lexical, queryVector, vectors) : lexical),
|
|
128
|
+
[queryVector, searchable, lexical, vectors]
|
|
129
|
+
);
|
|
123
130
|
const ordered = useMemo(() => FIND_GROUPS.flatMap((group) => shown.filter((hit) => hit.doc.group === group)), [shown]);
|
|
124
|
-
|
|
131
|
+
|
|
132
|
+
// Rank results ourselves, then let Spotlight own keyboard selection and scrolling.
|
|
133
|
+
// Compound Spotlight does not initialize a selection when asynchronous results arrive.
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (open) store.setState((state) => ({ ...state, selected: 0 }));
|
|
136
|
+
}, [open, query, store]);
|
|
125
137
|
const embedder = useRef<Embedder | null>(null);
|
|
126
138
|
const embedFailed = useRef(false);
|
|
127
|
-
|
|
128
139
|
useEffect(() => {
|
|
129
140
|
if (open) return;
|
|
130
141
|
setQuery('');
|
|
131
|
-
setPicked('');
|
|
132
142
|
}, [open]);
|
|
133
|
-
|
|
134
143
|
useEffect(() => {
|
|
135
144
|
const onKey = (event: KeyboardEvent) => {
|
|
136
145
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k' && !event.altKey && !event.shiftKey) {
|
|
@@ -157,7 +166,6 @@ export function Finder() {
|
|
|
157
166
|
window.addEventListener('keydown', onKey);
|
|
158
167
|
return () => window.removeEventListener('keydown', onKey);
|
|
159
168
|
}, []);
|
|
160
|
-
|
|
161
169
|
useEffect(() => {
|
|
162
170
|
if (!open || embedFailed.current || !embeddingsAvailable() || query.trim().length === 0) return;
|
|
163
171
|
let cancelled = false;
|
|
@@ -192,7 +200,6 @@ export function Finder() {
|
|
|
192
200
|
cancelled = true;
|
|
193
201
|
};
|
|
194
202
|
}, [open, query, docs, lexical]);
|
|
195
|
-
|
|
196
203
|
const openTarget = (target: FindTarget) => {
|
|
197
204
|
const run = typeof routeSearch.run === 'string' ? routeSearch.run : undefined;
|
|
198
205
|
if (target.to === '/chart/$chartId' && target.chartId) {
|
|
@@ -214,86 +221,95 @@ export function Finder() {
|
|
|
214
221
|
}
|
|
215
222
|
setFinderOpen(false);
|
|
216
223
|
};
|
|
217
|
-
|
|
218
224
|
const onCommandKey = (event: ReactKeyboardEvent) => {
|
|
219
225
|
if (event.key !== 'Enter' || (!event.metaKey && !event.ctrlKey)) return;
|
|
220
226
|
event.preventDefault();
|
|
221
|
-
|
|
227
|
+
event.stopPropagation();
|
|
228
|
+
const hit = ordered[Math.max(0, store.getState().selected)];
|
|
222
229
|
if (hit) openTarget(hit.doc.recording ?? hit.doc.target);
|
|
223
230
|
};
|
|
224
|
-
|
|
225
231
|
return (
|
|
226
232
|
<>
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
value={selected}
|
|
238
|
-
onValueChange={setPicked}
|
|
239
|
-
onKeyDown={onCommandKey}
|
|
233
|
+
<Spotlight.Root
|
|
234
|
+
store={store}
|
|
235
|
+
shortcut={null}
|
|
236
|
+
query={query}
|
|
237
|
+
onQueryChange={setQuery}
|
|
238
|
+
title="Find"
|
|
239
|
+
attributes={{ content: { 'aria-label': 'Find' } }}
|
|
240
|
+
onKeyDownCapture={onCommandKey}
|
|
241
|
+
scrollable
|
|
242
|
+
maxHeight="60vh"
|
|
240
243
|
>
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
+
{/* Spotlight updates selection silently; notify its public store after keyboard handling so ARIA follows it. */}
|
|
245
|
+
<Spotlight.Search
|
|
246
|
+
onKeyDown={() => queueMicrotask(() => store.setState(store.getState()))}
|
|
247
|
+
role="combobox"
|
|
248
|
+
aria-expanded={open}
|
|
249
|
+
aria-controls="atlas-finder-actions"
|
|
250
|
+
aria-activedescendant={ordered[Math.max(0, spotlightState.selected)]?.doc.id}
|
|
251
|
+
aria-label="Find a flow"
|
|
252
|
+
placeholder='Try "add a todo" or "gherkin"'
|
|
253
|
+
/>
|
|
254
|
+
<Spotlight.ActionsList id="atlas-finder-actions" role="listbox" aria-label="Flows, specs and runs">
|
|
255
|
+
{shown.length === 0 && (
|
|
256
|
+
<Spotlight.Empty>
|
|
257
|
+
{query.trim() ? 'Nothing close to that.' : 'Type what someone does. For example, add a todo.'}
|
|
258
|
+
</Spotlight.Empty>
|
|
259
|
+
)}
|
|
244
260
|
{FIND_GROUPS.map((group) => {
|
|
245
261
|
const hits = shown.filter((hit) => hit.doc.group === group);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<Command.Group key={group} heading={group} value={group}>
|
|
262
|
+
return hits.length ? (
|
|
263
|
+
<Spotlight.ActionsGroup key={group} label={group}>
|
|
249
264
|
{hits.map((hit) => (
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
265
|
+
<Spotlight.Action
|
|
266
|
+
key={hit.doc.id}
|
|
267
|
+
id={hit.doc.id}
|
|
268
|
+
ref={(node) => {
|
|
269
|
+
if (node) node.id = hit.doc.id;
|
|
270
|
+
}}
|
|
271
|
+
role="option"
|
|
272
|
+
data-selected={ordered[Math.max(0, spotlightState.selected)]?.doc.id === hit.doc.id || undefined}
|
|
273
|
+
aria-selected={ordered[Math.max(0, spotlightState.selected)]?.doc.id === hit.doc.id}
|
|
274
|
+
onClick={() => openTarget(hit.doc.target)}
|
|
275
|
+
label={hit.doc.title}
|
|
276
|
+
description={[hit.snippet !== hit.doc.subtitle ? hit.snippet : '', hit.doc.subtitle].filter(Boolean).join(' · ')}
|
|
277
|
+
leftSection={hit.doc.status ? <StatusMark status={hit.doc.status} size={16} /> : undefined}
|
|
278
|
+
rightSection={<Badge color="gray">{KIND_WORD[hit.doc.kind]}</Badge>}
|
|
279
|
+
/>
|
|
253
280
|
))}
|
|
254
|
-
</
|
|
255
|
-
);
|
|
281
|
+
</Spotlight.ActionsGroup>
|
|
282
|
+
) : null;
|
|
256
283
|
})}
|
|
257
|
-
</
|
|
258
|
-
<
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return (
|
|
287
|
-
<>
|
|
288
|
-
{doc.status && <StatusMark status={doc.status} size={14} />}
|
|
289
|
-
<span className={styles.text}>
|
|
290
|
-
<span className={styles.title}>{doc.title}</span>
|
|
291
|
-
{snippet && snippet !== doc.subtitle && <span className={styles.snippet}>{snippet}</span>}
|
|
292
|
-
</span>
|
|
293
|
-
<span className={styles.meta}>
|
|
294
|
-
{doc.subtitle && <span className={styles.subtitle}>{doc.subtitle}</span>}
|
|
295
|
-
<span className={styles.kind}>{KIND_WORD[doc.kind]}</span>
|
|
296
|
-
</span>
|
|
284
|
+
</Spotlight.ActionsList>
|
|
285
|
+
<Spotlight.Footer>
|
|
286
|
+
<Group gap="md" justify="space-between">
|
|
287
|
+
<Text size="xs" c="dimmed">
|
|
288
|
+
{pending ? 'Reading specs…' : '↑↓ move'}
|
|
289
|
+
</Text>
|
|
290
|
+
<Text size="xs" c="dimmed">
|
|
291
|
+
↵ open · esc close
|
|
292
|
+
</Text>
|
|
293
|
+
</Group>
|
|
294
|
+
</Spotlight.Footer>
|
|
295
|
+
</Spotlight.Root>
|
|
296
|
+
<Modal
|
|
297
|
+
opened={help}
|
|
298
|
+
onClose={() => setHelpOpen(false)}
|
|
299
|
+
title="Keyboard shortcuts"
|
|
300
|
+
attributes={{ content: { 'aria-label': 'Keyboard shortcuts' } }}
|
|
301
|
+
>
|
|
302
|
+
<List spacing="md" listStyleType="none">
|
|
303
|
+
{SHORTCUTS.map((shortcut) => (
|
|
304
|
+
<List.Item key={shortcut.keys}>
|
|
305
|
+
<Group justify="space-between">
|
|
306
|
+
<Kbd>{shortcut.keys}</Kbd>
|
|
307
|
+
<Text>{shortcut.does}</Text>
|
|
308
|
+
</Group>
|
|
309
|
+
</List.Item>
|
|
310
|
+
))}
|
|
311
|
+
</List>
|
|
312
|
+
</Modal>
|
|
297
313
|
</>
|
|
298
314
|
);
|
|
299
315
|
}
|
|
@@ -1,55 +1,52 @@
|
|
|
1
|
+
import { createSpotlight } from '@mantine/spotlight';
|
|
1
2
|
import { useSyncExternalStore } from 'react';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/** The application's finder and Mantine Spotlight share one store, including under React StrictMode. */
|
|
5
|
+
export const [finderStore, finderControls] = createSpotlight();
|
|
4
6
|
let help = false;
|
|
5
7
|
const listeners = new Set<() => void>();
|
|
6
|
-
|
|
7
8
|
function emit() {
|
|
8
9
|
for (const listener of listeners) listener();
|
|
9
10
|
}
|
|
10
|
-
|
|
11
11
|
export function subscribeFinder(listener: () => void) {
|
|
12
12
|
listeners.add(listener);
|
|
13
|
-
|
|
13
|
+
const stop = finderStore.subscribe(listener);
|
|
14
|
+
return () => {
|
|
15
|
+
listeners.delete(listener);
|
|
16
|
+
stop();
|
|
17
|
+
};
|
|
14
18
|
}
|
|
15
|
-
|
|
16
19
|
export function isFinderOpen() {
|
|
17
|
-
return
|
|
20
|
+
return finderStore.getState().opened;
|
|
18
21
|
}
|
|
19
|
-
|
|
20
22
|
export function isHelpOpen() {
|
|
21
23
|
return help;
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
/** Recording keys stay put while either layer is up. */
|
|
25
|
+
/** Playback shortcuts are suspended whenever a finder or help overlay is open. */
|
|
25
26
|
export function finderBlocksKeys() {
|
|
26
|
-
return
|
|
27
|
+
return isFinderOpen() || help;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
29
|
export function setFinderOpen(value: boolean) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if (value) {
|
|
31
|
+
help = false;
|
|
32
|
+
finderControls.open();
|
|
33
|
+
} else finderControls.close();
|
|
32
34
|
emit();
|
|
33
35
|
}
|
|
34
|
-
|
|
35
36
|
export function toggleFinder() {
|
|
36
|
-
setFinderOpen(!
|
|
37
|
+
setFinderOpen(!isFinderOpen());
|
|
37
38
|
}
|
|
38
|
-
|
|
39
39
|
export function setHelpOpen(value: boolean) {
|
|
40
40
|
help = value;
|
|
41
|
-
if (value)
|
|
41
|
+
if (value) finderControls.close();
|
|
42
42
|
emit();
|
|
43
43
|
}
|
|
44
|
-
|
|
45
44
|
export function useFinderOpen() {
|
|
46
45
|
return useSyncExternalStore(subscribeFinder, isFinderOpen, () => false);
|
|
47
46
|
}
|
|
48
|
-
|
|
49
47
|
export function useHelpOpen() {
|
|
50
48
|
return useSyncExternalStore(subscribeFinder, isHelpOpen, () => false);
|
|
51
49
|
}
|
|
52
|
-
|
|
53
50
|
export function commandKey() {
|
|
54
51
|
return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘K' : 'Ctrl+K';
|
|
55
52
|
}
|
|
@@ -1,34 +1,72 @@
|
|
|
1
1
|
import type { CheckSlot } from '@crvouga/atlas/reports/evidence-view';
|
|
2
|
+
import { Alert, Code, Flex, List, Stack, Text } from '@mantine/core';
|
|
2
3
|
import { StatusMark } from '../evidence/marks';
|
|
3
|
-
import styles from '../evidence/evidence.module.css';
|
|
4
4
|
import { useLiveTests, type LiveTest } from './live-runs';
|
|
5
5
|
import { testsForSlot } from './report-progress';
|
|
6
|
-
|
|
7
6
|
export function useTestProgress(slot: CheckSlot | null) {
|
|
8
7
|
const { index, error } = useLiveTests();
|
|
9
8
|
return { tests: slot ? testsForSlot(index, slot) : [], error };
|
|
10
9
|
}
|
|
11
|
-
|
|
12
10
|
/** A test's own progress log. Historical evidence stays available below it. */
|
|
13
11
|
export function TestProgress({ tests, error }: { tests: readonly LiveTest[]; error?: string | null }) {
|
|
14
12
|
if (!tests.length) return null;
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
13
|
+
return (
|
|
14
|
+
<Stack aria-label="Test progress" gap="sm">
|
|
15
|
+
{tests.map(({ run, check }) => {
|
|
16
|
+
const running = run.status === 'running' && check.status === 'running';
|
|
17
|
+
const interrupted = run.status === 'interrupted' && check.status === 'not-run';
|
|
18
|
+
const status = running ? 'running' : interrupted ? 'not-run' : check.status === 'running' ? 'not-run' : check.status;
|
|
19
|
+
return (
|
|
20
|
+
<Stack key={`${run.id}:${check.id}`} gap="sm">
|
|
21
|
+
<Flex role="status" gap="xs" align="center" wrap="wrap" c="dimmed">
|
|
22
|
+
<StatusMark status={status} />
|
|
23
|
+
<Text span inherit fw={600}>
|
|
24
|
+
{running
|
|
25
|
+
? 'Running'
|
|
26
|
+
: interrupted
|
|
27
|
+
? 'Interrupted'
|
|
28
|
+
: status === 'passed'
|
|
29
|
+
? 'Passed'
|
|
30
|
+
: status === 'failed'
|
|
31
|
+
? 'Failed'
|
|
32
|
+
: 'Not run'}
|
|
33
|
+
</Text>
|
|
34
|
+
<Text span inherit>
|
|
35
|
+
{run.producer}
|
|
36
|
+
{run.environment ? ` · ${run.environment}` : ''}
|
|
37
|
+
</Text>
|
|
38
|
+
</Flex>
|
|
39
|
+
{check.detail && check.detail !== run.reason && <Code block>{check.detail}</Code>}
|
|
40
|
+
{!!check.steps?.length && (
|
|
41
|
+
<List aria-label="Progress log" spacing="xs" listStyleType="none" type="ordered">
|
|
42
|
+
{check.steps.map((step, index) => (
|
|
43
|
+
<List.Item key={index}>
|
|
44
|
+
<Flex gap="xs" align="center" wrap="wrap">
|
|
45
|
+
<StatusMark
|
|
46
|
+
status={step.status === 'passed' || step.status === 'failed' || step.status === 'running' ? step.status : 'none'}
|
|
47
|
+
size={14}
|
|
48
|
+
/>
|
|
49
|
+
<Text span inherit>
|
|
50
|
+
{step.name}
|
|
51
|
+
</Text>
|
|
52
|
+
</Flex>
|
|
53
|
+
</List.Item>
|
|
54
|
+
))}
|
|
55
|
+
</List>
|
|
56
|
+
)}
|
|
57
|
+
{interrupted && (
|
|
58
|
+
<Alert role="status" color={'blue'} my="sm">
|
|
59
|
+
{run.reason ?? 'The test run was interrupted.'} No completed outcome was received for this test.
|
|
60
|
+
</Alert>
|
|
61
|
+
)}
|
|
62
|
+
</Stack>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
{error && (
|
|
66
|
+
<Alert role="alert" color={'red'} my="sm">
|
|
67
|
+
{error}
|
|
68
|
+
</Alert>
|
|
69
|
+
)}
|
|
70
|
+
</Stack>
|
|
71
|
+
);
|
|
34
72
|
}
|