@anlyx/ui 0.1.3 → 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/CaptureStatusEmptyState.js +2 -2
- package/dist/components/EndpointMapCanvas.js +1 -1
- package/dist/components/InspectorPanel.js +27 -1
- package/dist/components/StatusBadge.d.ts +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/overlay/MainFlowCanvas.js +1 -1
- package/dist/overlay/overlay-ui.js +1 -1
- package/dist/readme-demo/ReadmeDemoApp.d.ts +12 -1
- package/dist/readme-demo/ReadmeDemoApp.js +74 -16
- package/dist/styles.css +17 -4
- package/dist/viewer/ViewerApp.js +18 -15
- 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 +9 -2
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
|
+
}
|
|
@@ -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 })] })] })) : (_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: "
|
|
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";
|
|
@@ -10,11 +10,24 @@ export function InspectorPanel({ data, activeView, collapsed, selectedFlow, sele
|
|
|
10
10
|
}
|
|
11
11
|
const linkedPages = selectedNode ? findLinkedPages(data, selectedNode.id) : [];
|
|
12
12
|
const calls = selectedNode ? findCalls(selectedFlow, selectedNode.id) : [];
|
|
13
|
-
return (_jsxs("aside", { className: "anlyx-inspector", role: "complementary", "aria-label": "Inspector", children: [_jsxs("div", { className: "anlyx-panel-heading", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Inspector" }), _jsx("h2", { children: activeView === "process" ? "Process Step" : "Flow Evidence" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse inspector panel", onClick: onToggleCollapsed, children: "Collapse" })] }), selectedNode ? (_jsxs("div", { className: "anlyx-inspector-stack", children: [activeView === "process" ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Replay state", children: [_jsx("h3", { children: "Process replay" }), _jsx(Field, { label: "Active Step", value: replayState.phase }), _jsx(Field, { label: "Step", value: String(replayState.currentStepIndex + 1) }), _jsx(Field, { label: "Active Node", value: replayState.activeNodeId ?? "none" }), _jsx(Field, { label: "Active Edge", value: formatActiveEdge(replayState) }), _jsx("p", { className: "anlyx-inspector-note", children: "Source: scanned static flow graph" })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Details", children: [_jsx("h3", { children: "Details" }), _jsx(Field, { label: "Type", value: selectedNode.type }), _jsx(Field, { label: "Label", value: selectedNode.label }), _jsx(Field, { label: "File path", value: selectedNode.filePath ?? "Unknown" }), _jsx(Field, { label: "Line number", value: formatLineNumber(selectedNode.lineNumber) })] }), _jsx(AnalysisEvidenceList, { node: selectedNode }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Calls", children: [_jsx("h3", { children: "Calls" }), calls.length > 0 ? (_jsx("ul", { className: "anlyx-call-list", children: calls.map((call) => (_jsxs("li", { children: [_jsx("span", { children: call.label }), _jsx(StatusBadge, { tone: call.confidence, children: call.confidence })] }, call.id))) })) : (_jsx("p", { children: "No outgoing calls detected for this node." }))] }), selectedNode.metadata ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Metadata", children: [_jsxs("div", { className: "anlyx-inspector-group__heading", children: [_jsx("h3", { children: "Metadata" }), _jsx("button", { className: "anlyx-copy-button", type: "button", onClick: () => copyToClipboard(JSON.stringify(selectedNode.metadata, null, 2)), children: "Copy" })] }), _jsx("pre", { className: "anlyx-metadata", children: JSON.stringify(selectedNode.metadata, null, 2) })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Confidence", children: [_jsx("h3", { children: "Confidence" }), _jsx(StatusBadge, { tone: selectedNode.confidence ?? "unknown", label: "confidence", children: selectedNode.confidence ?? "unknown" })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Linked pages", children: [_jsx("h3", { children: "Linked pages" }), linkedPages.length > 0 ? (_jsx("ul", { children: linkedPages.map((page) => (_jsx("li", { children: page.route }, page.id))) })) : (_jsx("p", { children: "None" }))] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Sub flows", children: [_jsx("h3", { children: "Sub flows" }), _jsxs("p", { children: [selectedFlow?.subFlows.length ?? 0, " collapsed"] })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "DB tables", children: [_jsx("h3", { children: "DB tables" }), _jsx("p", { children: findDatabaseLabel(selectedFlow) ?? "None" })] })] })) : (_jsx("p", { className: "anlyx-empty", children: "No node selected" }))] }));
|
|
13
|
+
return (_jsxs("aside", { className: "anlyx-inspector", role: "complementary", "aria-label": "Inspector", children: [_jsxs("div", { className: "anlyx-panel-heading", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Inspector" }), _jsx("h2", { children: activeView === "process" ? "Process Step" : "Flow Evidence" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse inspector panel", onClick: onToggleCollapsed, children: "Collapse" })] }), selectedNode ? (_jsxs("div", { className: "anlyx-inspector-stack", children: [activeView === "process" ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Replay state", children: [_jsx("h3", { children: "Process replay" }), _jsx(Field, { label: "Active Step", value: replayState.phase }), _jsx(Field, { label: "Step", value: String(replayState.currentStepIndex + 1) }), _jsx(Field, { label: "Active Node", value: replayState.activeNodeId ?? "none" }), _jsx(Field, { label: "Active Edge", value: formatActiveEdge(replayState) }), _jsx("p", { className: "anlyx-inspector-note", children: "Source: scanned static flow graph" })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Details", children: [_jsx("h3", { children: "Details" }), _jsx(Field, { label: "Type", value: selectedNode.type }), _jsx(Field, { label: "Label", value: selectedNode.label }), _jsxs("div", { className: "anlyx-field", children: [_jsx("span", { className: "anlyx-field__label", children: "Status" }), _jsx(StatusBadge, { tone: selectedNode.status ?? "unknown", children: selectedNode.status ?? "unknown" })] }), _jsx(Field, { label: "File path", value: selectedNode.filePath ?? "Unknown" }), _jsx(Field, { label: "Line number", value: formatLineNumber(selectedNode.lineNumber) }), selectedNode.metadata ? (_jsx(Field, { label: "Generated by", value: formatGeneratedBy(selectedNode.metadata) })) : null] }), _jsx(TimingGroup, { timing: selectedNode.timing }), selectedNode.request ? (_jsx(JsonGroup, { title: "Request shape", ariaLabel: "Request shape", value: selectedNode.request })) : null, selectedNode.response ? (_jsx(JsonGroup, { title: "Response shape", ariaLabel: "Response shape", value: selectedNode.response })) : null, _jsx(AnalysisEvidenceList, { node: selectedNode }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Calls", children: [_jsx("h3", { children: "Calls" }), calls.length > 0 ? (_jsx("ul", { className: "anlyx-call-list", children: calls.map((call) => (_jsxs("li", { children: [_jsx("span", { children: call.label }), _jsx(StatusBadge, { tone: call.confidence, children: call.confidence })] }, call.id))) })) : (_jsx("p", { children: "No outgoing calls detected for this node." }))] }), selectedNode.metadata ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Metadata", children: [_jsxs("div", { className: "anlyx-inspector-group__heading", children: [_jsx("h3", { children: "Metadata" }), _jsx("button", { className: "anlyx-copy-button", type: "button", onClick: () => copyToClipboard(JSON.stringify(selectedNode.metadata, null, 2)), children: "Copy" })] }), _jsx("pre", { className: "anlyx-metadata", children: JSON.stringify(selectedNode.metadata, null, 2) })] })) : null, _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Confidence", children: [_jsx("h3", { children: "Confidence" }), _jsx(StatusBadge, { tone: selectedNode.confidence ?? "unknown", label: "confidence", children: selectedNode.confidence ?? "unknown" })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Linked pages", children: [_jsx("h3", { children: "Linked pages" }), linkedPages.length > 0 ? (_jsx("ul", { children: linkedPages.map((page) => (_jsx("li", { children: page.route }, page.id))) })) : (_jsx("p", { children: "None" }))] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Sub flows", children: [_jsx("h3", { children: "Sub flows" }), _jsxs("p", { children: [selectedFlow?.subFlows.length ?? 0, " collapsed"] })] }), _jsxs("section", { className: "anlyx-inspector-group", "aria-label": "DB tables", children: [_jsx("h3", { children: "DB tables" }), _jsx("p", { children: findDatabaseLabel(selectedFlow) ?? "None" })] })] })) : (_jsx("p", { className: "anlyx-empty", children: "No node selected" }))] }));
|
|
14
14
|
}
|
|
15
15
|
function Field({ label, value }) {
|
|
16
16
|
return (_jsxs("div", { className: "anlyx-field", children: [_jsx("span", { className: "anlyx-field__label", children: label }), _jsx("span", { className: "anlyx-field__value", children: value })] }));
|
|
17
17
|
}
|
|
18
|
+
function TimingGroup({ timing }) {
|
|
19
|
+
if (!timing || timing.kind === "unknown") {
|
|
20
|
+
return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Timing", children: [_jsx("h3", { children: "Timing" }), _jsx(Field, { label: "Kind", value: "Unknown" })] }));
|
|
21
|
+
}
|
|
22
|
+
if (timing.kind === "measured") {
|
|
23
|
+
return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Timing", children: [_jsx("h3", { children: "Timing" }), _jsx(Field, { label: "Kind", value: "Measured" }), _jsx(Field, { label: "Duration", value: `${timing.durationMs}ms` }), _jsx(Field, { label: "Evidence", value: timing.evidenceId })] }));
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Timing", children: [_jsx("h3", { children: "Timing" }), _jsx(Field, { label: "Kind", value: "estimate" }), timing.durationMs !== undefined ? (_jsx(Field, { label: "Duration", value: `${timing.durationMs}ms` })) : null, _jsx(Field, { label: "Reason", value: timing.reason })] }));
|
|
26
|
+
}
|
|
27
|
+
function JsonGroup({ title, ariaLabel, value }) {
|
|
28
|
+
const formatted = JSON.stringify(value, null, 2);
|
|
29
|
+
return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": ariaLabel, children: [_jsxs("div", { className: "anlyx-inspector-group__heading", children: [_jsx("h3", { children: title }), _jsx("button", { className: "anlyx-copy-button", type: "button", onClick: () => copyToClipboard(formatted), children: "Copy" })] }), _jsx("pre", { className: "anlyx-metadata anlyx-shape-json", children: formatted })] }));
|
|
30
|
+
}
|
|
18
31
|
function formatLineNumber(lineNumber) {
|
|
19
32
|
return lineNumber === undefined ? "Unknown" : String(lineNumber);
|
|
20
33
|
}
|
|
@@ -27,6 +40,19 @@ function formatActiveEdge(replayState) {
|
|
|
27
40
|
function copyToClipboard(value) {
|
|
28
41
|
void navigator.clipboard?.writeText(value);
|
|
29
42
|
}
|
|
43
|
+
function formatGeneratedBy(metadata) {
|
|
44
|
+
const generatedBy = metadata.generatedBy;
|
|
45
|
+
if (!isRecord(generatedBy)) {
|
|
46
|
+
return "Unknown";
|
|
47
|
+
}
|
|
48
|
+
const name = typeof generatedBy.name === "string" ? generatedBy.name : undefined;
|
|
49
|
+
const type = typeof generatedBy.type === "string" ? generatedBy.type : undefined;
|
|
50
|
+
const version = typeof generatedBy.version === "string" ? generatedBy.version : undefined;
|
|
51
|
+
return [name, type, version].filter(Boolean).join(" · ") || "Unknown";
|
|
52
|
+
}
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
30
56
|
function findLinkedPages(data, nodeId) {
|
|
31
57
|
return data.pages.filter((page) => page.apiCalls.some((apiCall) => apiCall.endpointId === nodeId));
|
|
32
58
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { CaptureStatus, ConfidenceLevel, HttpMethod } from "@anlyx/core";
|
|
1
|
+
import type { BridgeFlowStatus, CaptureStatus, ConfidenceLevel, HttpMethod } from "@anlyx/core";
|
|
2
2
|
type StatusBadgeProps = {
|
|
3
3
|
children: string;
|
|
4
|
-
tone?: CaptureStatus | ConfidenceLevel | HttpMethod | "neutral";
|
|
4
|
+
tone?: CaptureStatus | ConfidenceLevel | HttpMethod | BridgeFlowStatus | "neutral";
|
|
5
5
|
label?: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function StatusBadge({ children, tone, label }: StatusBadgeProps): JSX.Element;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export type { ReplayControlsProps } from "./components/ReplayControls.js";
|
|
|
5
5
|
export type { AnlyxFlowEdgeData, AnlyxFlowNodeData, AnlyxFlowRole, AnlyxReactFlowEdge, AnlyxReactFlowNode, ReactFlowModel } from "./flow/build-react-flow-model.js";
|
|
6
6
|
export type { ReplayPhase, ReplayStep } from "./replay/build-replay-steps.js";
|
|
7
7
|
export type { ReplayLiteState, UseReplayLiteOptions, UseReplayLiteResult } from "./replay/use-replay-lite.js";
|
|
8
|
+
export type { WorkspaceAppProps } from "./workspace/WorkspaceApp.js";
|
|
8
9
|
export { ViewerApp } from "./viewer/ViewerApp.js";
|
|
10
|
+
export { WorkspaceApp } from "./workspace/WorkspaceApp.js";
|
|
9
11
|
export { AnlyxAppShell } from "./components/AnlyxAppShell.js";
|
|
10
12
|
export { ApiCallList } from "./components/ApiCallList.js";
|
|
11
13
|
export { CaptureStatusEmptyState } from "./components/CaptureStatusEmptyState.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ViewerApp } from "./viewer/ViewerApp.js";
|
|
2
|
+
export { WorkspaceApp } from "./workspace/WorkspaceApp.js";
|
|
2
3
|
export { AnlyxAppShell } from "./components/AnlyxAppShell.js";
|
|
3
4
|
export { ApiCallList } from "./components/ApiCallList.js";
|
|
4
5
|
export { CaptureStatusEmptyState } from "./components/CaptureStatusEmptyState.js";
|
|
@@ -19,7 +19,7 @@ export function MainFlowCanvas({ flow, method, path, status, endpointConfidence
|
|
|
19
19
|
if (model.nodes.length === 0) {
|
|
20
20
|
return (_jsxs("section", { className: "anlyx-flow-rf-section", children: [_jsx("div", { className: "anlyx-flow-rf-head", children: _jsx("h3", { children: "Matched backend flow" }) }), _jsx("div", { className: "anlyx-flow-rf-empty", children: "No scanned main flow was inferred yet." })] }));
|
|
21
21
|
}
|
|
22
|
-
return (_jsxs("section", { className: "anlyx-flow-rf-section", children: [_jsxs("div", { className: "anlyx-flow-rf-head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Matched backend flow" }), _jsx("p", { children: "
|
|
22
|
+
return (_jsxs("section", { className: "anlyx-flow-rf-section", children: [_jsxs("div", { className: "anlyx-flow-rf-head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Matched backend flow" }), _jsx("p", { children: "Browser request first, scanned backend path follows." })] }), _jsxs("div", { className: "anlyx-flow-rf-head__badges", children: [_jsx(Badge, { tone: "blue", children: method }), _jsxs(Badge, { tone: "green", children: ["confidence ", endpointConfidence ?? "unknown"] })] })] }), _jsx("div", { className: "anlyx-flow-rf-canvas", "data-testid": "anlyx-react-flow-main", children: _jsxs(ReactFlow, { edgeTypes: edgeTypes, edges: model.edges, elementsSelectable: false, fitView: true, fitViewOptions: { padding: 0.16 }, maxZoom: 1.35, minZoom: 0.58, nodes: model.nodes, nodesConnectable: false, nodesDraggable: false, nodeTypes: nodeTypes, panOnDrag: true, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnPinch: true, zoomOnScroll: true, children: [_jsx(ViewportControls, {}), _jsx(Background, { color: "rgba(148, 163, 184, .42)", gap: 18, size: 1, variant: BackgroundVariant.Dots })] }) }), _jsx("p", { className: "anlyx-flow-rf-note", children: "Anlyx mapped this browser-observed request to a scanned backend path. Backend nodes are source-derived evidence, not a runtime trace; muted nodes are known code paths that the browser result did not prove were executed." })] }));
|
|
23
23
|
}
|
|
24
24
|
function ViewportControls() {
|
|
25
25
|
const { fitView, setViewport } = useReactFlow();
|