@anlyx/ui 0.1.2 → 0.1.5
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/README.md +3 -2
- package/dist/capture/capture-runtime.d.ts +14 -0
- package/dist/capture/capture-runtime.js +300 -0
- package/dist/components/AnalysisEvidenceList.d.ts +5 -0
- package/dist/components/AnalysisEvidenceList.js +61 -0
- package/dist/components/AnlyxAppShell.d.ts +1 -1
- package/dist/components/AnlyxAppShell.js +16 -7
- package/dist/components/ApiCallList.d.ts +3 -2
- package/dist/components/ApiCallList.js +12 -2
- package/dist/components/CaptureStatusEmptyState.js +2 -2
- package/dist/components/EndpointMapCanvas.js +1 -1
- package/dist/components/FlowStoryView.d.ts +22 -0
- package/dist/components/FlowStoryView.js +117 -0
- package/dist/components/InspectorPanel.d.ts +1 -1
- package/dist/components/InspectorPanel.js +46 -1
- package/dist/components/PageStoryboardView.js +9 -1
- package/dist/components/ProcessFlowView.js +8 -1
- package/dist/components/ReplayControls.d.ts +2 -1
- package/dist/components/ReplayControls.js +29 -2
- package/dist/components/Sidebar.d.ts +2 -2
- package/dist/components/Sidebar.js +15 -3
- package/dist/components/StatusBadge.d.ts +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mock-data.js +50 -4
- package/dist/overlay/AnlyxFlowEdge.d.ts +2 -0
- package/dist/overlay/AnlyxFlowEdge.js +15 -0
- package/dist/overlay/AnlyxFlowNode.d.ts +13 -0
- package/dist/overlay/AnlyxFlowNode.js +28 -0
- package/dist/overlay/FlowDrawer.d.ts +2 -0
- package/dist/overlay/FlowDrawer.js +59 -0
- package/dist/overlay/MainFlowCanvas.d.ts +20 -0
- package/dist/overlay/MainFlowCanvas.js +285 -0
- package/dist/overlay/RecentApiEventsTable.d.ts +5 -0
- package/dist/overlay/RecentApiEventsTable.js +19 -0
- package/dist/overlay/overlay-entry.d.ts +8 -0
- package/dist/overlay/overlay-entry.js +14 -0
- package/dist/overlay/overlay-ui.css +2 -0
- package/dist/overlay/overlay-ui.js +14 -0
- package/dist/overlay/types.d.ts +38 -0
- package/dist/overlay/types.js +1 -0
- package/dist/overlay/ui.d.ts +18 -0
- package/dist/overlay/ui.js +13 -0
- package/dist/readme-demo/ReadmeDemoApp.d.ts +15 -0
- package/dist/readme-demo/ReadmeDemoApp.js +184 -0
- package/dist/readme-demo/readme-demo-entry.d.ts +1 -0
- package/dist/readme-demo/readme-demo-entry.js +8 -0
- package/dist/styles.css +1165 -38
- package/dist/viewer/ViewerApp.js +26 -16
- package/dist/viewer/styles.css +2639 -0
- package/dist/viewer/viewer-entry.d.ts +1 -0
- package/dist/viewer/viewer-entry.js +1 -0
- package/dist/viewer/workspace/anlyx-logo-transparent.png +0 -0
- package/dist/viewer/workspace/workspace.css +6354 -0
- package/dist/workspace/ScanTreeMap.d.ts +6 -0
- package/dist/workspace/ScanTreeMap.js +838 -0
- package/dist/workspace/WorkspaceApp.d.ts +8 -0
- package/dist/workspace/WorkspaceApp.js +2293 -0
- package/dist/workspace/project-view-model.d.ts +63 -0
- package/dist/workspace/project-view-model.js +170 -0
- package/dist/workspace/workspace.css +6354 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# @anlyx/ui
|
|
2
2
|
|
|
3
|
-
React UI components and the local viewer shell for Anlyx.
|
|
3
|
+
React UI components and the local Pages / Map / JSON viewer shell for Anlyx.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The UI renders validated Project JSON. It must not invent real project pages,
|
|
6
|
+
requests, architecture, or timing data.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CaptureRuntimeOptions = {
|
|
2
|
+
runtimeBaseUrl?: string;
|
|
3
|
+
ingestPath?: string;
|
|
4
|
+
actionWindowMs?: number;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
};
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
__ANLYX_CAPTURE_INSTALLED__?: boolean;
|
|
11
|
+
__ANLYX_RUNTIME_BASE_URL__?: string;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export declare function installAnlyxCaptureRuntime(options?: CaptureRuntimeOptions): () => void;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
const DEFAULT_RUNTIME_BASE_URL = "http://localhost:4777";
|
|
2
|
+
const DEFAULT_INGEST_PATH = "/_anlyx/events";
|
|
3
|
+
const DEFAULT_ACTION_WINDOW_MS = 2_000;
|
|
4
|
+
let eventSequence = 0;
|
|
5
|
+
export function installAnlyxCaptureRuntime(options = {}) {
|
|
6
|
+
const runtimeBaseUrl = options.runtimeBaseUrl ?? window.__ANLYX_RUNTIME_BASE_URL__ ?? DEFAULT_RUNTIME_BASE_URL;
|
|
7
|
+
const ingestPath = options.ingestPath ?? DEFAULT_INGEST_PATH;
|
|
8
|
+
const actionWindowMs = options.actionWindowMs ?? DEFAULT_ACTION_WINDOW_MS;
|
|
9
|
+
const now = options.now ?? Date.now;
|
|
10
|
+
const ingestUrl = new URL(ingestPath, runtimeBaseUrl).toString();
|
|
11
|
+
const originalFetch = options.fetchImpl ?? window.fetch;
|
|
12
|
+
const originalWindowFetch = window.fetch;
|
|
13
|
+
const originalOpen = window.XMLHttpRequest?.prototype.open;
|
|
14
|
+
const originalSend = window.XMLHttpRequest?.prototype.send;
|
|
15
|
+
const originalPushState = window.history.pushState;
|
|
16
|
+
const originalReplaceState = window.history.replaceState;
|
|
17
|
+
const xhrMetadata = new WeakMap();
|
|
18
|
+
let latestAction;
|
|
19
|
+
let latestPagePath = "";
|
|
20
|
+
function observeClick(event) {
|
|
21
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
22
|
+
if (!target) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const text = readableText(target);
|
|
26
|
+
latestAction = {
|
|
27
|
+
label: readableLabel(target, text),
|
|
28
|
+
selector: selectorFor(target),
|
|
29
|
+
observedAt: observedAt(now),
|
|
30
|
+
capturedAt: now()
|
|
31
|
+
};
|
|
32
|
+
if (text) {
|
|
33
|
+
latestAction.text = text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function postEvent(event) {
|
|
37
|
+
if (!originalFetch) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
void originalFetch(ingestUrl, {
|
|
41
|
+
body: JSON.stringify(event),
|
|
42
|
+
headers: { "content-type": "application/json" },
|
|
43
|
+
keepalive: true,
|
|
44
|
+
method: "POST"
|
|
45
|
+
}).catch(() => { });
|
|
46
|
+
}
|
|
47
|
+
function observePageView() {
|
|
48
|
+
const normalized = normalizeUrl(window.location.href);
|
|
49
|
+
if (shouldIgnorePagePath(normalized.path) || normalized.path === latestPagePath) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
latestPagePath = normalized.path;
|
|
53
|
+
postEvent({
|
|
54
|
+
id: `page-view:${now()}:${++eventSequence}`,
|
|
55
|
+
type: "page_view",
|
|
56
|
+
url: normalized.href,
|
|
57
|
+
path: normalized.path,
|
|
58
|
+
title: document.title,
|
|
59
|
+
observedAt: observedAt(now)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function buildEvent(metadata, status) {
|
|
63
|
+
const normalized = normalizeUrl(metadata.url);
|
|
64
|
+
if (shouldIgnoreUrl(normalized, ingestUrl, ingestPath)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const event = {
|
|
68
|
+
id: metadata.id,
|
|
69
|
+
type: "request",
|
|
70
|
+
method: metadata.method.toUpperCase(),
|
|
71
|
+
url: normalized.href,
|
|
72
|
+
path: normalized.path,
|
|
73
|
+
durationMs: Math.max(0, now() - metadata.startedAt),
|
|
74
|
+
observedAt: observedAt(now)
|
|
75
|
+
};
|
|
76
|
+
if (status !== undefined) {
|
|
77
|
+
event.status = status;
|
|
78
|
+
}
|
|
79
|
+
const action = consumeFreshAction(metadata.startedAt, normalized);
|
|
80
|
+
if (action) {
|
|
81
|
+
event.action = action;
|
|
82
|
+
}
|
|
83
|
+
return event;
|
|
84
|
+
}
|
|
85
|
+
function consumeFreshAction(startedAt, normalized) {
|
|
86
|
+
if (!latestAction) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (!isActionRequestTarget(normalized)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const { capturedAt, ...action } = latestAction;
|
|
93
|
+
if (startedAt - capturedAt > actionWindowMs) {
|
|
94
|
+
latestAction = undefined;
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
latestAction = undefined;
|
|
98
|
+
return action;
|
|
99
|
+
}
|
|
100
|
+
function wrappedFetch(input, init) {
|
|
101
|
+
const metadata = metadataFromFetchInput(input, init, now);
|
|
102
|
+
const normalized = normalizeUrl(metadata.url);
|
|
103
|
+
const requestInit = isActionRequestTarget(normalized)
|
|
104
|
+
? withAnlyxRequestHeader(init, metadata.id)
|
|
105
|
+
: init;
|
|
106
|
+
return originalFetch(input, requestInit)
|
|
107
|
+
.then((response) => {
|
|
108
|
+
const event = buildEvent(metadata, response.status);
|
|
109
|
+
if (event) {
|
|
110
|
+
postEvent(event);
|
|
111
|
+
}
|
|
112
|
+
return response;
|
|
113
|
+
})
|
|
114
|
+
.catch((error) => {
|
|
115
|
+
const event = buildEvent(metadata);
|
|
116
|
+
if (event) {
|
|
117
|
+
postEvent(event);
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function wrappedOpen(method, url, async, username, password) {
|
|
123
|
+
xhrMetadata.set(this, {
|
|
124
|
+
id: nextRequestId(now),
|
|
125
|
+
method,
|
|
126
|
+
url: String(url),
|
|
127
|
+
startedAt: 0
|
|
128
|
+
});
|
|
129
|
+
return originalOpen.call(this, method, url, async ?? true, username, password);
|
|
130
|
+
}
|
|
131
|
+
function wrappedSend(body) {
|
|
132
|
+
const metadata = xhrMetadata.get(this);
|
|
133
|
+
if (metadata) {
|
|
134
|
+
metadata.startedAt = now();
|
|
135
|
+
const normalized = normalizeUrl(metadata.url);
|
|
136
|
+
if (isActionRequestTarget(normalized)) {
|
|
137
|
+
try {
|
|
138
|
+
this.setRequestHeader("X-Anlyx-Request-Id", metadata.id);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Some XHR states or environments can reject late header writes. Capture still continues.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.addEventListener("loadend", () => {
|
|
145
|
+
const event = buildEvent(metadata, this.status);
|
|
146
|
+
if (event) {
|
|
147
|
+
postEvent(event);
|
|
148
|
+
}
|
|
149
|
+
}, { once: true });
|
|
150
|
+
}
|
|
151
|
+
return originalSend.call(this, body);
|
|
152
|
+
}
|
|
153
|
+
function wrappedPushState(data, unused, url) {
|
|
154
|
+
const result = originalPushState.call(this, data, unused, url);
|
|
155
|
+
queuePageView();
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
function wrappedReplaceState(data, unused, url) {
|
|
159
|
+
const result = originalReplaceState.call(this, data, unused, url);
|
|
160
|
+
queuePageView();
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
function queuePageView() {
|
|
164
|
+
window.setTimeout(observePageView, 0);
|
|
165
|
+
}
|
|
166
|
+
document.addEventListener("click", observeClick, true);
|
|
167
|
+
window.addEventListener("popstate", queuePageView);
|
|
168
|
+
window.fetch = wrappedFetch;
|
|
169
|
+
window.history.pushState = wrappedPushState;
|
|
170
|
+
window.history.replaceState = wrappedReplaceState;
|
|
171
|
+
window.XMLHttpRequest.prototype.open = wrappedOpen;
|
|
172
|
+
window.XMLHttpRequest.prototype.send = wrappedSend;
|
|
173
|
+
window.__ANLYX_CAPTURE_INSTALLED__ = true;
|
|
174
|
+
queuePageView();
|
|
175
|
+
return () => {
|
|
176
|
+
document.removeEventListener("click", observeClick, true);
|
|
177
|
+
window.removeEventListener("popstate", queuePageView);
|
|
178
|
+
window.fetch = originalWindowFetch;
|
|
179
|
+
window.history.pushState = originalPushState;
|
|
180
|
+
window.history.replaceState = originalReplaceState;
|
|
181
|
+
window.XMLHttpRequest.prototype.open = originalOpen;
|
|
182
|
+
window.XMLHttpRequest.prototype.send = originalSend;
|
|
183
|
+
window.__ANLYX_CAPTURE_INSTALLED__ = false;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function metadataFromFetchInput(input, init, now) {
|
|
187
|
+
if (input instanceof Request) {
|
|
188
|
+
return {
|
|
189
|
+
id: nextRequestId(now),
|
|
190
|
+
method: init?.method ?? input.method ?? "GET",
|
|
191
|
+
url: input.url,
|
|
192
|
+
startedAt: now()
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
id: nextRequestId(now),
|
|
197
|
+
method: init?.method ?? "GET",
|
|
198
|
+
url: String(input),
|
|
199
|
+
startedAt: now()
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function nextRequestId(now) {
|
|
203
|
+
return `browser-request:${now()}:${++eventSequence}`;
|
|
204
|
+
}
|
|
205
|
+
function withAnlyxRequestHeader(init, requestId) {
|
|
206
|
+
const headers = new Headers(init?.headers);
|
|
207
|
+
headers.set("X-Anlyx-Request-Id", requestId);
|
|
208
|
+
return {
|
|
209
|
+
...init,
|
|
210
|
+
headers
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function normalizeUrl(urlOrPath) {
|
|
214
|
+
const url = new URL(urlOrPath, window.location.href);
|
|
215
|
+
return {
|
|
216
|
+
href: url.toString(),
|
|
217
|
+
path: `${url.pathname}${url.search}`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function shouldIgnoreUrl(normalized, ingestUrl, ingestPath) {
|
|
221
|
+
if (normalized.href === ingestUrl || normalized.path === ingestPath) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (normalized.path.includes("/_anlyx")) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (normalized.path.startsWith("/@vite") ||
|
|
228
|
+
normalized.path.startsWith("/@react-refresh") ||
|
|
229
|
+
normalized.path.startsWith("/__vite") ||
|
|
230
|
+
normalized.path.startsWith("/node_modules/")) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (normalized.path === "/favicon.ico") {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (new URL(normalized.href).searchParams.has("_rsc")) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return /\.(?:avif|css|eot|gif|ico|jpe?g|js|map|mjs|png|svg|ts|tsx|ttf|webp|woff2?)($|\?)/i.test(normalized.path);
|
|
240
|
+
}
|
|
241
|
+
function shouldIgnorePagePath(path) {
|
|
242
|
+
return (path.includes("/_anlyx") ||
|
|
243
|
+
path.startsWith("/@vite") ||
|
|
244
|
+
path.startsWith("/@react-refresh") ||
|
|
245
|
+
path.startsWith("/__vite") ||
|
|
246
|
+
path.startsWith("/node_modules/") ||
|
|
247
|
+
/\.(?:avif|css|eot|gif|ico|jpe?g|js|map|mjs|png|svg|ts|tsx|ttf|webp|woff2?)($|\?)/i.test(path));
|
|
248
|
+
}
|
|
249
|
+
function isActionRequestTarget(normalized) {
|
|
250
|
+
return normalized.path.startsWith("/api/");
|
|
251
|
+
}
|
|
252
|
+
function observedAt(now) {
|
|
253
|
+
return new Date(now()).toISOString();
|
|
254
|
+
}
|
|
255
|
+
function readableText(element) {
|
|
256
|
+
return element.textContent?.replace(/\s+/g, " ").trim() || undefined;
|
|
257
|
+
}
|
|
258
|
+
function readableLabel(element, text) {
|
|
259
|
+
return (element.getAttribute("aria-label") ||
|
|
260
|
+
element.getAttribute("title") ||
|
|
261
|
+
element.getAttribute("alt") ||
|
|
262
|
+
valueFor(element) ||
|
|
263
|
+
text ||
|
|
264
|
+
element.tagName.toLowerCase());
|
|
265
|
+
}
|
|
266
|
+
function valueFor(element) {
|
|
267
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLButtonElement) {
|
|
268
|
+
return element.value || undefined;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
function selectorFor(element) {
|
|
273
|
+
const id = element.getAttribute("id");
|
|
274
|
+
if (id) {
|
|
275
|
+
return `#${cssIdentifierEscape(id)}`;
|
|
276
|
+
}
|
|
277
|
+
const testId = element.getAttribute("data-testid");
|
|
278
|
+
if (testId) {
|
|
279
|
+
return `[data-testid="${cssStringEscape(testId)}"]`;
|
|
280
|
+
}
|
|
281
|
+
const parts = [];
|
|
282
|
+
let current = element;
|
|
283
|
+
while (current && current !== document.body && parts.length < 4) {
|
|
284
|
+
const tagName = current.tagName.toLowerCase();
|
|
285
|
+
const className = Array.from(current.classList)
|
|
286
|
+
.slice(0, 2)
|
|
287
|
+
.map((name) => `.${cssIdentifierEscape(name)}`);
|
|
288
|
+
parts.unshift(`${tagName}${className.join("")}`);
|
|
289
|
+
current = current.parentElement;
|
|
290
|
+
}
|
|
291
|
+
return parts.length > 0 ? parts.join(" > ") : element.tagName.toLowerCase();
|
|
292
|
+
}
|
|
293
|
+
function cssStringEscape(value) {
|
|
294
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
295
|
+
}
|
|
296
|
+
function cssIdentifierEscape(value) {
|
|
297
|
+
return globalThis.CSS?.escape
|
|
298
|
+
? globalThis.CSS.escape(value)
|
|
299
|
+
: value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
300
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CheckCircle2, CircleHelp, Info, TriangleAlert } from "lucide-react";
|
|
3
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
4
|
+
export function AnalysisEvidenceList({ node }) {
|
|
5
|
+
const evidence = getEvidence(node);
|
|
6
|
+
return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Analysis evidence", children: [_jsx("h3", { children: "Analysis evidence" }), _jsx("ul", { className: "anlyx-evidence-list", children: evidence.map((item, index) => {
|
|
7
|
+
const Icon = getEvidenceIcon(item.confidence ?? node.confidence ?? "unknown");
|
|
8
|
+
return (_jsxs("li", { children: [_jsx(Icon, { size: 14, strokeWidth: 2.5 }), _jsxs("span", { children: [_jsx("strong", { children: item.label }), item.detail ? _jsx("em", { children: item.detail }) : null] }), _jsx(StatusBadge, { tone: item.confidence ?? node.confidence ?? "unknown", children: item.confidence ?? node.confidence ?? "unknown" })] }, `${item.label}:${index}`));
|
|
9
|
+
}) })] }));
|
|
10
|
+
}
|
|
11
|
+
function getEvidence(node) {
|
|
12
|
+
if (node.evidence && node.evidence.length > 0) {
|
|
13
|
+
return node.evidence;
|
|
14
|
+
}
|
|
15
|
+
if (node.type === "unknown") {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
label: "Analysis stopped",
|
|
19
|
+
detail: "Anlyx could not resolve this code element from the scanned source.",
|
|
20
|
+
confidence: "unknown"
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
if (node.type === "database") {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
label: "Database table inferred",
|
|
28
|
+
detail: "Derived from repository entity metadata or entity naming fallback.",
|
|
29
|
+
confidence: node.confidence ?? "unknown"
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
if (node.type === "endpoint") {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
label: "Endpoint matched",
|
|
37
|
+
detail: "Derived from the backend adapter endpoint list.",
|
|
38
|
+
confidence: node.confidence ?? "unknown"
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
label: "Code node resolved",
|
|
45
|
+
detail: "Resolved from the scanned static flow graph.",
|
|
46
|
+
confidence: node.confidence ?? "unknown"
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function getEvidenceIcon(confidence) {
|
|
51
|
+
if (confidence === "high") {
|
|
52
|
+
return CheckCircle2;
|
|
53
|
+
}
|
|
54
|
+
if (confidence === "medium") {
|
|
55
|
+
return Info;
|
|
56
|
+
}
|
|
57
|
+
if (confidence === "low") {
|
|
58
|
+
return TriangleAlert;
|
|
59
|
+
}
|
|
60
|
+
return CircleHelp;
|
|
61
|
+
}
|
|
@@ -2,5 +2,5 @@ import type { ScanResult } from "@anlyx/core";
|
|
|
2
2
|
export type AnlyxAppShellProps = {
|
|
3
3
|
data: ScanResult;
|
|
4
4
|
};
|
|
5
|
-
export type ViewMode = "structure" | "frontend" | "process";
|
|
5
|
+
export type ViewMode = "flowStory" | "structure" | "frontend" | "process";
|
|
6
6
|
export declare function AnlyxAppShell({ data }: AnlyxAppShellProps): JSX.Element;
|
|
@@ -2,15 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { Group, Panel, Separator, usePanelRef } from "react-resizable-panels";
|
|
4
4
|
import { EndpointMapCanvas } from "./EndpointMapCanvas.js";
|
|
5
|
+
import { FlowStoryView } from "./FlowStoryView.js";
|
|
5
6
|
import { InspectorPanel } from "./InspectorPanel.js";
|
|
6
7
|
import { PageStoryboardView } from "./PageStoryboardView.js";
|
|
7
8
|
import { ProcessFlowView } from "./ProcessFlowView.js";
|
|
8
9
|
import { Sidebar } from "./Sidebar.js";
|
|
9
10
|
import { useReplayLite } from "../replay/use-replay-lite.js";
|
|
10
11
|
const STORAGE_KEYS = {
|
|
11
|
-
leftCollapsed: "anlyx:ui:leftCollapsed",
|
|
12
|
-
panelLayout: "anlyx:ui:panelLayout",
|
|
13
|
-
rightCollapsed: "anlyx:ui:rightCollapsed",
|
|
12
|
+
leftCollapsed: "anlyx:ui:v2:leftCollapsed",
|
|
13
|
+
panelLayout: "anlyx:ui:v2:panelLayout",
|
|
14
|
+
rightCollapsed: "anlyx:ui:v2:rightCollapsed",
|
|
14
15
|
selectedEndpointId: "anlyx:ui:selectedEndpointId",
|
|
15
16
|
selectedPageId: "anlyx:ui:selectedPageId"
|
|
16
17
|
};
|
|
@@ -20,7 +21,7 @@ const DEFAULT_PANEL_LAYOUT = {
|
|
|
20
21
|
right: 26
|
|
21
22
|
};
|
|
22
23
|
export function AnlyxAppShell({ data }) {
|
|
23
|
-
const [activeView, setActiveView] = useState("
|
|
24
|
+
const [activeView, setActiveView] = useState("flowStory");
|
|
24
25
|
const [selectedEndpointId, setSelectedEndpointId] = usePersistentString(STORAGE_KEYS.selectedEndpointId, selectInitialEndpointId(data));
|
|
25
26
|
const [selectedPageId, setSelectedPageId] = usePersistentString(STORAGE_KEYS.selectedPageId, data.pages[0]?.id);
|
|
26
27
|
const leftPanelRef = usePanelRef();
|
|
@@ -80,11 +81,19 @@ export function AnlyxAppShell({ data }) {
|
|
|
80
81
|
};
|
|
81
82
|
return (_jsxs("div", { className: "anlyx-shell", role: "application", "aria-label": "Anlyx application shell", children: [_jsxs(Group, { className: "anlyx-panel-group", defaultLayout: panelLayout, id: "anlyx-main-panels", orientation: "horizontal", onLayoutChanged: (layout) => writeLocalStorage(STORAGE_KEYS.panelLayout, JSON.stringify(layout)), children: [_jsx(Panel, { className: "anlyx-panel anlyx-panel--sidebar", collapsedSize: "52px", collapsible: true, defaultSize: "300px", id: "left", maxSize: "420px", minSize: "240px", panelRef: leftPanelRef, children: _jsx(Sidebar, { data: data, activeView: activeView, collapsed: leftCollapsed, selectedEndpointId: selectedEndpoint?.id, selectedPageId: selectedPage?.id, onSelectView: setActiveView, onToggleCollapsed: toggleLeftPanel, onSelectEndpoint: (endpoint) => {
|
|
82
83
|
setSelectedEndpointId(endpoint.id);
|
|
83
|
-
|
|
84
|
+
if (activeView !== "structure" && activeView !== "process") {
|
|
85
|
+
setActiveView("flowStory");
|
|
86
|
+
}
|
|
84
87
|
}, onSelectPage: (page) => {
|
|
85
88
|
setSelectedPageId(page.id);
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const linkedEndpointId = page.apiCalls.find((apiCall) => apiCall.endpointId)?.endpointId;
|
|
90
|
+
if (linkedEndpointId) {
|
|
91
|
+
setSelectedEndpointId(linkedEndpointId);
|
|
92
|
+
}
|
|
93
|
+
if (activeView !== "frontend") {
|
|
94
|
+
setActiveView("flowStory");
|
|
95
|
+
}
|
|
96
|
+
} }) }), _jsx(Separator, { "aria-label": "Resize navigation panel", className: "anlyx-resize-handle", children: _jsx("span", { "aria-hidden": "true" }) }), _jsx(Panel, { className: "anlyx-panel anlyx-panel--main", id: "center", minSize: "420px", children: _jsxs("div", { className: activeView === "process" ? "anlyx-main anlyx-main--process" : "anlyx-main", "aria-live": "polite", children: [activeView === "flowStory" ? (_jsx(FlowStoryView, { data: data, endpoint: selectedEndpoint, flow: selectedFlow, page: selectedPage, replayDisabled: replayUnavailable, replayLoop: replay.loop, replaySpeed: replaySpeed, replayState: replay.state, replaySteps: replay.steps, selectedNodeId: selectedNode?.id, onPause: replay.pause, onPlay: replay.play, onRestart: replay.restart, onSelectNode: (node) => setSelectedNodeId(node.id), onSpeedChange: setReplaySpeed, onToggleLoop: replay.toggleLoop })) : null, activeView === "structure" ? (_jsx(EndpointMapCanvas, { eyebrow: "Backend API Structure", endpoint: selectedEndpoint, flow: selectedFlow, replayState: replay.state, selectedNodeId: selectedNode?.id, toolbar: _jsx(StructureToolbar, {}), onSelectNode: (node) => setSelectedNodeId(node.id) })) : null, activeView === "frontend" ? (_jsx(PageStoryboardView, { data: data, page: selectedPage, onViewProcessFlow: setActiveView })) : null, activeView === "process" ? (_jsx(ProcessFlowView, { endpoint: selectedEndpoint, flow: selectedFlow, replayDisabled: replayUnavailable, replayLoop: replay.loop, replaySpeed: replaySpeed, replayState: replay.state, replaySteps: replay.steps, selectedNodeId: selectedNode?.id, onPause: replay.pause, onPlay: replay.play, onRestart: replay.restart, onSelectNode: (node) => setSelectedNodeId(node.id), onSpeedChange: setReplaySpeed, onToggleLoop: replay.toggleLoop, onViewStructure: () => setActiveView("structure") })) : null] }) }), _jsx(Separator, { "aria-label": "Resize inspector panel", className: "anlyx-resize-handle", children: _jsx("span", { "aria-hidden": "true" }) }), _jsx(Panel, { className: "anlyx-panel anlyx-panel--inspector", collapsedSize: "52px", collapsible: true, defaultSize: "360px", id: "right", maxSize: "520px", minSize: "300px", panelRef: rightPanelRef, children: _jsx(InspectorPanel, { activeView: activeView, collapsed: rightCollapsed, data: data, replayState: replay.state, selectedFlow: selectedFlow, selectedNode: selectedNode, selectedPage: selectedPage, onToggleCollapsed: toggleRightPanel }) })] }), _jsxs("div", { className: "anlyx-generated-at", children: ["Generated ", data.generatedAt] })] }));
|
|
88
97
|
}
|
|
89
98
|
function StructureToolbar() {
|
|
90
99
|
return (_jsxs("div", { className: "anlyx-toolbar", "aria-label": "Structure view actions", children: [_jsx("button", { className: "anlyx-toolbar-button", type: "button", children: "Fit view" }), _jsxs("select", { "aria-label": "Zoom level", className: "anlyx-toolbar-select", defaultValue: "100", children: [_jsx("option", { value: "75", children: "75%" }), _jsx("option", { value: "100", children: "100%" }), _jsx("option", { value: "125", children: "125%" })] }), _jsx("button", { className: "anlyx-toolbar-button anlyx-toolbar-button--icon", type: "button", children: "More" })] }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ApiCall } from "@anlyx/core";
|
|
1
|
+
import type { ApiCall, Endpoint } from "@anlyx/core";
|
|
2
2
|
export type ApiCallListProps = {
|
|
3
3
|
apiCalls: ApiCall[];
|
|
4
|
+
endpoints?: Endpoint[];
|
|
4
5
|
};
|
|
5
|
-
export declare function ApiCallList({ apiCalls }: ApiCallListProps): JSX.Element;
|
|
6
|
+
export declare function ApiCallList({ apiCalls, endpoints }: ApiCallListProps): JSX.Element;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StatusBadge } from "./StatusBadge.js";
|
|
3
|
-
export function ApiCallList({ apiCalls }) {
|
|
4
|
-
|
|
3
|
+
export function ApiCallList({ apiCalls, endpoints = [] }) {
|
|
4
|
+
const endpointById = new Map(endpoints.map((endpoint) => [endpoint.id, endpoint]));
|
|
5
|
+
return (_jsxs("section", { className: "anlyx-storyboard-panel", "aria-label": "API Calls", children: [_jsxs("div", { className: "anlyx-storyboard-section-heading", children: [_jsx("h2", { children: "API Calls" }), _jsx("span", { children: apiCalls.length })] }), apiCalls.length > 0 ? (_jsx("ul", { className: "anlyx-api-call-list", children: apiCalls.map((apiCall, index) => {
|
|
6
|
+
const endpoint = apiCall.endpointId ? endpointById.get(apiCall.endpointId) : undefined;
|
|
7
|
+
return (_jsxs("li", { className: "anlyx-api-call", children: [_jsxs("div", { className: "anlyx-api-call__line", children: [_jsx(StatusBadge, { tone: apiCall.method, children: apiCall.method }), _jsx("span", { className: "anlyx-api-call__path", children: apiCall.path })] }), _jsxs("div", { className: "anlyx-api-call__meta", children: [_jsx(StatusBadge, { tone: statusTone(apiCall.status), children: apiCall.status === undefined ? "unknown" : String(apiCall.status) }), _jsx(StatusBadge, { tone: apiCall.endpointId ? "success" : "unknown", children: apiCall.endpointId ? "Linked endpoint" : "Unmatched" })] }), endpoint ? (_jsxs("div", { className: "anlyx-api-call__endpoint", children: [_jsx("span", { children: "Matched endpoint" }), _jsxs("strong", { children: [endpoint.method, " ", endpoint.path] }), endpoint.controller || endpoint.handler ? (_jsx("em", { children: formatEndpointHandler(endpoint) })) : null] })) : apiCall.endpointId ? (_jsxs("div", { className: "anlyx-api-call__endpoint", children: [_jsx("span", { children: "Matched endpoint" }), _jsx("strong", { children: apiCall.endpointId })] })) : null] }, `${apiCall.method}:${apiCall.path}:${index}`));
|
|
8
|
+
}) })) : (_jsx("p", { className: "anlyx-empty-inline", children: "No API calls captured yet." }))] }));
|
|
9
|
+
}
|
|
10
|
+
function formatEndpointHandler(endpoint) {
|
|
11
|
+
if (endpoint.controller && endpoint.handler) {
|
|
12
|
+
return `${endpoint.controller}#${endpoint.handler}`;
|
|
13
|
+
}
|
|
14
|
+
return endpoint.controller ?? endpoint.handler ?? "Unknown handler";
|
|
5
15
|
}
|
|
6
16
|
function statusTone(status) {
|
|
7
17
|
if (status === undefined) {
|
|
@@ -5,7 +5,7 @@ export function CaptureStatusEmptyState({ status, reason }) {
|
|
|
5
5
|
}
|
|
6
6
|
const title = status === "failed" ? "Capture failed" : "Capture was skipped.";
|
|
7
7
|
const fallbackReason = status === "pending"
|
|
8
|
-
? "
|
|
8
|
+
? "The imported Flow JSON did not include page capture evidence."
|
|
9
9
|
: "Unknown";
|
|
10
|
-
return (_jsxs("section", { className: `anlyx-capture-state anlyx-capture-state--${status}`, children: [_jsx("h2", { children: title }), _jsxs("p", { children: ["Reason: ", reason ?? fallbackReason] }), status === "pending" ? (_jsx("p", { children: "
|
|
10
|
+
return (_jsxs("section", { className: `anlyx-capture-state anlyx-capture-state--${status}`, children: [_jsx("h2", { children: title }), _jsxs("p", { children: ["Reason: ", reason ?? fallbackReason] }), status === "pending" ? (_jsx("p", { children: "Add page evidence to Flow JSON, then run `anlyx import` again." })) : null] }));
|
|
11
11
|
}
|
|
@@ -73,7 +73,7 @@ export function EndpointMapCanvas({ endpoint, flow, selectedNodeId, title, eyebr
|
|
|
73
73
|
data: nextData
|
|
74
74
|
};
|
|
75
75
|
}) ?? [], [model, replayState?.activeEdge]);
|
|
76
|
-
return (_jsxs("main", { className: `anlyx-workspace anlyx-workspace--${variant}`, children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: eyebrow }), _jsx("h1", { children: title ?? (endpoint ? `${endpoint.method} ${endpoint.path}` : "No endpoint selected") })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [toolbar, endpoint ? (_jsx(StatusBadge, { tone: endpoint.confidence ?? "unknown", children: endpoint.confidence ?? "unknown" })) : null] })] }), _jsx("section", { className: `anlyx-endpoint-map anlyx-endpoint-map--${variant}`, role: "region", "aria-label": variant === "process" ? "Process Flow map" : "Endpoint Map", children: flow && model && model.nodes.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(FlowLegend, { variant: variant }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Endpoint map node list", children: nodes.map((node) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelectNode(node.data.node), children: ["Select node ", node.data.label] }) }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay node state", children: nodes.map((node) => (_jsx("li", { "data-replay-active": String(Boolean(node.data.isReplayActive)), "data-testid": `replay-node-${node.id}`, children: node.id }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay edge state", children: edges.map((edge) => (_jsxs("li", { "data-replay-active": String(Boolean(edge.data?.isReplayActive)), "data-testid": `replay-edge-${edge.source}-${edge.target}`, children: [edge.source, " to ", edge.target] }, edge.id))) }), _jsxs(ReactFlow, { className: "anlyx-react-flow", edges: edges, fitView: true, fitViewOptions: { padding: 0.18 }, maxZoom: 1.35, minZoom: 0.62, nodes: nodes, nodesConnectable: false, nodesDraggable: false, edgeTypes: edgeTypes, nodeTypes: nodeTypes, onNodeClick: (_, node) => onSelectNode(node.data.node), panOnScroll: true, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnScroll: false, children: [_jsx(Background, { color: "#dfe5ee", gap: 24, variant: BackgroundVariant.Dots }), _jsx(Controls, { showInteractive: false })] })] })) : (
|
|
76
|
+
return (_jsxs("main", { className: `anlyx-workspace anlyx-workspace--${variant}`, children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: eyebrow }), _jsx("h1", { children: title ?? (endpoint ? `${endpoint.method} ${endpoint.path}` : "No endpoint selected") })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [toolbar, endpoint ? (_jsx(StatusBadge, { tone: endpoint.confidence ?? "unknown", children: endpoint.confidence ?? "unknown" })) : null] })] }), _jsx("section", { className: `anlyx-endpoint-map anlyx-endpoint-map--${variant}`, role: "region", "aria-label": variant === "process" ? "Process Flow map" : "Endpoint Map", children: flow && model && model.nodes.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(FlowLegend, { variant: variant }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Endpoint map node list", children: nodes.map((node) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelectNode(node.data.node), children: ["Select node ", node.data.label] }) }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay node state", children: nodes.map((node) => (_jsx("li", { "data-replay-active": String(Boolean(node.data.isReplayActive)), "data-testid": `replay-node-${node.id}`, children: node.id }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay edge state", children: edges.map((edge) => (_jsxs("li", { "data-replay-active": String(Boolean(edge.data?.isReplayActive)), "data-testid": `replay-edge-${edge.source}-${edge.target}`, children: [edge.source, " to ", edge.target] }, edge.id))) }), _jsxs(ReactFlow, { className: "anlyx-react-flow", edges: edges, fitView: true, fitViewOptions: { padding: 0.18 }, maxZoom: 1.35, minZoom: 0.62, nodes: nodes, nodesConnectable: false, nodesDraggable: false, edgeTypes: edgeTypes, nodeTypes: nodeTypes, onNodeClick: (_, node) => onSelectNode(node.data.node), panOnScroll: true, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnScroll: false, children: [_jsx(Background, { color: "#dfe5ee", gap: 24, variant: BackgroundVariant.Dots }), _jsx(Controls, { showInteractive: false })] })] })) : (_jsxs("div", { className: "anlyx-endpoint-map-empty", role: "status", "aria-label": "Flow unavailable", children: [_jsx("span", { children: "Flow unavailable" }), _jsx("h2", { children: "No scanned flow for this endpoint yet" }), _jsx("p", { children: "Anlyx can list this endpoint, but no Controller -> Service -> Repository path was found." }), _jsx("p", { children: "Update the Flow JSON with source evidence, then run `anlyx import` again." })] })) })] }));
|
|
77
77
|
}
|
|
78
78
|
function isUnitTestRuntime() {
|
|
79
79
|
return typeof process !== "undefined" && process.env.NODE_ENV === "test";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Endpoint, EndpointFlow, FlowNode, PageStoryboard, ScanResult } from "@anlyx/core";
|
|
2
|
+
import type { ReplayStep } from "../replay/build-replay-steps.js";
|
|
3
|
+
import type { ReplayLiteState } from "../replay/use-replay-lite.js";
|
|
4
|
+
export type FlowStoryViewProps = {
|
|
5
|
+
data: ScanResult;
|
|
6
|
+
endpoint: Endpoint | undefined;
|
|
7
|
+
flow: EndpointFlow | undefined;
|
|
8
|
+
page: PageStoryboard | undefined;
|
|
9
|
+
replayDisabled: boolean;
|
|
10
|
+
replayLoop: boolean;
|
|
11
|
+
replaySpeed: number;
|
|
12
|
+
replayState: ReplayLiteState;
|
|
13
|
+
replaySteps: ReplayStep[];
|
|
14
|
+
selectedNodeId: string | undefined;
|
|
15
|
+
onPause: () => void;
|
|
16
|
+
onPlay: () => void;
|
|
17
|
+
onRestart: () => void;
|
|
18
|
+
onSelectNode: (node: FlowNode) => void;
|
|
19
|
+
onSpeedChange: (speed: number) => void;
|
|
20
|
+
onToggleLoop: () => void;
|
|
21
|
+
};
|
|
22
|
+
export declare function FlowStoryView({ data, endpoint, flow, page, replayDisabled, replayLoop, replaySpeed, replayState, replaySteps, selectedNodeId, onPause, onPlay, onRestart, onSelectNode, onSpeedChange, onToggleLoop }: FlowStoryViewProps): JSX.Element;
|