@anlyx/ui 0.1.2 → 0.1.3
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/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/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 +20 -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/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 +4 -0
- package/dist/readme-demo/ReadmeDemoApp.js +126 -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 +1134 -20
- package/dist/viewer/ViewerApp.js +13 -6
- package/package.json +3 -3
package/dist/mock-data.js
CHANGED
|
@@ -36,7 +36,15 @@ export const mockScanResult = scanResultSchema.parse({
|
|
|
36
36
|
id: "endpoint:get:/api/public/benefits/{id}",
|
|
37
37
|
type: "endpoint",
|
|
38
38
|
label: "GET /api/public/benefits/{id}",
|
|
39
|
-
confidence: "high"
|
|
39
|
+
confidence: "high",
|
|
40
|
+
evidence: [
|
|
41
|
+
{
|
|
42
|
+
label: "Matched Spring mapping",
|
|
43
|
+
detail: '@GetMapping("/api/public/benefits/{id}")',
|
|
44
|
+
source: "spring-endpoint-scanner",
|
|
45
|
+
confidence: "high"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
40
48
|
},
|
|
41
49
|
{
|
|
42
50
|
id: "controller:PublicBenefitController#getDetail",
|
|
@@ -44,25 +52,63 @@ export const mockScanResult = scanResultSchema.parse({
|
|
|
44
52
|
label: "PublicBenefitController#getDetail",
|
|
45
53
|
filePath: "backend/src/main/java/com/zup/benefit/PublicBenefitController.java",
|
|
46
54
|
lineNumber: 24,
|
|
47
|
-
confidence: "unknown"
|
|
55
|
+
confidence: "unknown",
|
|
56
|
+
evidence: [
|
|
57
|
+
{
|
|
58
|
+
label: "Controller method detected",
|
|
59
|
+
detail: "PublicBenefitController#getDetail",
|
|
60
|
+
source: "spring-flow-scanner",
|
|
61
|
+
confidence: "unknown"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
48
64
|
},
|
|
49
65
|
{
|
|
50
66
|
id: "service:PublicBenefitService#getBenefitDetail",
|
|
51
67
|
type: "service",
|
|
52
68
|
label: "PublicBenefitService#getBenefitDetail",
|
|
53
|
-
confidence: "high"
|
|
69
|
+
confidence: "high",
|
|
70
|
+
evidence: [
|
|
71
|
+
{
|
|
72
|
+
label: "Matched from controller field call",
|
|
73
|
+
detail: "publicBenefitService.getBenefitDetail(...)",
|
|
74
|
+
source: "spring-flow-scanner",
|
|
75
|
+
confidence: "high"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: "Resolved service class by field type",
|
|
79
|
+
detail: "PublicBenefitService",
|
|
80
|
+
source: "spring-flow-scanner",
|
|
81
|
+
confidence: "high"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
54
84
|
},
|
|
55
85
|
{
|
|
56
86
|
id: "repository:BenefitRepository#findById",
|
|
57
87
|
type: "repository",
|
|
58
88
|
label: "BenefitRepository#findById",
|
|
59
|
-
confidence: "high"
|
|
89
|
+
confidence: "high",
|
|
90
|
+
evidence: [
|
|
91
|
+
{
|
|
92
|
+
label: "Repository call detected in method body",
|
|
93
|
+
detail: "benefitRepository.findById(...)",
|
|
94
|
+
source: "spring-flow-scanner",
|
|
95
|
+
confidence: "high"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
60
98
|
},
|
|
61
99
|
{
|
|
62
100
|
id: "database:benefits",
|
|
63
101
|
type: "database",
|
|
64
102
|
label: "benefits",
|
|
65
103
|
confidence: "high",
|
|
104
|
+
evidence: [
|
|
105
|
+
{
|
|
106
|
+
label: "Resolved repository entity",
|
|
107
|
+
detail: "Benefit -> benefits",
|
|
108
|
+
source: "spring-flow-scanner",
|
|
109
|
+
confidence: "high"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
66
112
|
metadata: {
|
|
67
113
|
tableName: "benefits"
|
|
68
114
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { BaseEdge, getSmoothStepPath } from "@xyflow/react";
|
|
3
|
+
export function AnlyxFlowEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, data }) {
|
|
4
|
+
const [edgePath] = getSmoothStepPath({
|
|
5
|
+
sourceX,
|
|
6
|
+
sourceY,
|
|
7
|
+
sourcePosition,
|
|
8
|
+
targetX,
|
|
9
|
+
targetY,
|
|
10
|
+
targetPosition,
|
|
11
|
+
borderRadius: 14
|
|
12
|
+
});
|
|
13
|
+
const tone = (data?.tone ?? "blue").toLowerCase();
|
|
14
|
+
return (_jsx(BaseEdge, { id: id, className: `anlyx-flow-rf-edge anlyx-flow-rf-edge--${tone}`, path: edgePath, ...(markerEnd ? { markerEnd } : {}) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type NodeProps } from "@xyflow/react";
|
|
2
|
+
export type AnlyxFlowNodeData = {
|
|
3
|
+
kind: "api" | "controller" | "service" | "repository" | "database" | "auth" | "result";
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
sub?: string;
|
|
7
|
+
badge: string;
|
|
8
|
+
accent: "blue" | "green" | "amber" | "violet" | "gray";
|
|
9
|
+
fullValue?: string;
|
|
10
|
+
step?: string;
|
|
11
|
+
state?: "taken" | "blocked" | "scanned";
|
|
12
|
+
};
|
|
13
|
+
export declare function AnlyxFlowNode({ data }: NodeProps): JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Handle, Position } from "@xyflow/react";
|
|
3
|
+
import { Box, Code2, Database, Globe2, Layers3, LockKeyhole, ShieldCheck } from "lucide-react";
|
|
4
|
+
import { Badge, Card, Tooltip } from "./ui.js";
|
|
5
|
+
export function AnlyxFlowNode({ data }) {
|
|
6
|
+
const nodeData = data;
|
|
7
|
+
const Icon = getIcon(nodeData.kind);
|
|
8
|
+
const state = nodeData.state ?? "taken";
|
|
9
|
+
return (_jsxs(Card, { className: `anlyx-flow-rf-node anlyx-flow-rf-node--${nodeData.accent} anlyx-flow-rf-node--${state}`, children: [_jsx(Handle, { className: "anlyx-flow-rf-handle", position: Position.Left, type: "target" }), _jsxs("div", { className: "anlyx-flow-rf-node__top", children: [_jsx("span", { className: "anlyx-flow-rf-node__icon", "aria-hidden": "true", children: _jsx(Icon, { size: 14, strokeWidth: 2.25 }) }), _jsx("span", { className: "anlyx-flow-rf-node__label", children: nodeData.label }), nodeData.step ? _jsx("span", { className: "anlyx-flow-rf-node__step", children: nodeData.step }) : null] }), _jsx(Tooltip, { content: nodeData.fullValue ?? nodeData.value, children: _jsx("p", { className: "anlyx-flow-rf-node__value", children: nodeData.value }) }), nodeData.sub ? _jsx("p", { className: "anlyx-flow-rf-node__sub", children: nodeData.sub }) : null, _jsx(Badge, { tone: nodeData.accent === "violet" ? "violet" : nodeData.accent, children: nodeData.badge }), _jsx(Handle, { className: "anlyx-flow-rf-handle", position: Position.Right, type: "source" })] }));
|
|
10
|
+
}
|
|
11
|
+
function getIcon(kind) {
|
|
12
|
+
switch (kind) {
|
|
13
|
+
case "api":
|
|
14
|
+
return Globe2;
|
|
15
|
+
case "controller":
|
|
16
|
+
return Code2;
|
|
17
|
+
case "service":
|
|
18
|
+
return Layers3;
|
|
19
|
+
case "repository":
|
|
20
|
+
return Box;
|
|
21
|
+
case "database":
|
|
22
|
+
return Database;
|
|
23
|
+
case "auth":
|
|
24
|
+
return LockKeyhole;
|
|
25
|
+
case "result":
|
|
26
|
+
return ShieldCheck;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Card } from "./ui.js";
|
|
3
|
+
import { MainFlowCanvas } from "./MainFlowCanvas.js";
|
|
4
|
+
import { RecentApiEventsTable } from "./RecentApiEventsTable.js";
|
|
5
|
+
export function FlowDrawer({ selectedEvent, events, latestAction, scannedHints = [], loadError }) {
|
|
6
|
+
if (loadError) {
|
|
7
|
+
return (_jsx("div", { className: "anlyx-flow-drawer-body", children: _jsxs(Card, { children: [_jsx("h3", { className: "anlyx-ov-section-title", children: "Report data" }), _jsx("div", { className: "anlyx-ov-empty", children: loadError })] }) }));
|
|
8
|
+
}
|
|
9
|
+
if (!selectedEvent) {
|
|
10
|
+
return (_jsxs("div", { className: "anlyx-flow-drawer-body", children: [latestAction ? _jsx(NoPrimaryRequest, { latestAction: latestAction }) : _jsx(WaitingForAction, {}), scannedHints.length > 0 ? _jsx(ScannedHints, { hints: scannedHints }) : null, _jsx(RecentApiEventsTable, { events: events, selectedEventId: null })] }));
|
|
11
|
+
}
|
|
12
|
+
return (_jsxs("div", { className: "anlyx-flow-drawer-body", children: [_jsx(CapturedRequest, { event: selectedEvent }), selectedEvent.matchedEndpoint ? (_jsx(MainFlowCanvas, { flow: selectedEvent.matchedFlow, method: selectedEvent.method, path: selectedEvent.path, status: selectedEvent.status, ...(selectedEvent.matchedEndpoint.confidence
|
|
13
|
+
? { endpointConfidence: selectedEvent.matchedEndpoint.confidence }
|
|
14
|
+
: {}) })) : (_jsxs(Card, { className: "anlyx-unmatched-card", children: [_jsx("h3", { children: "No scanned endpoint matched" }), _jsx("p", { children: "Anlyx saw this browser request, but no scanned endpoint path matched it." })] })), _jsx(RecentApiEventsTable, { events: events, selectedEventId: selectedEvent.id })] }));
|
|
15
|
+
}
|
|
16
|
+
function ScannedHints({ hints }) {
|
|
17
|
+
return (_jsxs(Card, { className: "anlyx-scanned-hints", children: [_jsxs("div", { className: "anlyx-scanned-hints__head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Scanned / inferred hints" }), _jsx("p", { children: "These are known page links, not browser-live requests." })] }), _jsx(Badge, { tone: "gray", children: "Not browser-live" })] }), _jsx("div", { className: "anlyx-scanned-hints__list", children: hints.slice(0, 4).map((hint, index) => (_jsxs("div", { className: "anlyx-scanned-hint", children: [_jsxs("div", { children: [_jsx(Badge, { tone: hint.evidence === "capture" ? "green" : "violet", children: hint.evidence === "capture" ? "captured page link" : "scanned page link" }), _jsx("strong", { title: hint.endpointLabel ?? `${hint.method} ${hint.path}`, children: hint.endpointLabel ?? `${hint.method} ${hint.path}` })] }), _jsx("p", { title: hint.pageFilePath ?? hint.pageRoute, children: hint.pageRoute })] }, `${hint.pageRoute}:${hint.method}:${hint.path}:${index}`))) })] }));
|
|
18
|
+
}
|
|
19
|
+
function WaitingForAction() {
|
|
20
|
+
return (_jsxs(Card, { children: [_jsx("h3", { className: "anlyx-ov-section-title", children: "Waiting" }), _jsx("div", { className: "anlyx-ov-empty", children: "Use the app normally. Anlyx opens the main flow for requests caused by your direct action. Background requests stay in Recent API events until you select them." })] }));
|
|
21
|
+
}
|
|
22
|
+
function NoPrimaryRequest({ latestAction }) {
|
|
23
|
+
return (_jsxs(Card, { className: "anlyx-no-primary-card", children: [_jsxs("div", { children: [_jsx(Badge, { tone: "amber", children: "No action API yet" }), _jsx("h3", { children: formatAction(latestAction) }), _jsx("p", { children: latestAction.selector ?? "No stable selector captured" })] }), _jsx("div", { className: "anlyx-no-primary-card__note", children: "Only background session checks were detected. They are hidden from the main flow." })] }));
|
|
24
|
+
}
|
|
25
|
+
function CapturedRequest({ event }) {
|
|
26
|
+
const matched = Boolean(event.matchedEndpoint);
|
|
27
|
+
return (_jsxs(Card, { className: "anlyx-captured-request", children: [_jsxs("div", { className: "anlyx-captured-request__top", children: [_jsxs("div", { children: [_jsx("h3", { children: "Captured request" }), _jsxs("div", { className: "anlyx-captured-request__path", children: [_jsx(Badge, { tone: "blue", children: event.method }), _jsx("strong", { children: event.path })] })] }), _jsx(Badge, { tone: matched ? "green" : "amber", children: matched ? "matched" : "unmatched" })] }), _jsxs("div", { className: "anlyx-captured-request__summary", children: [_jsxs("span", { children: [_jsx("b", { children: event.matchedEndpoint ? "endpoint" : "browser" }), " ", event.matchedEndpoint
|
|
28
|
+
? `${event.matchedEndpoint.method} ${event.matchedEndpoint.path}`
|
|
29
|
+
: event.path] }), _jsx("span", { children: _jsx("b", { children: getStatusLabel(event.status) }) }), _jsxs("span", { children: [event.durationMs, "ms", event.count && event.count > 1 ? ` · seen x${event.count}` : ""] }), event.matchedEndpoint?.confidence ? (_jsxs("span", { children: ["confidence ", event.matchedEndpoint.confidence] })) : null, _jsx("span", { children: event.triggeredBy
|
|
30
|
+
? "user action"
|
|
31
|
+
: event.source === "health"
|
|
32
|
+
? "health/background"
|
|
33
|
+
: "background" })] }), _jsxs("div", { className: "anlyx-captured-request__steps", children: [_jsx(CapturedStep, { label: "Action", title: event.triggeredBy ? formatAction(event.triggeredBy) : "No user action captured", detail: event.triggeredBy?.selector ?? "Request may have fired on page load", tone: event.triggeredBy ? "blue" : "amber" }), _jsx(CapturedStep, { label: "Request", title: `${event.method} ${event.path}`, detail: matched ? "Matched scanned endpoint" : "No scanned endpoint matched", tone: matched ? "green" : "amber" }), _jsx(CapturedStep, { label: "Result", title: getStatusLabel(event.status), detail: `${event.durationMs}ms${event.count && event.count > 1 ? ` · seen x${event.count}` : ""}`, tone: Number(event.status) >= 400 ? "amber" : "green" })] })] }));
|
|
34
|
+
}
|
|
35
|
+
function CapturedStep({ label, title, detail, tone }) {
|
|
36
|
+
return (_jsxs("div", { className: `anlyx-captured-step anlyx-captured-step--${tone}`, children: [_jsx("span", { className: "anlyx-captured-step__dot" }), _jsxs("div", { children: [_jsx("p", { children: label }), _jsx("strong", { children: title }), _jsx("span", { children: detail })] })] }));
|
|
37
|
+
}
|
|
38
|
+
function formatAction(action) {
|
|
39
|
+
return `${action.type ?? "Action"} ${action.label ?? "unnamed element"}`;
|
|
40
|
+
}
|
|
41
|
+
function getStatusLabel(status) {
|
|
42
|
+
const numeric = Number(status);
|
|
43
|
+
if (numeric === 401) {
|
|
44
|
+
return "login required · 401";
|
|
45
|
+
}
|
|
46
|
+
if (numeric === 403) {
|
|
47
|
+
return "permission denied · 403";
|
|
48
|
+
}
|
|
49
|
+
if (numeric >= 500) {
|
|
50
|
+
return `server error · ${status}`;
|
|
51
|
+
}
|
|
52
|
+
if (numeric >= 400) {
|
|
53
|
+
return `client error · ${status}`;
|
|
54
|
+
}
|
|
55
|
+
if (numeric >= 200) {
|
|
56
|
+
return `success · ${status}`;
|
|
57
|
+
}
|
|
58
|
+
return `status ${status}`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EndpointFlow } from "@anlyx/core";
|
|
2
|
+
import { type Edge, type Node } from "@xyflow/react";
|
|
3
|
+
import { type AnlyxFlowNodeData } from "./AnlyxFlowNode.js";
|
|
4
|
+
export type MainFlowCanvasProps = {
|
|
5
|
+
flow: EndpointFlow | null | undefined;
|
|
6
|
+
method: string;
|
|
7
|
+
path: string;
|
|
8
|
+
status: string | number;
|
|
9
|
+
endpointConfidence?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function MainFlowCanvas({ flow, method, path, status, endpointConfidence }: MainFlowCanvasProps): JSX.Element;
|
|
12
|
+
export declare function buildDrawerFlowModel({ flow, method, path, status }: {
|
|
13
|
+
flow: EndpointFlow | null | undefined;
|
|
14
|
+
method: string;
|
|
15
|
+
path: string;
|
|
16
|
+
status: string | number;
|
|
17
|
+
}): {
|
|
18
|
+
nodes: Node<AnlyxFlowNodeData>[];
|
|
19
|
+
edges: Edge[];
|
|
20
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Background, BackgroundVariant, MarkerType, Panel, ReactFlow, useReactFlow } from "@xyflow/react";
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
import { AnlyxFlowEdge } from "./AnlyxFlowEdge.js";
|
|
5
|
+
import { AnlyxFlowNode } from "./AnlyxFlowNode.js";
|
|
6
|
+
import { Badge } from "./ui.js";
|
|
7
|
+
const nodeTypes = {
|
|
8
|
+
anlyxFlowNode: AnlyxFlowNode
|
|
9
|
+
};
|
|
10
|
+
const edgeTypes = {
|
|
11
|
+
anlyxFlowEdge: AnlyxFlowEdge
|
|
12
|
+
};
|
|
13
|
+
const NODE_X_GAP = 224;
|
|
14
|
+
const MAIN_Y = 76;
|
|
15
|
+
const AUTH_Y = 54;
|
|
16
|
+
const SUPPORT_Y = 196;
|
|
17
|
+
export function MainFlowCanvas({ flow, method, path, status, endpointConfidence }) {
|
|
18
|
+
const model = useMemo(() => buildDrawerFlowModel({ flow, method, path, status }), [flow, method, path, status]);
|
|
19
|
+
if (model.nodes.length === 0) {
|
|
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
|
+
}
|
|
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: "Live path first, scanned downstream stays muted." })] }), _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-visible request to the scanned backend flow. Muted nodes are known code paths; framework server-side fetches, such as Next.js server component data loading, require the scanned path until a server runtime bridge is enabled." })] }));
|
|
23
|
+
}
|
|
24
|
+
function ViewportControls() {
|
|
25
|
+
const { fitView, setViewport } = useReactFlow();
|
|
26
|
+
const fit = useCallback(() => {
|
|
27
|
+
void fitView({ padding: 0.18, duration: 160 });
|
|
28
|
+
}, [fitView]);
|
|
29
|
+
const reset = useCallback(() => {
|
|
30
|
+
void setViewport({ x: 20, y: 22, zoom: 0.82 }, { duration: 160 });
|
|
31
|
+
}, [setViewport]);
|
|
32
|
+
return (_jsxs(Panel, { className: "anlyx-flow-rf-controls", position: "top-right", children: [_jsx("button", { type: "button", onClick: fit, title: "Fit the full flow into view", children: "Fit view" }), _jsx("button", { type: "button", onClick: reset, title: "Reset to the default canvas position", children: "Reset view" })] }));
|
|
33
|
+
}
|
|
34
|
+
export function buildDrawerFlowModel({ flow, method, path, status }) {
|
|
35
|
+
const isAuthBlocked = Number(status) === 401 || Number(status) === 403;
|
|
36
|
+
const mainNodes = getMainNodes(flow);
|
|
37
|
+
const controller = mainNodes.find((node) => node.type === "controller");
|
|
38
|
+
const supportNodes = mainNodes.filter((item) => item.type !== "endpoint" && item.type !== "controller");
|
|
39
|
+
const nodes = [];
|
|
40
|
+
const edges = [];
|
|
41
|
+
nodes.push(createFlowNode("api", 0, MAIN_Y, {
|
|
42
|
+
kind: "api",
|
|
43
|
+
label: "API",
|
|
44
|
+
value: `${method} ${path}`,
|
|
45
|
+
badge: "high",
|
|
46
|
+
accent: "blue",
|
|
47
|
+
fullValue: `${method} ${path}`,
|
|
48
|
+
step: "01",
|
|
49
|
+
state: "taken"
|
|
50
|
+
}));
|
|
51
|
+
if (controller) {
|
|
52
|
+
nodes.push(createFlowNode("controller", NODE_X_GAP, MAIN_Y, {
|
|
53
|
+
kind: "controller",
|
|
54
|
+
label: "Controller",
|
|
55
|
+
value: compactHandlerName(controller.label),
|
|
56
|
+
sub: compactHandlerClassName(controller.label),
|
|
57
|
+
badge: controller.confidence ?? "unknown",
|
|
58
|
+
accent: "blue",
|
|
59
|
+
fullValue: controller.label,
|
|
60
|
+
step: "02",
|
|
61
|
+
state: "taken"
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
if (isAuthBlocked) {
|
|
65
|
+
const authX = controller ? NODE_X_GAP * 2 : NODE_X_GAP;
|
|
66
|
+
const resultX = controller ? NODE_X_GAP * 3 : NODE_X_GAP * 2;
|
|
67
|
+
nodes.push(createFlowNode("auth", authX, AUTH_Y, {
|
|
68
|
+
kind: "auth",
|
|
69
|
+
label: "Auth / Session",
|
|
70
|
+
value: "Auth gate inferred",
|
|
71
|
+
sub: Number(status) === 403 ? "Likely permission gate" : "Likely login gate",
|
|
72
|
+
badge: `inferred ${status}`,
|
|
73
|
+
accent: "violet",
|
|
74
|
+
fullValue: "Inferred from the browser 401/403 result, not a runtime server trace.",
|
|
75
|
+
step: controller ? "03" : "02",
|
|
76
|
+
state: "taken"
|
|
77
|
+
}));
|
|
78
|
+
nodes.push(createFlowNode("result", resultX, AUTH_Y, getResultNodeData(status, { step: controller ? "04" : "03" })));
|
|
79
|
+
supportNodes.forEach((node, index) => {
|
|
80
|
+
nodes.push(createFlowNode(`support-${index}`, authX + index * NODE_X_GAP, SUPPORT_Y, toNodeData(node, true)));
|
|
81
|
+
});
|
|
82
|
+
edges.push(createFlowEdge("api-controller", "api", controller ? "controller" : "auth", "blue", true));
|
|
83
|
+
if (controller) {
|
|
84
|
+
edges.push(createFlowEdge("controller-auth", "controller", "auth", "violet", true));
|
|
85
|
+
}
|
|
86
|
+
edges.push(createFlowEdge("auth-result", "auth", "result", "amber", true));
|
|
87
|
+
supportNodes.forEach((_, index) => {
|
|
88
|
+
edges.push(createFlowEdge(`support-${index}`, index === 0 ? (controller ? "controller" : "api") : `support-${index - 1}`, `support-${index}`, "gray", false));
|
|
89
|
+
});
|
|
90
|
+
return { nodes, edges };
|
|
91
|
+
}
|
|
92
|
+
supportNodes.forEach((node, index) => {
|
|
93
|
+
nodes.push(createFlowNode(`support-${index}`, (index + (controller ? 2 : 1)) * NODE_X_GAP, MAIN_Y, toNodeData(node)));
|
|
94
|
+
});
|
|
95
|
+
nodes.push(createFlowNode("result", (supportNodes.length + (controller ? 2 : 1)) * NODE_X_GAP, MAIN_Y, getResultNodeData(status, {
|
|
96
|
+
step: String(supportNodes.length + (controller ? 3 : 2)).padStart(2, "0")
|
|
97
|
+
})));
|
|
98
|
+
nodes.slice(0, -1).forEach((node, index) => {
|
|
99
|
+
const target = nodes[index + 1];
|
|
100
|
+
edges.push(createFlowEdge(`main-${index}`, node.id, target.id, getEdgeTone(index, target.data.kind), true));
|
|
101
|
+
});
|
|
102
|
+
return { nodes, edges };
|
|
103
|
+
}
|
|
104
|
+
function createFlowNode(id, x, y, data) {
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
type: "anlyxFlowNode",
|
|
108
|
+
position: { x, y },
|
|
109
|
+
data
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function createFlowEdge(id, source, target, tone, animated) {
|
|
113
|
+
return {
|
|
114
|
+
id: `drawer-edge-${id}`,
|
|
115
|
+
source,
|
|
116
|
+
target,
|
|
117
|
+
type: "anlyxFlowEdge",
|
|
118
|
+
animated,
|
|
119
|
+
markerEnd: {
|
|
120
|
+
type: MarkerType.ArrowClosed,
|
|
121
|
+
width: 10,
|
|
122
|
+
height: 10,
|
|
123
|
+
color: getEdgeColor(tone)
|
|
124
|
+
},
|
|
125
|
+
data: { tone }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function getResultNodeData(status, options = {}) {
|
|
129
|
+
return {
|
|
130
|
+
kind: "result",
|
|
131
|
+
label: "Result",
|
|
132
|
+
value: `${status} ${getStatusShortLabel(status)}`,
|
|
133
|
+
sub: Number(status) >= 400 ? "Request blocked" : "Request completed",
|
|
134
|
+
badge: Number(status) >= 400 ? "blocked" : "observed",
|
|
135
|
+
accent: Number(status) >= 400 ? "amber" : "green",
|
|
136
|
+
fullValue: getStatusLabel(status),
|
|
137
|
+
...(options.step ? { step: options.step } : {}),
|
|
138
|
+
state: Number(status) >= 400 ? "blocked" : "taken"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function getMainNodes(flow) {
|
|
142
|
+
if (!flow || !Array.isArray(flow.mainPath)) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const byId = new Map(flow.nodes.map((node) => [node.id, node]));
|
|
146
|
+
return flow.mainPath.map((id) => byId.get(id)).filter((node) => Boolean(node));
|
|
147
|
+
}
|
|
148
|
+
function toNodeData(node, blockedByAuth = false) {
|
|
149
|
+
const kind = getKind(node.type);
|
|
150
|
+
const sub = blockedByAuth
|
|
151
|
+
? `Scanned ${getNodeSub(kind)?.toLowerCase() ?? "step"}`
|
|
152
|
+
: getNodeSub(kind);
|
|
153
|
+
return {
|
|
154
|
+
kind,
|
|
155
|
+
label: getNodeLabel(kind),
|
|
156
|
+
value: compactHandlerName(node.label),
|
|
157
|
+
badge: blockedByAuth ? "scanned" : (node.confidence ?? "unknown"),
|
|
158
|
+
accent: blockedByAuth ? "gray" : getNodeAccent(kind),
|
|
159
|
+
fullValue: node.label,
|
|
160
|
+
state: blockedByAuth ? "scanned" : "taken",
|
|
161
|
+
...(sub ? { sub } : {})
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function getKind(type) {
|
|
165
|
+
if (type === "service") {
|
|
166
|
+
return "service";
|
|
167
|
+
}
|
|
168
|
+
if (type === "repository") {
|
|
169
|
+
return "repository";
|
|
170
|
+
}
|
|
171
|
+
if (type === "database") {
|
|
172
|
+
return "database";
|
|
173
|
+
}
|
|
174
|
+
return "service";
|
|
175
|
+
}
|
|
176
|
+
function getNodeLabel(kind) {
|
|
177
|
+
if (kind === "repository") {
|
|
178
|
+
return "Repository";
|
|
179
|
+
}
|
|
180
|
+
if (kind === "database") {
|
|
181
|
+
return "Database";
|
|
182
|
+
}
|
|
183
|
+
return "Service";
|
|
184
|
+
}
|
|
185
|
+
function getNodeSub(kind) {
|
|
186
|
+
if (kind === "repository") {
|
|
187
|
+
return "Data access";
|
|
188
|
+
}
|
|
189
|
+
if (kind === "database") {
|
|
190
|
+
return "Persistence";
|
|
191
|
+
}
|
|
192
|
+
return "Business logic";
|
|
193
|
+
}
|
|
194
|
+
function getNodeAccent(kind) {
|
|
195
|
+
if (kind === "database") {
|
|
196
|
+
return "green";
|
|
197
|
+
}
|
|
198
|
+
if (kind === "repository") {
|
|
199
|
+
return "amber";
|
|
200
|
+
}
|
|
201
|
+
return "violet";
|
|
202
|
+
}
|
|
203
|
+
function getEdgeTone(index, targetKind) {
|
|
204
|
+
if (targetKind === "auth") {
|
|
205
|
+
return "violet";
|
|
206
|
+
}
|
|
207
|
+
if (targetKind === "result") {
|
|
208
|
+
return "amber";
|
|
209
|
+
}
|
|
210
|
+
return index === 0 ? "blue" : "violet";
|
|
211
|
+
}
|
|
212
|
+
function getEdgeColor(tone) {
|
|
213
|
+
if (tone === "amber") {
|
|
214
|
+
return "#f59e0b";
|
|
215
|
+
}
|
|
216
|
+
if (tone === "violet") {
|
|
217
|
+
return "#7c3aed";
|
|
218
|
+
}
|
|
219
|
+
if (tone === "gray") {
|
|
220
|
+
return "#94a3b8";
|
|
221
|
+
}
|
|
222
|
+
return "#2563eb";
|
|
223
|
+
}
|
|
224
|
+
function compactHandlerName(label) {
|
|
225
|
+
if (!label) {
|
|
226
|
+
return "Unknown";
|
|
227
|
+
}
|
|
228
|
+
if (label.includes("#")) {
|
|
229
|
+
const [className, methodName] = label.split("#");
|
|
230
|
+
return `${compactClassName(className ?? "")}#${methodName ?? ""}`;
|
|
231
|
+
}
|
|
232
|
+
return compactClassName(label);
|
|
233
|
+
}
|
|
234
|
+
function compactHandlerClassName(label) {
|
|
235
|
+
if (!label) {
|
|
236
|
+
return "Unknown";
|
|
237
|
+
}
|
|
238
|
+
if (label.includes("#")) {
|
|
239
|
+
const [className] = label.split("#");
|
|
240
|
+
return compactClassName(className ?? "");
|
|
241
|
+
}
|
|
242
|
+
return compactClassName(label);
|
|
243
|
+
}
|
|
244
|
+
function compactClassName(label) {
|
|
245
|
+
const parts = String(label).split(".");
|
|
246
|
+
return parts[parts.length - 1] || label;
|
|
247
|
+
}
|
|
248
|
+
function getStatusShortLabel(status) {
|
|
249
|
+
const numeric = Number(status);
|
|
250
|
+
if (numeric === 401) {
|
|
251
|
+
return "Auth required";
|
|
252
|
+
}
|
|
253
|
+
if (numeric === 403) {
|
|
254
|
+
return "Permission denied";
|
|
255
|
+
}
|
|
256
|
+
if (numeric >= 500) {
|
|
257
|
+
return "Server error";
|
|
258
|
+
}
|
|
259
|
+
if (numeric >= 400) {
|
|
260
|
+
return "Client error";
|
|
261
|
+
}
|
|
262
|
+
if (numeric >= 200 && numeric < 300) {
|
|
263
|
+
return "OK";
|
|
264
|
+
}
|
|
265
|
+
return "Observed";
|
|
266
|
+
}
|
|
267
|
+
function getStatusLabel(status) {
|
|
268
|
+
const numeric = Number(status);
|
|
269
|
+
if (numeric === 401) {
|
|
270
|
+
return "login required · 401";
|
|
271
|
+
}
|
|
272
|
+
if (numeric === 403) {
|
|
273
|
+
return "permission denied · 403";
|
|
274
|
+
}
|
|
275
|
+
if (numeric >= 500) {
|
|
276
|
+
return `server error · ${status}`;
|
|
277
|
+
}
|
|
278
|
+
if (numeric >= 400) {
|
|
279
|
+
return `client error · ${status}`;
|
|
280
|
+
}
|
|
281
|
+
if (numeric >= 200 && numeric < 300) {
|
|
282
|
+
return `success · ${status}`;
|
|
283
|
+
}
|
|
284
|
+
return `status ${status}`;
|
|
285
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { Badge, Table } from "./ui.js";
|
|
4
|
+
export function RecentApiEventsTable({ events, selectedEventId }) {
|
|
5
|
+
const [filter, setFilter] = useState("action");
|
|
6
|
+
const actionEvents = useMemo(() => events.filter((event) => Boolean(event.triggeredBy)), [events]);
|
|
7
|
+
const backgroundEvents = useMemo(() => events.filter((event) => !event.triggeredBy), [events]);
|
|
8
|
+
const visibleEvents = filter === "action" ? actionEvents : filter === "background" ? backgroundEvents : events;
|
|
9
|
+
const emptyText = filter === "action"
|
|
10
|
+
? "No user-action API requests yet."
|
|
11
|
+
: filter === "background"
|
|
12
|
+
? "No background API requests observed."
|
|
13
|
+
: "No API events observed yet.";
|
|
14
|
+
return (_jsxs("section", { className: "anlyx-events-table-section", children: [_jsxs("div", { className: "anlyx-events-table-head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Recent API events" }), _jsx("p", { children: "Action requests are primary. Background traffic stays quiet until selected." })] }), _jsxs("div", { className: "anlyx-events-filter", role: "tablist", "aria-label": "Filter API events", children: [_jsxs("button", { "aria-selected": filter === "action", type: "button", onClick: () => setFilter("action"), children: ["Actions ", _jsx("span", { children: actionEvents.length })] }), _jsxs("button", { "aria-selected": filter === "background", type: "button", onClick: () => setFilter("background"), children: ["Background ", _jsx("span", { children: backgroundEvents.length })] }), _jsxs("button", { "aria-selected": filter === "all", type: "button", onClick: () => setFilter("all"), children: ["All ", _jsx("span", { children: events.length })] })] })] }), visibleEvents.length === 0 ? (_jsx("div", { className: "anlyx-ov-empty", children: emptyText })) : (_jsxs(Table, { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Method" }), _jsx("th", { children: "Path" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Match" }), _jsx("th", { children: "Source" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: visibleEvents.slice(0, 6).map((event) => (_jsxs("tr", { className: event.id === selectedEventId ? "anlyx-events-table-row--selected" : "", "data-event-id": event.id, tabIndex: 0, title: "Inspect this API event", children: [_jsx("td", { children: _jsx(Badge, { tone: "blue", children: event.method }) }), _jsx("td", { className: "anlyx-events-table-path", children: event.path }), _jsx("td", { children: _jsx(Badge, { tone: Number(event.status) >= 400 ? "amber" : "green", children: event.status }) }), _jsx("td", { children: _jsx(Badge, { tone: event.matchedEndpoint ? "green" : "amber", children: event.matchedEndpoint ? "matched" : "unmatched" }) }), _jsx("td", { children: _jsx(Badge, { tone: event.triggeredBy ? "blue" : "gray", children: event.triggeredBy
|
|
15
|
+
? "action"
|
|
16
|
+
: event.source === "health"
|
|
17
|
+
? "health"
|
|
18
|
+
: "background" }) }), _jsxs("td", { children: [event.durationMs, "ms"] })] }, event.id))) })] }))] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import "@xyflow/react/dist/style.css";
|
|
4
|
+
import "./overlay.css";
|
|
5
|
+
import { FlowDrawer } from "./FlowDrawer.js";
|
|
6
|
+
const roots = new WeakMap();
|
|
7
|
+
window.__ANLYX_RENDER_FLOW_DRAWER__ = (container, props) => {
|
|
8
|
+
const existingRoot = roots.get(container);
|
|
9
|
+
const root = existingRoot ?? createRoot(container);
|
|
10
|
+
if (!existingRoot) {
|
|
11
|
+
roots.set(container, root);
|
|
12
|
+
}
|
|
13
|
+
root.render(_jsx(FlowDrawer, { ...props }));
|
|
14
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.react-flow{--xy-edge-stroke-default:#b1b1b7;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#555;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#ffffff80;--xy-minimap-background-color-default:#fff;--xy-minimap-mask-background-color-default:#f0f0f099;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#e2e2e2;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:transparent;--xy-background-pattern-dots-color-default:#91919a;--xy-background-pattern-lines-color-default:#eee;--xy-background-pattern-cross-color-default:#e2e2e2;background-color:var(--xy-background-color,var(--xy-background-color-default));--xy-node-color-default:inherit;--xy-node-border-default:1px solid #1a192b;--xy-node-background-color-default:#fff;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #00000014;--xy-node-boxshadow-selected-default:0 0 0 .5px #1a192b;--xy-node-border-radius-default:3px;--xy-handle-background-color-default:#1a192b;--xy-handle-border-color-default:#fff;--xy-selection-background-color-default:#0059dc14;--xy-selection-border-default:1px dotted #0059dccc;--xy-controls-button-background-color-default:#fefefe;--xy-controls-button-background-color-hover-default:#f4f4f4;--xy-controls-button-color-default:inherit;--xy-controls-button-color-hover-default:inherit;--xy-controls-button-border-color-default:#eee;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#fff;--xy-edge-label-color-default:inherit;--xy-resize-background-color-default:#3367d9;direction:ltr}.react-flow.dark{--xy-edge-stroke-default:#3e3e3e;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#727272;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#96969640;--xy-minimap-background-color-default:#141414;--xy-minimap-mask-background-color-default:#3c3c3c99;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#2b2b2b;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:#141414;--xy-background-pattern-dots-color-default:#777;--xy-background-pattern-lines-color-default:#777;--xy-background-pattern-cross-color-default:#777;--xy-node-color-default:#f8f8f8;--xy-node-border-default:1px solid #3c3c3c;--xy-node-background-color-default:#1e1e1e;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #ffffff14;--xy-node-boxshadow-selected-default:0 0 0 .5px #999;--xy-handle-background-color-default:#bebebe;--xy-handle-border-color-default:#1e1e1e;--xy-selection-background-color-default:#c8c8dc14;--xy-selection-border-default:1px dotted #c8c8dccc;--xy-controls-button-background-color-default:#2b2b2b;--xy-controls-button-background-color-hover-default:#3e3e3e;--xy-controls-button-color-default:#f8f8f8;--xy-controls-button-color-hover-default:#fff;--xy-controls-button-border-color-default:#5b5b5b;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#141414;--xy-edge-label-color-default:#f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props,var(--xy-background-color,var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width,var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke,var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width,var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{pointer-events:none;position:absolute;overflow:visible}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:.5s linear infinite dashdraw}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected,var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:.5s linear infinite dashdraw}svg.react-flow__connectionline{z-index:1001;position:absolute;overflow:visible}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{-webkit-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default;position:absolute}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:0 0;pointer-events:none}.react-flow__nodesselection-rect{pointer-events:all;cursor:grab;position:absolute}.react-flow__handle{pointer-events:none;background-color:var(--xy-handle-background-color,var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color,var(--xy-handle-border-color-default));border-radius:100%;width:6px;min-width:5px;height:6px;min-height:5px;position:absolute}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;bottom:0;left:50%;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{z-index:5;margin:15px;position:absolute}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px)translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px)translateY(-50%)}.react-flow__attribution{background:var(--xy-attribution-background-color,var(--xy-attribution-background-color-default));margin:0;padding:2px 3px;font-size:10px}.react-flow__attribution a{color:#999;text-decoration:none}@keyframes dashdraw{0%{stroke-dashoffset:10px}}.react-flow__edgelabel-renderer{pointer-events:none;-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__viewport-portal{-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__minimap{background:var(--xy-minimap-background-color-props,var(--xy-minimap-background-color,var(--xy-minimap-background-color-default)))}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var(--xy-minimap-mask-background-color-props,var(--xy-minimap-mask-background-color,var(--xy-minimap-mask-background-color-default)));stroke:var(--xy-minimap-mask-stroke-color-props,var(--xy-minimap-mask-stroke-color,var(--xy-minimap-mask-stroke-color-default)));stroke-width:var(--xy-minimap-mask-stroke-width-props,var(--xy-minimap-mask-stroke-width,var(--xy-minimap-mask-stroke-width-default)))}.react-flow__minimap-node{fill:var(--xy-minimap-node-background-color-props,var(--xy-minimap-node-background-color,var(--xy-minimap-node-background-color-default)));stroke:var(--xy-minimap-node-stroke-color-props,var(--xy-minimap-node-stroke-color,var(--xy-minimap-node-stroke-color-default)));stroke-width:var(--xy-minimap-node-stroke-width-props,var(--xy-minimap-node-stroke-width,var(--xy-minimap-node-stroke-width-default)))}.react-flow__background-pattern.dots{fill:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-dots-color-default)))}.react-flow__background-pattern.lines{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-lines-color-default)))}.react-flow__background-pattern.cross{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-cross-color-default)))}.react-flow__controls{box-shadow:var(--xy-controls-box-shadow,var(--xy-controls-box-shadow-default));flex-direction:column;display:flex}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{background:var(--xy-controls-button-background-color,var(--xy-controls-button-background-color-default));border:none;border-bottom:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)));width:26px;height:26px;color:var(--xy-controls-button-color-props,var(--xy-controls-button-color,var(--xy-controls-button-color-default)));cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;padding:4px;display:flex}.react-flow__controls-button svg{fill:currentColor;width:100%;max-width:12px;max-height:12px}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{border-radius:var(--xy-node-border-radius,var(--xy-node-border-radius-default));width:150px;color:var(--xy-node-color,var(--xy-node-color-default));text-align:center;border:var(--xy-node-border,var(--xy-node-border-default));background-color:var(--xy-node-background-color,var(--xy-node-background-color-default));padding:10px;font-size:12px}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover,var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected,var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color,var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color,var(--xy-selection-background-color-default));border:var(--xy-selection-border,var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var(--xy-controls-button-background-color-hover-props,var(--xy-controls-button-background-color-hover,var(--xy-controls-button-background-color-hover-default)));color:var(--xy-controls-button-color-hover-props,var(--xy-controls-button-color-hover,var(--xy-controls-button-color-hover-default)))}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)))}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{background-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border:1px solid #fff;border-radius:1px;width:5px;height:5px;translate:-50% -50%}.react-flow__resize-control.handle.left{top:50%;left:0}.react-flow__resize-control.handle.right{top:50%;left:100%}.react-flow__resize-control.handle.top{top:0;left:50%}.react-flow__resize-control.handle.bottom{top:100%;left:50%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border-style:solid;border-width:0}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;height:100%;top:0;transform:translate(-50%)}.react-flow__resize-control.line.left{border-left-width:1px;left:0}.react-flow__resize-control.line.right{border-right-width:1px;left:100%}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{width:100%;height:1px;left:0;transform:translateY(-50%)}.react-flow__resize-control.line.top{border-top-width:1px;top:0}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color,var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color,var(--xy-edge-label-color-default))}*{box-sizing:border-box}.anlyx-flow-drawer-body{color:#0f172a;gap:9px;padding:11px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;display:grid}.anlyx-ov-card{background:#fffffff7;border:1px solid #0f172a14;border-radius:12px;overflow:hidden;box-shadow:0 1px 2px #0f172a0a}.anlyx-ov-section-title{color:#64748b;letter-spacing:.08em;text-transform:uppercase;border-bottom:1px solid #eef2f7;margin:0;padding:10px 12px;font-size:10px;font-weight:900}.anlyx-ov-empty{color:#667085;padding:18px 12px;font-size:13px;line-height:1.5}.anlyx-ov-badge{color:#475569;white-space:nowrap;background:#fff;border:1px solid #e2e8f0;border-radius:999px;justify-content:center;align-items:center;width:fit-content;padding:3px 7px;font-size:9px;font-weight:850;line-height:1;display:inline-flex}.anlyx-ov-badge--blue{color:#1d4ed8;background:#eff6ff;border-color:#bfdbfe}.anlyx-ov-badge--green{color:#087443;background:#ecfdf3;border-color:#bbf7d0}.anlyx-ov-badge--amber{color:#b45309;background:#fffbeb;border-color:#fed7aa}.anlyx-ov-badge--violet{color:#6d28d9;background:#f5f3ff;border-color:#ddd6fe}.anlyx-ov-badge--gray,.anlyx-ov-badge--neutral{color:#475569;background:#f8fafc;border-color:#e2e8f0}.anlyx-no-primary-card{background:linear-gradient(#fffbeb8a,#fffffffa),#fff;gap:10px;padding:12px;display:grid}.anlyx-no-primary-card h3{color:#101828;letter-spacing:0;margin:8px 0 3px;font-size:15px;font-weight:900}.anlyx-no-primary-card p{color:#667085;text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:12px;font-weight:700;overflow:hidden}.anlyx-no-primary-card__note{color:#7c2d12;background:#fff7ed9e;border:1px dashed #fed7aa;border-radius:10px;padding:9px 10px;font-size:12px;font-weight:750;line-height:1.45}.anlyx-scanned-hints{padding:11px 12px}.anlyx-scanned-hints__head{grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:10px;margin-bottom:10px;display:grid}.anlyx-scanned-hints__head h3{color:#101828;letter-spacing:0;margin:0 0 3px;font-size:13px;font-weight:900}.anlyx-scanned-hints__head p{color:#667085;margin:0;font-size:11px;font-weight:750;line-height:1.35}.anlyx-scanned-hints__list{gap:7px;display:grid}.anlyx-scanned-hint{background:#fbfdff;border:1px solid #e4e9f2;border-radius:10px;gap:5px;padding:8px 9px;display:grid}.anlyx-scanned-hint>div{align-items:center;gap:7px;min-width:0;display:flex}.anlyx-scanned-hint strong{color:#101828;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:900;overflow:hidden}.anlyx-scanned-hint p{color:#667085;text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:11px;font-weight:700;overflow:hidden}.anlyx-captured-request__top{grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:10px;padding:10px 12px 8px;display:grid}.anlyx-captured-request h3{color:#64748b;letter-spacing:.05em;text-transform:uppercase;margin:0 0 5px;font-size:10px;font-weight:900}.anlyx-captured-request__path{align-items:center;gap:8px;min-width:0;display:flex}.anlyx-captured-request__path strong{color:#101828;text-overflow:ellipsis;white-space:nowrap;font-size:15px;font-weight:900;line-height:1.35;overflow:hidden}.anlyx-captured-request__summary{color:#64748b;background:linear-gradient(#f8fafcdb,#fffffff0);border:1px solid #e5edf8;border-radius:10px;flex-wrap:wrap;align-items:center;gap:6px;margin:0 12px 8px;padding:7px 9px;font-size:9px;font-weight:850;line-height:1.3;display:flex}.anlyx-captured-request__summary span{align-items:center;gap:3px;min-width:0;display:inline-flex}.anlyx-captured-request__summary span:not(:last-child):after{content:"";background:#cbd5e1;border-radius:999px;width:3px;height:3px;margin-left:3px}.anlyx-captured-request__summary b{color:#334155;text-overflow:ellipsis;white-space:nowrap;font-weight:950;overflow:hidden}.anlyx-captured-request__steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;padding:7px 11px 10px;display:grid}.anlyx-captured-step{background:linear-gradient(#fff,#fbfdff);border:1px solid #e2e8f0e6;border-radius:10px;grid-template-columns:22px minmax(0,1fr);align-items:start;gap:8px;min-width:0;padding:7px;display:grid}.anlyx-captured-step__dot{background:#eff6ff;border-radius:8px;width:22px;height:22px;box-shadow:inset 0 0 0 7px #2563eb}.anlyx-captured-step--green .anlyx-captured-step__dot{background:#dcfae6;box-shadow:inset 0 0 0 7px #16a34a}.anlyx-captured-step--amber .anlyx-captured-step__dot{background:#fff7ed;box-shadow:inset 0 0 0 7px #f59e0b}.anlyx-captured-step p,.anlyx-captured-step strong,.anlyx-captured-step span{text-overflow:ellipsis;white-space:nowrap;margin:0;display:block;overflow:hidden}.anlyx-captured-step p{color:#667085;letter-spacing:.08em;text-transform:uppercase;font-size:9px;font-weight:900}.anlyx-captured-step strong{color:#101828;margin-top:2px;font-size:11px;font-weight:900}.anlyx-captured-step span{color:#667085;margin-top:2px;font-size:10px;font-weight:700}.anlyx-flow-rf-section,.anlyx-events-table-section{background:#fff;border:1px solid #0f172a14;border-radius:12px;overflow:hidden;box-shadow:0 1px 2px #0f172a0a}.anlyx-flow-rf-head,.anlyx-events-table-head{justify-content:space-between;align-items:center;gap:10px;padding:10px 12px;display:flex}.anlyx-flow-rf-head h3,.anlyx-events-table-head h3{color:#344054;margin:0;font-size:12px;font-weight:900}.anlyx-flow-rf-head p{color:#8390a4;margin:3px 0 0;font-size:9px;font-weight:750;line-height:1.35}.anlyx-flow-rf-head__badges{flex-wrap:wrap;flex-shrink:0;justify-content:flex-end;gap:5px;display:inline-flex}.anlyx-events-table-head p{color:#8390a4;margin:3px 0 0;font-size:9px;font-weight:750;line-height:1.35}.anlyx-events-filter{background:#f8fafc;border:1px solid #e2e8f0;border-radius:999px;flex-shrink:0;gap:2px;padding:2px;display:inline-flex}.anlyx-events-filter button{color:#64748b;cursor:pointer;white-space:nowrap;background:0 0;border:0;border-radius:999px;height:24px;padding:0 7px;font-size:9px;font-weight:850}.anlyx-events-filter button[aria-selected=true]{color:#1d4ed8;background:#fff;box-shadow:0 1px 2px #0f172a14}.anlyx-events-filter span{color:#94a3b8;margin-left:3px}.anlyx-flow-rf-canvas{cursor:grab;background:radial-gradient(circle at 50% 30%,#2563eb0d,#0000 34%),#fbfdff;border:1px solid #cbd5e1bd;border-radius:12px;height:318px;margin:0 12px;overflow:hidden}.anlyx-flow-rf-canvas:active{cursor:grabbing}.anlyx-flow-rf-canvas .react-flow__background{opacity:.46}.anlyx-flow-rf-canvas .react-flow__viewport{transition:transform .18s}.anlyx-flow-rf-controls{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:#ffffffe0;border:1px solid #cbd5e1cc;border-radius:9px;gap:4px;padding:3px;display:inline-flex;box-shadow:0 8px 18px #0f172a14}.anlyx-flow-rf-controls button{color:#475467;cursor:pointer;white-space:nowrap;background:0 0;border:0;border-radius:7px;height:24px;padding:0 8px;font-size:10px;font-weight:850}.anlyx-flow-rf-controls button:hover{color:#1d4ed8;background:#eff6ff}.anlyx-flow-rf-controls button:focus-visible{outline-offset:1px;outline:2px solid #2563eb61}.anlyx-flow-rf-node{background:linear-gradient(#fffffffc,#f8fafcf5);border-color:#93c5fdf2 #93c5fdf2 #93c5fdf2 #2563eb;border-left-style:solid;border-left-width:2px;grid-template-rows:auto minmax(0,1fr) auto;gap:6px;width:168px;height:116px;padding:10px 11px;display:grid;position:relative;box-shadow:0 1px #0f172a05,0 14px 30px #0f172a0f}.anlyx-flow-rf-node--violet{border-color:#ddd6fe #ddd6fe #ddd6fe #7c3aed}.anlyx-flow-rf-node--amber{background:linear-gradient(#fff,#fffaf0);border-color:#fed7aa #fed7aa #fed7aa #f59e0b}.anlyx-flow-rf-node--green{border-color:#bbf7d0 #bbf7d0 #bbf7d0 #0f766e}.anlyx-flow-rf-node--gray{border-color:#e2e8f0 #e2e8f0 #e2e8f0 #94a3b8}.anlyx-flow-rf-node--blocked{box-shadow:0 0 0 1px #f59e0b1a,0 14px 30px #f59e0b14}.anlyx-flow-rf-node--scanned{box-shadow:none;opacity:.82;background:linear-gradient(#ffffffe6,#f8fafcc7);border-style:dashed}.anlyx-flow-rf-node--scanned .anlyx-flow-rf-node__value,.anlyx-flow-rf-node--scanned .anlyx-flow-rf-node__sub{color:#64748b}.anlyx-flow-rf-node__top{align-items:center;gap:7px;min-width:0;display:flex}.anlyx-flow-rf-node__icon{color:#2563eb;background:#eff6ff;border-radius:8px;flex:none;place-items:center;width:22px;height:22px;display:inline-grid}.anlyx-flow-rf-node--violet .anlyx-flow-rf-node__icon{color:#7c3aed;background:#f5f3ff}.anlyx-flow-rf-node--amber .anlyx-flow-rf-node__icon{color:#d97706;background:#fff7ed}.anlyx-flow-rf-node--green .anlyx-flow-rf-node__icon{color:#0f766e;background:#ecfdf3}.anlyx-flow-rf-node__label{color:#475467;letter-spacing:.04em;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap;font-size:9px;font-weight:950;overflow:hidden}.anlyx-flow-rf-node__step{color:#98a2b3;text-align:right;min-width:18px;margin-left:auto;font-size:10px;font-weight:950}.anlyx-flow-rf-node__value{color:#101828;-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:0;font-size:11px;font-weight:900;line-height:1.28;display:-webkit-box;overflow:hidden}.anlyx-flow-rf-node__sub{color:#667085;text-overflow:ellipsis;white-space:nowrap;margin:-3px 0 0;font-size:9px;font-weight:700;overflow:hidden}.anlyx-flow-rf-handle{background:#93c5fd;border:2px solid #fff;width:8px;height:8px;box-shadow:0 0 0 1px #2563eb47}.anlyx-flow-rf-edge{stroke-width:2px;fill:none}.anlyx-flow-rf-edge--blue{stroke:#2563eb}.anlyx-flow-rf-edge--violet{stroke:#7c3aed}.anlyx-flow-rf-edge--amber{stroke:#f59e0b}.anlyx-flow-rf-edge--gray{stroke:#94a3b8;stroke-width:1.5px;stroke-dasharray:5 5}.react-flow__edge.animated .anlyx-flow-rf-edge{stroke-dasharray:7 7;animation:.9s linear infinite anlyx-edge-dash}@keyframes anlyx-edge-dash{to{stroke-dashoffset:-14px}}.anlyx-flow-rf-note{color:#738196;background:#f8fafc61;border-top:1px solid #e2e8f08c;margin:0;padding:8px 12px;font-size:10px;font-weight:700;line-height:1.4}.anlyx-flow-rf-empty{color:#667085;padding:18px 12px;font-size:13px}.anlyx-ov-table{border-collapse:collapse;width:100%;font-size:9px}.anlyx-ov-table th{color:#667085;letter-spacing:.07em;text-align:left;text-transform:uppercase;padding:0 10px 6px;font-size:9px;font-weight:900}.anlyx-ov-table td{color:#64748b;border-top:1px solid #f1f5f9;height:28px;padding:4px 10px;font-weight:700}.anlyx-ov-table tbody tr[data-event-id]{cursor:pointer}.anlyx-ov-table tbody tr[data-event-id]:hover{background:#2563eb09}.anlyx-ov-table tbody tr.anlyx-events-table-row--selected{background:#2563eb0f;box-shadow:inset 2px 0 #2563eb}.anlyx-ov-table tbody tr[data-event-id]:focus-visible{outline-offset:-2px;outline:2px solid #2563eb57}.anlyx-events-table-path{color:#1f2937;text-overflow:ellipsis;white-space:nowrap;max-width:340px;overflow:hidden}.anlyx-unmatched-card{background:#fffaf5;border-color:#fed7aa;padding:14px}.anlyx-unmatched-card h3{color:#9a3412;margin:0;font-size:13px;font-weight:900}.anlyx-unmatched-card p{color:#667085;margin:5px 0 0;font-size:12px;font-weight:700;line-height:1.45}@media (width<=760px){.anlyx-flow-rf-head{flex-direction:column;align-items:flex-start}.anlyx-flow-rf-head__badges{justify-content:flex-start}.anlyx-events-table-head{flex-direction:column;align-items:flex-start}.anlyx-events-filter{max-width:100%;overflow-x:auto}.anlyx-captured-request__steps{grid-template-columns:1fr}.anlyx-flow-rf-canvas{height:320px}}
|
|
2
|
+
/*$vite$:1*/
|