@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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CheckCircle2, CircleHelp, Info, TriangleAlert } from "lucide-react";
|
|
3
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
4
|
+
export function AnalysisEvidenceList({ node }) {
|
|
5
|
+
const evidence = getEvidence(node);
|
|
6
|
+
return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Analysis evidence", children: [_jsx("h3", { children: "Analysis evidence" }), _jsx("ul", { className: "anlyx-evidence-list", children: evidence.map((item, index) => {
|
|
7
|
+
const Icon = getEvidenceIcon(item.confidence ?? node.confidence ?? "unknown");
|
|
8
|
+
return (_jsxs("li", { children: [_jsx(Icon, { size: 14, strokeWidth: 2.5 }), _jsxs("span", { children: [_jsx("strong", { children: item.label }), item.detail ? _jsx("em", { children: item.detail }) : null] }), _jsx(StatusBadge, { tone: item.confidence ?? node.confidence ?? "unknown", children: item.confidence ?? node.confidence ?? "unknown" })] }, `${item.label}:${index}`));
|
|
9
|
+
}) })] }));
|
|
10
|
+
}
|
|
11
|
+
function getEvidence(node) {
|
|
12
|
+
if (node.evidence && node.evidence.length > 0) {
|
|
13
|
+
return node.evidence;
|
|
14
|
+
}
|
|
15
|
+
if (node.type === "unknown") {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
label: "Analysis stopped",
|
|
19
|
+
detail: "Anlyx could not resolve this code element from the scanned source.",
|
|
20
|
+
confidence: "unknown"
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
if (node.type === "database") {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
label: "Database table inferred",
|
|
28
|
+
detail: "Derived from repository entity metadata or entity naming fallback.",
|
|
29
|
+
confidence: node.confidence ?? "unknown"
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
if (node.type === "endpoint") {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
label: "Endpoint matched",
|
|
37
|
+
detail: "Derived from the backend adapter endpoint list.",
|
|
38
|
+
confidence: node.confidence ?? "unknown"
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
label: "Code node resolved",
|
|
45
|
+
detail: "Resolved from the scanned static flow graph.",
|
|
46
|
+
confidence: node.confidence ?? "unknown"
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function getEvidenceIcon(confidence) {
|
|
51
|
+
if (confidence === "high") {
|
|
52
|
+
return CheckCircle2;
|
|
53
|
+
}
|
|
54
|
+
if (confidence === "medium") {
|
|
55
|
+
return Info;
|
|
56
|
+
}
|
|
57
|
+
if (confidence === "low") {
|
|
58
|
+
return TriangleAlert;
|
|
59
|
+
}
|
|
60
|
+
return CircleHelp;
|
|
61
|
+
}
|
|
@@ -2,5 +2,5 @@ import type { ScanResult } from "@anlyx/core";
|
|
|
2
2
|
export type AnlyxAppShellProps = {
|
|
3
3
|
data: ScanResult;
|
|
4
4
|
};
|
|
5
|
-
export type ViewMode = "structure" | "frontend" | "process";
|
|
5
|
+
export type ViewMode = "flowStory" | "structure" | "frontend" | "process";
|
|
6
6
|
export declare function AnlyxAppShell({ data }: AnlyxAppShellProps): JSX.Element;
|
|
@@ -2,15 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { Group, Panel, Separator, usePanelRef } from "react-resizable-panels";
|
|
4
4
|
import { EndpointMapCanvas } from "./EndpointMapCanvas.js";
|
|
5
|
+
import { FlowStoryView } from "./FlowStoryView.js";
|
|
5
6
|
import { InspectorPanel } from "./InspectorPanel.js";
|
|
6
7
|
import { PageStoryboardView } from "./PageStoryboardView.js";
|
|
7
8
|
import { ProcessFlowView } from "./ProcessFlowView.js";
|
|
8
9
|
import { Sidebar } from "./Sidebar.js";
|
|
9
10
|
import { useReplayLite } from "../replay/use-replay-lite.js";
|
|
10
11
|
const STORAGE_KEYS = {
|
|
11
|
-
leftCollapsed: "anlyx:ui:leftCollapsed",
|
|
12
|
-
panelLayout: "anlyx:ui:panelLayout",
|
|
13
|
-
rightCollapsed: "anlyx:ui:rightCollapsed",
|
|
12
|
+
leftCollapsed: "anlyx:ui:v2:leftCollapsed",
|
|
13
|
+
panelLayout: "anlyx:ui:v2:panelLayout",
|
|
14
|
+
rightCollapsed: "anlyx:ui:v2:rightCollapsed",
|
|
14
15
|
selectedEndpointId: "anlyx:ui:selectedEndpointId",
|
|
15
16
|
selectedPageId: "anlyx:ui:selectedPageId"
|
|
16
17
|
};
|
|
@@ -20,7 +21,7 @@ const DEFAULT_PANEL_LAYOUT = {
|
|
|
20
21
|
right: 26
|
|
21
22
|
};
|
|
22
23
|
export function AnlyxAppShell({ data }) {
|
|
23
|
-
const [activeView, setActiveView] = useState("
|
|
24
|
+
const [activeView, setActiveView] = useState("flowStory");
|
|
24
25
|
const [selectedEndpointId, setSelectedEndpointId] = usePersistentString(STORAGE_KEYS.selectedEndpointId, selectInitialEndpointId(data));
|
|
25
26
|
const [selectedPageId, setSelectedPageId] = usePersistentString(STORAGE_KEYS.selectedPageId, data.pages[0]?.id);
|
|
26
27
|
const leftPanelRef = usePanelRef();
|
|
@@ -80,11 +81,19 @@ export function AnlyxAppShell({ data }) {
|
|
|
80
81
|
};
|
|
81
82
|
return (_jsxs("div", { className: "anlyx-shell", role: "application", "aria-label": "Anlyx application shell", children: [_jsxs(Group, { className: "anlyx-panel-group", defaultLayout: panelLayout, id: "anlyx-main-panels", orientation: "horizontal", onLayoutChanged: (layout) => writeLocalStorage(STORAGE_KEYS.panelLayout, JSON.stringify(layout)), children: [_jsx(Panel, { className: "anlyx-panel anlyx-panel--sidebar", collapsedSize: "52px", collapsible: true, defaultSize: "300px", id: "left", maxSize: "420px", minSize: "240px", panelRef: leftPanelRef, children: _jsx(Sidebar, { data: data, activeView: activeView, collapsed: leftCollapsed, selectedEndpointId: selectedEndpoint?.id, selectedPageId: selectedPage?.id, onSelectView: setActiveView, onToggleCollapsed: toggleLeftPanel, onSelectEndpoint: (endpoint) => {
|
|
82
83
|
setSelectedEndpointId(endpoint.id);
|
|
83
|
-
|
|
84
|
+
if (activeView !== "structure" && activeView !== "process") {
|
|
85
|
+
setActiveView("flowStory");
|
|
86
|
+
}
|
|
84
87
|
}, onSelectPage: (page) => {
|
|
85
88
|
setSelectedPageId(page.id);
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const linkedEndpointId = page.apiCalls.find((apiCall) => apiCall.endpointId)?.endpointId;
|
|
90
|
+
if (linkedEndpointId) {
|
|
91
|
+
setSelectedEndpointId(linkedEndpointId);
|
|
92
|
+
}
|
|
93
|
+
if (activeView !== "frontend") {
|
|
94
|
+
setActiveView("flowStory");
|
|
95
|
+
}
|
|
96
|
+
} }) }), _jsx(Separator, { "aria-label": "Resize navigation panel", className: "anlyx-resize-handle", children: _jsx("span", { "aria-hidden": "true" }) }), _jsx(Panel, { className: "anlyx-panel anlyx-panel--main", id: "center", minSize: "420px", children: _jsxs("div", { className: activeView === "process" ? "anlyx-main anlyx-main--process" : "anlyx-main", "aria-live": "polite", children: [activeView === "flowStory" ? (_jsx(FlowStoryView, { data: data, endpoint: selectedEndpoint, flow: selectedFlow, page: selectedPage, replayDisabled: replayUnavailable, replayLoop: replay.loop, replaySpeed: replaySpeed, replayState: replay.state, replaySteps: replay.steps, selectedNodeId: selectedNode?.id, onPause: replay.pause, onPlay: replay.play, onRestart: replay.restart, onSelectNode: (node) => setSelectedNodeId(node.id), onSpeedChange: setReplaySpeed, onToggleLoop: replay.toggleLoop })) : null, activeView === "structure" ? (_jsx(EndpointMapCanvas, { eyebrow: "Backend API Structure", endpoint: selectedEndpoint, flow: selectedFlow, replayState: replay.state, selectedNodeId: selectedNode?.id, toolbar: _jsx(StructureToolbar, {}), onSelectNode: (node) => setSelectedNodeId(node.id) })) : null, activeView === "frontend" ? (_jsx(PageStoryboardView, { data: data, page: selectedPage, onViewProcessFlow: setActiveView })) : null, activeView === "process" ? (_jsx(ProcessFlowView, { endpoint: selectedEndpoint, flow: selectedFlow, replayDisabled: replayUnavailable, replayLoop: replay.loop, replaySpeed: replaySpeed, replayState: replay.state, replaySteps: replay.steps, selectedNodeId: selectedNode?.id, onPause: replay.pause, onPlay: replay.play, onRestart: replay.restart, onSelectNode: (node) => setSelectedNodeId(node.id), onSpeedChange: setReplaySpeed, onToggleLoop: replay.toggleLoop, onViewStructure: () => setActiveView("structure") })) : null] }) }), _jsx(Separator, { "aria-label": "Resize inspector panel", className: "anlyx-resize-handle", children: _jsx("span", { "aria-hidden": "true" }) }), _jsx(Panel, { className: "anlyx-panel anlyx-panel--inspector", collapsedSize: "52px", collapsible: true, defaultSize: "360px", id: "right", maxSize: "520px", minSize: "300px", panelRef: rightPanelRef, children: _jsx(InspectorPanel, { activeView: activeView, collapsed: rightCollapsed, data: data, replayState: replay.state, selectedFlow: selectedFlow, selectedNode: selectedNode, selectedPage: selectedPage, onToggleCollapsed: toggleRightPanel }) })] }), _jsxs("div", { className: "anlyx-generated-at", children: ["Generated ", data.generatedAt] })] }));
|
|
88
97
|
}
|
|
89
98
|
function StructureToolbar() {
|
|
90
99
|
return (_jsxs("div", { className: "anlyx-toolbar", "aria-label": "Structure view actions", children: [_jsx("button", { className: "anlyx-toolbar-button", type: "button", children: "Fit view" }), _jsxs("select", { "aria-label": "Zoom level", className: "anlyx-toolbar-select", defaultValue: "100", children: [_jsx("option", { value: "75", children: "75%" }), _jsx("option", { value: "100", children: "100%" }), _jsx("option", { value: "125", children: "125%" })] }), _jsx("button", { className: "anlyx-toolbar-button anlyx-toolbar-button--icon", type: "button", children: "More" })] }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ApiCall } from "@anlyx/core";
|
|
1
|
+
import type { ApiCall, Endpoint } from "@anlyx/core";
|
|
2
2
|
export type ApiCallListProps = {
|
|
3
3
|
apiCalls: ApiCall[];
|
|
4
|
+
endpoints?: Endpoint[];
|
|
4
5
|
};
|
|
5
|
-
export declare function ApiCallList({ apiCalls }: ApiCallListProps): JSX.Element;
|
|
6
|
+
export declare function ApiCallList({ apiCalls, endpoints }: ApiCallListProps): JSX.Element;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StatusBadge } from "./StatusBadge.js";
|
|
3
|
-
export function ApiCallList({ apiCalls }) {
|
|
4
|
-
|
|
3
|
+
export function ApiCallList({ apiCalls, endpoints = [] }) {
|
|
4
|
+
const endpointById = new Map(endpoints.map((endpoint) => [endpoint.id, endpoint]));
|
|
5
|
+
return (_jsxs("section", { className: "anlyx-storyboard-panel", "aria-label": "API Calls", children: [_jsxs("div", { className: "anlyx-storyboard-section-heading", children: [_jsx("h2", { children: "API Calls" }), _jsx("span", { children: apiCalls.length })] }), apiCalls.length > 0 ? (_jsx("ul", { className: "anlyx-api-call-list", children: apiCalls.map((apiCall, index) => {
|
|
6
|
+
const endpoint = apiCall.endpointId ? endpointById.get(apiCall.endpointId) : undefined;
|
|
7
|
+
return (_jsxs("li", { className: "anlyx-api-call", children: [_jsxs("div", { className: "anlyx-api-call__line", children: [_jsx(StatusBadge, { tone: apiCall.method, children: apiCall.method }), _jsx("span", { className: "anlyx-api-call__path", children: apiCall.path })] }), _jsxs("div", { className: "anlyx-api-call__meta", children: [_jsx(StatusBadge, { tone: statusTone(apiCall.status), children: apiCall.status === undefined ? "unknown" : String(apiCall.status) }), _jsx(StatusBadge, { tone: apiCall.endpointId ? "success" : "unknown", children: apiCall.endpointId ? "Linked endpoint" : "Unmatched" })] }), endpoint ? (_jsxs("div", { className: "anlyx-api-call__endpoint", children: [_jsx("span", { children: "Matched endpoint" }), _jsxs("strong", { children: [endpoint.method, " ", endpoint.path] }), endpoint.controller || endpoint.handler ? (_jsx("em", { children: formatEndpointHandler(endpoint) })) : null] })) : apiCall.endpointId ? (_jsxs("div", { className: "anlyx-api-call__endpoint", children: [_jsx("span", { children: "Matched endpoint" }), _jsx("strong", { children: apiCall.endpointId })] })) : null] }, `${apiCall.method}:${apiCall.path}:${index}`));
|
|
8
|
+
}) })) : (_jsx("p", { className: "anlyx-empty-inline", children: "No API calls captured yet." }))] }));
|
|
9
|
+
}
|
|
10
|
+
function formatEndpointHandler(endpoint) {
|
|
11
|
+
if (endpoint.controller && endpoint.handler) {
|
|
12
|
+
return `${endpoint.controller}#${endpoint.handler}`;
|
|
13
|
+
}
|
|
14
|
+
return endpoint.controller ?? endpoint.handler ?? "Unknown handler";
|
|
5
15
|
}
|
|
6
16
|
function statusTone(status) {
|
|
7
17
|
if (status === undefined) {
|
|
@@ -73,7 +73,7 @@ export function EndpointMapCanvas({ endpoint, flow, selectedNodeId, title, eyebr
|
|
|
73
73
|
data: nextData
|
|
74
74
|
};
|
|
75
75
|
}) ?? [], [model, replayState?.activeEdge]);
|
|
76
|
-
return (_jsxs("main", { className: `anlyx-workspace anlyx-workspace--${variant}`, children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: eyebrow }), _jsx("h1", { children: title ?? (endpoint ? `${endpoint.method} ${endpoint.path}` : "No endpoint selected") })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [toolbar, endpoint ? (_jsx(StatusBadge, { tone: endpoint.confidence ?? "unknown", children: endpoint.confidence ?? "unknown" })) : null] })] }), _jsx("section", { className: `anlyx-endpoint-map anlyx-endpoint-map--${variant}`, role: "region", "aria-label": variant === "process" ? "Process Flow map" : "Endpoint Map", children: flow && model && model.nodes.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(FlowLegend, { variant: variant }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Endpoint map node list", children: nodes.map((node) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelectNode(node.data.node), children: ["Select node ", node.data.label] }) }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay node state", children: nodes.map((node) => (_jsx("li", { "data-replay-active": String(Boolean(node.data.isReplayActive)), "data-testid": `replay-node-${node.id}`, children: node.id }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay edge state", children: edges.map((edge) => (_jsxs("li", { "data-replay-active": String(Boolean(edge.data?.isReplayActive)), "data-testid": `replay-edge-${edge.source}-${edge.target}`, children: [edge.source, " to ", edge.target] }, edge.id))) }), _jsxs(ReactFlow, { className: "anlyx-react-flow", edges: edges, fitView: true, fitViewOptions: { padding: 0.18 }, maxZoom: 1.35, minZoom: 0.62, nodes: nodes, nodesConnectable: false, nodesDraggable: false, edgeTypes: edgeTypes, nodeTypes: nodeTypes, onNodeClick: (_, node) => onSelectNode(node.data.node), panOnScroll: true, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnScroll: false, children: [_jsx(Background, { color: "#dfe5ee", gap: 24, variant: BackgroundVariant.Dots }), _jsx(Controls, { showInteractive: false })] })] })) : (
|
|
76
|
+
return (_jsxs("main", { className: `anlyx-workspace anlyx-workspace--${variant}`, children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: eyebrow }), _jsx("h1", { children: title ?? (endpoint ? `${endpoint.method} ${endpoint.path}` : "No endpoint selected") })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [toolbar, endpoint ? (_jsx(StatusBadge, { tone: endpoint.confidence ?? "unknown", children: endpoint.confidence ?? "unknown" })) : null] })] }), _jsx("section", { className: `anlyx-endpoint-map anlyx-endpoint-map--${variant}`, role: "region", "aria-label": variant === "process" ? "Process Flow map" : "Endpoint Map", children: flow && model && model.nodes.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(FlowLegend, { variant: variant }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Endpoint map node list", children: nodes.map((node) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelectNode(node.data.node), children: ["Select node ", node.data.label] }) }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay node state", children: nodes.map((node) => (_jsx("li", { "data-replay-active": String(Boolean(node.data.isReplayActive)), "data-testid": `replay-node-${node.id}`, children: node.id }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay edge state", children: edges.map((edge) => (_jsxs("li", { "data-replay-active": String(Boolean(edge.data?.isReplayActive)), "data-testid": `replay-edge-${edge.source}-${edge.target}`, children: [edge.source, " to ", edge.target] }, edge.id))) }), _jsxs(ReactFlow, { className: "anlyx-react-flow", edges: edges, fitView: true, fitViewOptions: { padding: 0.18 }, maxZoom: 1.35, minZoom: 0.62, nodes: nodes, nodesConnectable: false, nodesDraggable: false, edgeTypes: edgeTypes, nodeTypes: nodeTypes, onNodeClick: (_, node) => onSelectNode(node.data.node), panOnScroll: true, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnScroll: false, children: [_jsx(Background, { color: "#dfe5ee", gap: 24, variant: BackgroundVariant.Dots }), _jsx(Controls, { showInteractive: false })] })] })) : (_jsxs("div", { className: "anlyx-endpoint-map-empty", role: "status", "aria-label": "Flow unavailable", children: [_jsx("span", { children: "Flow unavailable" }), _jsx("h2", { children: "No scanned flow for this endpoint yet" }), _jsx("p", { children: "Anlyx can list this endpoint, but no Controller -> Service -> Repository path was found." }), _jsx("p", { children: "Check that the backend source directory is configured, then run `anlyx scan` again." })] })) })] }));
|
|
77
77
|
}
|
|
78
78
|
function isUnitTestRuntime() {
|
|
79
79
|
return typeof process !== "undefined" && process.env.NODE_ENV === "test";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Endpoint, EndpointFlow, FlowNode, PageStoryboard, ScanResult } from "@anlyx/core";
|
|
2
|
+
import type { ReplayStep } from "../replay/build-replay-steps.js";
|
|
3
|
+
import type { ReplayLiteState } from "../replay/use-replay-lite.js";
|
|
4
|
+
export type FlowStoryViewProps = {
|
|
5
|
+
data: ScanResult;
|
|
6
|
+
endpoint: Endpoint | undefined;
|
|
7
|
+
flow: EndpointFlow | undefined;
|
|
8
|
+
page: PageStoryboard | undefined;
|
|
9
|
+
replayDisabled: boolean;
|
|
10
|
+
replayLoop: boolean;
|
|
11
|
+
replaySpeed: number;
|
|
12
|
+
replayState: ReplayLiteState;
|
|
13
|
+
replaySteps: ReplayStep[];
|
|
14
|
+
selectedNodeId: string | undefined;
|
|
15
|
+
onPause: () => void;
|
|
16
|
+
onPlay: () => void;
|
|
17
|
+
onRestart: () => void;
|
|
18
|
+
onSelectNode: (node: FlowNode) => void;
|
|
19
|
+
onSpeedChange: (speed: number) => void;
|
|
20
|
+
onToggleLoop: () => void;
|
|
21
|
+
};
|
|
22
|
+
export declare function FlowStoryView({ data, endpoint, flow, page, replayDisabled, replayLoop, replaySpeed, replayState, replaySteps, selectedNodeId, onPause, onPlay, onRestart, onSelectNode, onSpeedChange, onToggleLoop }: FlowStoryViewProps): JSX.Element;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Braces, Code2, Database, FileText, GitBranch, Globe2, Layers3, Maximize2, MonitorSmartphone, MousePointerClick, Route, ShieldCheck, Wifi } from "lucide-react";
|
|
3
|
+
import { ReplayControls } from "./ReplayControls.js";
|
|
4
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
5
|
+
export function FlowStoryView({ data, endpoint, flow, page, replayDisabled, replayLoop, replaySpeed, replayState, replaySteps, selectedNodeId, onPause, onPlay, onRestart, onSelectNode, onSpeedChange, onToggleLoop }) {
|
|
6
|
+
const storyPage = page ?? findPageForEndpoint(data, endpoint);
|
|
7
|
+
const screenshot = storyPage?.screenshots[0];
|
|
8
|
+
const stats = getFlowStoryStats(flow);
|
|
9
|
+
const replayStepLabels = buildReplayStepLabels(flow);
|
|
10
|
+
const primaryApiCall = storyPage?.apiCalls.find((apiCall) => apiCall.endpointId === endpoint?.id);
|
|
11
|
+
return (_jsxs("main", { className: "anlyx-flow-story", children: [_jsxs("header", { className: "anlyx-flow-story__header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Flow Story" }), _jsx("h1", { children: endpoint ? titleForEndpoint(endpoint) : "Application flow story" }), _jsxs("div", { className: "anlyx-flow-story__summary", "aria-label": "Flow Story summary", children: [_jsxs("span", { children: [stats.mainSteps, " main steps"] }), _jsxs("span", { children: [stats.supportCalls, " support calls"] }), _jsxs("span", { children: [stats.evidenceCount, " evidence items"] })] })] }), _jsxs("div", { className: "anlyx-flow-story__actions", children: [_jsx(StatusBadge, { tone: "neutral", children: "Replay Lite" }), _jsxs("button", { className: "anlyx-toolbar-button", type: "button", children: [_jsx(Maximize2, { size: 14, strokeWidth: 2.4 }), "Fit view"] })] })] }), _jsx(InteractionEvidenceChain, { apiCallLabel: formatObservedApiCall(primaryApiCall, endpoint), backendStepCount: stats.mainSteps, pageRoute: storyPage?.route }), _jsxs("section", { className: "anlyx-flow-story__stage", "aria-label": "Flow Story canvas", children: [_jsx(PagePreviewCard, { page: storyPage }), _jsxs("div", { className: "anlyx-flow-story__request", "aria-hidden": "true", children: [_jsx("span", { children: "Request" }), _jsx("i", {})] }), _jsx(FlowStoryDiagram, { flow: flow, replayState: replayState, selectedNodeId: selectedNodeId, onSelectNode: onSelectNode }), _jsx("div", { className: "anlyx-flow-story__response", "aria-hidden": "true", children: _jsx("span", { children: "Response" }) })] }), _jsx(ReplayControls, { disabled: replayDisabled, loop: replayLoop, speed: replaySpeed, state: replayState, stepLabels: replayStepLabels, steps: replaySteps, unavailableReason: "Replay is unavailable because this flow has no scanned main path.", onPause: onPause, onPlay: onPlay, onRestart: onRestart, onSpeedChange: onSpeedChange, onToggleLoop: onToggleLoop }), screenshot?.path ? _jsx("span", { className: "anlyx-sr-only", children: screenshot.path }) : null] }));
|
|
12
|
+
}
|
|
13
|
+
function InteractionEvidenceChain({ apiCallLabel, backendStepCount, pageRoute }) {
|
|
14
|
+
return (_jsxs("section", { className: "anlyx-interaction-chain", "aria-label": "Interaction evidence chain", children: [_jsx(EvidenceChainItem, { detail: pageRoute ?? "No page linked", icon: MousePointerClick, label: "Triggered page", tone: "page" }), _jsx(EvidenceChainItem, { detail: apiCallLabel, icon: Wifi, label: "Browser API event", tone: "api" }), _jsx(EvidenceChainItem, { detail: `${backendStepCount} steps`, icon: Route, label: "Scanned backend path", tone: "backend" }), _jsx(EvidenceChainItem, { detail: "not runtime tracing", icon: ShieldCheck, label: "Static evidence", tone: "guard" })] }));
|
|
15
|
+
}
|
|
16
|
+
function EvidenceChainItem({ detail, icon: Icon, label, tone }) {
|
|
17
|
+
return (_jsxs("article", { className: `anlyx-interaction-chain__item anlyx-interaction-chain__item--${tone}`, children: [_jsx("span", { className: "anlyx-interaction-chain__icon", "aria-hidden": "true", children: _jsx(Icon, { size: 16, strokeWidth: 2.5 }) }), _jsxs("div", { children: [_jsx("span", { children: label }), _jsx("strong", { children: detail })] })] }));
|
|
18
|
+
}
|
|
19
|
+
function PagePreviewCard({ page }) {
|
|
20
|
+
const screenshot = page?.screenshots[0];
|
|
21
|
+
const primaryApiCall = page?.apiCalls[0];
|
|
22
|
+
return (_jsxs("article", { className: "anlyx-page-preview", "aria-label": "Frontend page preview", children: [_jsxs("div", { className: "anlyx-page-preview__chrome", children: [_jsx(MonitorSmartphone, { size: 16, strokeWidth: 2.4 }), _jsx("span", { children: page?.route ?? "No page linked" })] }), _jsxs("div", { className: "anlyx-page-preview__screen", children: [_jsxs("div", { className: "anlyx-page-preview__topbar", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {}), _jsx("strong", { children: "STARBUCKS" })] }), _jsxs("div", { className: "anlyx-page-preview__hero", children: [_jsxs("div", { children: [_jsx("span", { children: screenshot?.title ?? "Captured page" }), _jsx("strong", { children: "Birthday Reward" }), _jsx("p", { children: "Enjoy a free drink or food item on your birthday." })] }), _jsxs("div", { className: "anlyx-page-preview__reward", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("strong", { children: "1" })] })] }), _jsxs("div", { className: "anlyx-page-preview__facts", children: [_jsx("span", { children: "Valid Jan 1 - Dec 31" }), _jsx("span", { children: "Once per year" })] }), _jsxs("div", { className: "anlyx-page-preview__steps", children: [_jsx("span", { children: "Show this reward in store" }), _jsx("span", { children: "Choose drink or food" }), _jsx("span", { children: "Enjoy birthday treat" })] }), _jsxs("div", { className: "anlyx-page-preview__api", children: [_jsx("strong", { children: primaryApiCall?.method ?? "GET" }), _jsx("span", { children: primaryApiCall?.path ?? "/api/..." })] })] }), _jsxs("div", { className: "anlyx-page-preview__meta", children: [_jsx(StatusBadge, { tone: page?.captureStatus ?? "pending", children: page?.captureStatus ?? "pending" }), _jsxs("span", { children: [page?.apiCalls.length ?? 0, " API calls"] }), _jsxs("span", { children: [page?.screenshots.length ?? 0, " screenshots"] })] })] }));
|
|
23
|
+
}
|
|
24
|
+
function FlowStoryDiagram({ flow, replayState, selectedNodeId, onSelectNode }) {
|
|
25
|
+
if (!flow) {
|
|
26
|
+
return (_jsx("div", { className: "anlyx-flow-story__diagram anlyx-flow-story__diagram--empty", children: _jsx("p", { children: "No scanned backend flow available for this endpoint." }) }));
|
|
27
|
+
}
|
|
28
|
+
const nodesById = new Map(flow.nodes.map((node) => [node.id, node]));
|
|
29
|
+
const mainNodes = flow.mainPath
|
|
30
|
+
.map((nodeId) => nodesById.get(nodeId))
|
|
31
|
+
.filter((node) => Boolean(node))
|
|
32
|
+
.filter((node) => node.type !== "page");
|
|
33
|
+
const supportingNodes = flow.subFlows.flatMap((subFlow) => subFlow.nodes);
|
|
34
|
+
return (_jsxs("div", { className: "anlyx-flow-story__diagram", role: "region", "aria-label": "Flow Story path", children: [_jsxs("div", { className: "anlyx-flow-story__diagram-head", children: [_jsxs("div", { children: [_jsx("span", { children: "Main path" }), _jsxs("strong", { children: [mainNodes.length, " steps"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Support" }), _jsxs("strong", { children: [supportingNodes.length, " calls"] })] })] }), _jsx("div", { className: "anlyx-flow-story__lane", "aria-label": "Main request path", children: mainNodes.map((node, index) => (_jsx(FlowStoryStep, { isActive: replayState.activeNodeId === node.id, isSelected: selectedNodeId === node.id, node: node, stepNumber: index + 1, showArrow: index < mainNodes.length - 1, onSelectNode: onSelectNode }, node.id))) }), supportingNodes.length > 0 ? (_jsxs("div", { className: "anlyx-flow-story__support", children: [_jsxs("div", { className: "anlyx-flow-story__support-heading", children: [_jsx(GitBranch, { size: 15, strokeWidth: 2.5 }), _jsx("span", { children: "Support calls from service" })] }), _jsx("div", { className: "anlyx-flow-story__support-grid", children: supportingNodes.map((node) => (_jsx(FlowStorySupportNode, { isActive: replayState.activeNodeId === node.id, isSelected: selectedNodeId === node.id, node: node, onSelectNode: onSelectNode }, node.id))) })] })) : null] }));
|
|
35
|
+
}
|
|
36
|
+
function FlowStoryStep({ node, isActive, isSelected, stepNumber, showArrow, onSelectNode }) {
|
|
37
|
+
const Icon = getFlowStoryIcon(node.type);
|
|
38
|
+
return (_jsxs("div", { className: "anlyx-flow-story__step-wrap", children: [_jsxs("button", { className: [
|
|
39
|
+
"anlyx-flow-story__step",
|
|
40
|
+
`anlyx-flow-story__step--${node.type}`,
|
|
41
|
+
isActive ? "anlyx-flow-story__step--active" : "",
|
|
42
|
+
isSelected ? "anlyx-flow-story__step--selected" : ""
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join(" "), type: "button", onClick: () => onSelectNode(node), "aria-label": `Select ${node.type} step ${stepNumber}: ${node.label}`, children: [_jsx("span", { className: "anlyx-flow-story__step-number", children: String(stepNumber).padStart(2, "0") }), _jsx("span", { className: "anlyx-flow-story__step-icon", "aria-hidden": "true", children: _jsx(Icon, { size: 16, strokeWidth: 2.5 }) }), _jsx("span", { className: "anlyx-flow-story__step-type", children: node.type }), _jsx("strong", { children: node.label }), _jsx(StatusBadge, { tone: node.confidence ?? "unknown", label: "confidence", children: node.confidence ?? "unknown" })] }), showArrow ? _jsx("span", { className: "anlyx-flow-story__arrow", "aria-hidden": "true" }) : null] }));
|
|
46
|
+
}
|
|
47
|
+
function FlowStorySupportNode({ node, isActive, isSelected, onSelectNode }) {
|
|
48
|
+
const Icon = getFlowStoryIcon(node.type);
|
|
49
|
+
return (_jsxs("button", { className: [
|
|
50
|
+
"anlyx-flow-story__support-node",
|
|
51
|
+
isActive ? "anlyx-flow-story__support-node--active" : "",
|
|
52
|
+
isSelected ? "anlyx-flow-story__support-node--selected" : ""
|
|
53
|
+
]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join(" "), type: "button", onClick: () => onSelectNode(node), "aria-label": `Select support call ${node.label}`, children: [_jsx(Icon, { size: 15, strokeWidth: 2.5 }), _jsx("span", { children: node.type }), _jsx("strong", { children: node.label }), _jsx(StatusBadge, { tone: node.confidence ?? "unknown", label: "confidence", children: node.confidence ?? "unknown" })] }));
|
|
56
|
+
}
|
|
57
|
+
function findPageForEndpoint(data, endpoint) {
|
|
58
|
+
if (!endpoint) {
|
|
59
|
+
return data.pages[0];
|
|
60
|
+
}
|
|
61
|
+
return (data.pages.find((page) => page.apiCalls.some((apiCall) => apiCall.endpointId === endpoint.id)) ?? data.pages[0]);
|
|
62
|
+
}
|
|
63
|
+
function titleForEndpoint(endpoint) {
|
|
64
|
+
const handler = endpoint.controller && endpoint.handler
|
|
65
|
+
? `${endpoint.controller}#${endpoint.handler}`
|
|
66
|
+
: undefined;
|
|
67
|
+
return handler ?? `${endpoint.method} ${endpoint.path}`;
|
|
68
|
+
}
|
|
69
|
+
function formatObservedApiCall(apiCall, endpoint) {
|
|
70
|
+
if (apiCall) {
|
|
71
|
+
return `${apiCall.method} ${apiCall.path}`;
|
|
72
|
+
}
|
|
73
|
+
if (endpoint) {
|
|
74
|
+
return `${endpoint.method} ${endpoint.path}`;
|
|
75
|
+
}
|
|
76
|
+
return "No API event matched";
|
|
77
|
+
}
|
|
78
|
+
function getFlowStoryStats(flow) {
|
|
79
|
+
if (!flow) {
|
|
80
|
+
return { evidenceCount: 0, mainSteps: 0, supportCalls: 0 };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
mainSteps: flow.mainPath.filter((nodeId) => !nodeId.startsWith("page:")).length,
|
|
84
|
+
supportCalls: flow.subFlows.reduce((count, subFlow) => count + subFlow.nodes.length, 0),
|
|
85
|
+
evidenceCount: flow.nodes.reduce((count, node) => count + (node.evidence?.length ?? 0), 0) +
|
|
86
|
+
flow.edges.reduce((count, edge) => count + (edge.evidence?.length ?? 0), 0) +
|
|
87
|
+
flow.subFlows.reduce((count, subFlow) => count +
|
|
88
|
+
subFlow.nodes.reduce((nodeCount, node) => nodeCount + (node.evidence?.length ?? 0), 0) +
|
|
89
|
+
subFlow.edges.reduce((edgeCount, edge) => edgeCount + (edge.evidence?.length ?? 0), 0), 0)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function buildReplayStepLabels(flow) {
|
|
93
|
+
if (!flow) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
return Object.fromEntries(flow.nodes.map((node) => [node.id, node.label]));
|
|
97
|
+
}
|
|
98
|
+
function getFlowStoryIcon(type) {
|
|
99
|
+
switch (type) {
|
|
100
|
+
case "endpoint":
|
|
101
|
+
return Globe2;
|
|
102
|
+
case "controller":
|
|
103
|
+
return Code2;
|
|
104
|
+
case "service":
|
|
105
|
+
return Layers3;
|
|
106
|
+
case "repository":
|
|
107
|
+
return Braces;
|
|
108
|
+
case "database":
|
|
109
|
+
return Database;
|
|
110
|
+
case "mapper":
|
|
111
|
+
case "validator":
|
|
112
|
+
case "utility":
|
|
113
|
+
return GitBranch;
|
|
114
|
+
default:
|
|
115
|
+
return FileText;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { EndpointFlow, FlowNode, PageStoryboard, ScanResult } from "@anlyx/
|
|
|
2
2
|
import type { ReplayLiteState } from "../replay/use-replay-lite.js";
|
|
3
3
|
type InspectorPanelProps = {
|
|
4
4
|
data: ScanResult;
|
|
5
|
-
activeView: "structure" | "frontend" | "process";
|
|
5
|
+
activeView: "flowStory" | "structure" | "frontend" | "process";
|
|
6
6
|
collapsed: boolean;
|
|
7
7
|
selectedFlow: EndpointFlow | undefined;
|
|
8
8
|
selectedNode: FlowNode | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AnalysisEvidenceList } from "./AnalysisEvidenceList.js";
|
|
2
3
|
import { StatusBadge } from "./StatusBadge.js";
|
|
3
4
|
export function InspectorPanel({ data, activeView, collapsed, selectedFlow, selectedNode, selectedPage, replayState, onToggleCollapsed }) {
|
|
4
5
|
if (collapsed) {
|
|
@@ -8,7 +9,8 @@ export function InspectorPanel({ data, activeView, collapsed, selectedFlow, sele
|
|
|
8
9
|
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: "Frontend Page" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse inspector panel", onClick: onToggleCollapsed, children: "Collapse" })] }), selectedPage ? (_jsxs("div", { className: "anlyx-inspector-stack", children: [_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Details", children: [_jsx("h3", { children: "Details" }), _jsx(Field, { label: "Route", value: selectedPage.route }), _jsx(Field, { label: "File path", value: selectedPage.filePath ?? "Manual or unknown" }), _jsxs("div", { className: "anlyx-field", children: [_jsx("span", { className: "anlyx-field__label", children: "Capture status" }), _jsx(StatusBadge, { tone: selectedPage.captureStatus, children: selectedPage.captureStatus })] }), _jsx(Field, { label: "Screenshots", value: String(selectedPage.screenshots.length) }), _jsx(Field, { label: "API calls", value: String(selectedPage.apiCalls.length) })] }), selectedPage.errorMessage ? (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Capture error", children: [_jsx("h3", { children: "Capture error" }), _jsx("p", { children: selectedPage.errorMessage })] })) : null] })) : (_jsx("p", { className: "anlyx-empty", children: "No page selected" }))] }));
|
|
9
10
|
}
|
|
10
11
|
const linkedPages = selectedNode ? findLinkedPages(data, selectedNode.id) : [];
|
|
11
|
-
|
|
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" }))] }));
|
|
12
14
|
}
|
|
13
15
|
function Field({ label, value }) {
|
|
14
16
|
return (_jsxs("div", { className: "anlyx-field", children: [_jsx("span", { className: "anlyx-field__label", children: label }), _jsx("span", { className: "anlyx-field__value", children: value })] }));
|
|
@@ -31,3 +33,20 @@ function findLinkedPages(data, nodeId) {
|
|
|
31
33
|
function findDatabaseLabel(flow) {
|
|
32
34
|
return flow?.nodes.find((node) => node.type === "database")?.label;
|
|
33
35
|
}
|
|
36
|
+
function findCalls(flow, nodeId) {
|
|
37
|
+
if (!flow) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const allNodes = [...flow.nodes, ...flow.subFlows.flatMap((subFlow) => subFlow.nodes)];
|
|
41
|
+
const allEdges = [...flow.edges, ...flow.subFlows.flatMap((subFlow) => subFlow.edges)];
|
|
42
|
+
return allEdges
|
|
43
|
+
.filter((edge) => edge.from === nodeId)
|
|
44
|
+
.map((edge) => {
|
|
45
|
+
const target = allNodes.find((node) => node.id === edge.to);
|
|
46
|
+
return {
|
|
47
|
+
id: edge.id,
|
|
48
|
+
label: target?.label ?? edge.to,
|
|
49
|
+
confidence: edge.confidence ?? "unknown"
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -7,5 +7,13 @@ export function PageStoryboardView({ data, page, onViewProcessFlow }) {
|
|
|
7
7
|
const linkedEndpoints = page
|
|
8
8
|
? data.endpoints.filter((endpoint) => page.apiCalls.some((apiCall) => apiCall.endpointId === endpoint.id))
|
|
9
9
|
: [];
|
|
10
|
-
|
|
10
|
+
const linkedApiCallCount = page?.apiCalls.filter((apiCall) => apiCall.endpointId).length ?? 0;
|
|
11
|
+
const unmatchedApiCallCount = page ? page.apiCalls.length - linkedApiCallCount : 0;
|
|
12
|
+
return (_jsxs("main", { className: "anlyx-workspace", children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Connected Frontend" }), _jsx("h1", { children: page ? page.route : "Page Storyboard" })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [_jsx("button", { className: "anlyx-toolbar-button", type: "button", disabled: linkedEndpoints.length === 0, onClick: () => onViewProcessFlow("process"), children: "View Process Flow" }), page ? _jsx(StatusBadge, { tone: page.captureStatus, children: page.captureStatus }) : null] })] }), _jsx("section", { className: "anlyx-page-storyboard", role: "region", "aria-label": "Page Storyboard", children: page ? (_jsxs(_Fragment, { children: [_jsxs("section", { className: "anlyx-page-summary", "aria-label": "Selected page summary", children: [_jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Route" }), _jsx("h2", { children: page.route })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "File" }), _jsx("p", { children: page.filePath ?? "Unknown" })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Status" }), _jsx(StatusBadge, { tone: page.captureStatus, children: page.captureStatus })] }), _jsxs("div", { children: [_jsx("span", { className: "anlyx-field__label", children: "Linked endpoints" }), _jsx("p", { children: linkedEndpoints.length })] })] }), _jsx(CaptureStatusEmptyState, { status: page.captureStatus, ...(page.errorMessage ? { reason: page.errorMessage } : {}) }), _jsx(PageEvidenceBoard, { linkedApiCallCount: linkedApiCallCount, linkedEndpointCount: linkedEndpoints.length, page: page, unmatchedApiCallCount: unmatchedApiCallCount }), _jsxs("div", { className: "anlyx-storyboard-grid", children: [_jsx(PageStoryboardCard, { page: page }), _jsxs("div", { className: "anlyx-storyboard-side", children: [_jsx(ApiCallList, { apiCalls: page.apiCalls, endpoints: data.endpoints }), _jsxs("section", { className: "anlyx-storyboard-panel", "aria-label": "Page to endpoint relationship", children: [_jsxs("div", { className: "anlyx-storyboard-section-heading", children: [_jsx("h2", { children: "Page to Endpoint" }), _jsx("span", { children: linkedEndpoints.length })] }), linkedEndpoints.length > 0 ? (_jsxs("div", { className: "anlyx-relationship-diagram", children: [_jsxs("div", { className: "anlyx-relationship-source", children: [_jsx("strong", { children: page.route }), _jsx("span", { children: "Frontend Page" })] }), _jsx("ul", { className: "anlyx-relationship-list", children: linkedEndpoints.map((endpoint) => (_jsxs("li", { children: [_jsx("span", { className: "anlyx-relationship-line", "aria-hidden": "true" }), _jsx(StatusBadge, { tone: endpoint.method, children: endpoint.method }), _jsx("span", { children: endpoint.path })] }, endpoint.id))) })] })) : (_jsx("p", { className: "anlyx-empty-inline", children: "No backend endpoint was linked during capture." }))] })] })] })] })) : (_jsx("div", { className: "anlyx-storyboard-empty", children: _jsx("p", { children: "No pages available yet." }) })) })] }));
|
|
13
|
+
}
|
|
14
|
+
function PageEvidenceBoard({ linkedApiCallCount, linkedEndpointCount, page, unmatchedApiCallCount }) {
|
|
15
|
+
return (_jsxs("section", { className: "anlyx-page-evidence-board", role: "region", "aria-label": "Page execution evidence", children: [_jsxs("div", { className: "anlyx-page-evidence-board__intro", children: [_jsx("span", { children: "Capture proof" }), _jsx("strong", { children: page.captureStatus === "success" ? "Observed page run" : "Incomplete page run" })] }), _jsx(EvidenceMetric, { label: "Screenshots", value: String(page.screenshots.length), detail: page.screenshots.length > 0 ? "visual segments captured" : "waiting for capture" }), _jsx(EvidenceMetric, { label: "API evidence", value: String(page.apiCalls.length), detail: `${linkedApiCallCount} linked` }), _jsx(EvidenceMetric, { label: "Backend linkage", value: String(linkedEndpointCount), detail: `${unmatchedApiCallCount} unmatched` })] }));
|
|
16
|
+
}
|
|
17
|
+
function EvidenceMetric({ detail, label, value }) {
|
|
18
|
+
return (_jsxs("div", { className: "anlyx-page-evidence-metric", children: [_jsx("span", { children: label }), _jsx("strong", { children: value }), _jsx("em", { children: detail })] }));
|
|
11
19
|
}
|
|
@@ -3,7 +3,14 @@ import { EndpointMapCanvas } from "./EndpointMapCanvas.js";
|
|
|
3
3
|
import { ProcessTimeline } from "./ProcessTimeline.js";
|
|
4
4
|
import { ReplayControls } from "./ReplayControls.js";
|
|
5
5
|
export function ProcessFlowView({ endpoint, flow, replayState, replaySteps, replayLoop, replayDisabled, replaySpeed, selectedNodeId, onSelectNode, onPause, onPlay, onRestart, onToggleLoop, onSpeedChange, onViewStructure }) {
|
|
6
|
-
|
|
6
|
+
const replayStepLabels = buildReplayStepLabels(flow);
|
|
7
|
+
return (_jsxs("div", { className: "anlyx-process-view", children: [_jsx(ReplayControls, { disabled: replayDisabled, loop: replayLoop, speed: replaySpeed, state: replayState, stepLabels: replayStepLabels, steps: replaySteps, unavailableReason: "Process Flow is unavailable because this endpoint has no scanned main path.", onPause: onPause, onPlay: onPlay, onRestart: onRestart, onSpeedChange: onSpeedChange, onToggleLoop: onToggleLoop }), _jsx(EndpointMapCanvas, { eyebrow: "Request Process Flow", endpoint: endpoint, flow: flow, replayState: replayState, selectedNodeId: selectedNodeId, title: endpoint
|
|
7
8
|
? `${endpoint.method} ${endpoint.path}`
|
|
8
9
|
: "Request process flow from scanned graph", toolbar: _jsx("button", { className: "anlyx-toolbar-button", type: "button", onClick: onViewStructure, children: "View on Structure" }), variant: "process", onSelectNode: onSelectNode }), _jsx(ProcessTimeline, { flow: flow, state: replayState, steps: replaySteps })] }));
|
|
9
10
|
}
|
|
11
|
+
function buildReplayStepLabels(flow) {
|
|
12
|
+
if (!flow) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
return Object.fromEntries(flow.nodes.map((node) => [node.id, node.label]));
|
|
16
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { ReplayStep } from "../replay/build-replay-steps.js";
|
|
|
3
3
|
export type ReplayControlsProps = {
|
|
4
4
|
state: ReplayLiteState;
|
|
5
5
|
steps: ReplayStep[];
|
|
6
|
+
stepLabels?: Record<string, string>;
|
|
6
7
|
loop: boolean;
|
|
7
8
|
disabled?: boolean;
|
|
8
9
|
speed: number;
|
|
@@ -13,4 +14,4 @@ export type ReplayControlsProps = {
|
|
|
13
14
|
onToggleLoop: () => void;
|
|
14
15
|
onSpeedChange: (speed: number) => void;
|
|
15
16
|
};
|
|
16
|
-
export declare function ReplayControls({ state, steps, loop, speed, disabled, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }: ReplayControlsProps): JSX.Element;
|
|
17
|
+
export declare function ReplayControls({ state, steps, stepLabels, loop, speed, disabled, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }: ReplayControlsProps): JSX.Element;
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Pause, Play, RefreshCw, Repeat2 } from "lucide-react";
|
|
3
|
-
export function ReplayControls({ state, steps, loop, speed, disabled = false, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }) {
|
|
3
|
+
export function ReplayControls({ state, steps, stepLabels = {}, loop, speed, disabled = false, unavailableReason, onPlay, onPause, onRestart, onToggleLoop, onSpeedChange }) {
|
|
4
4
|
const currentStepNumber = state.phase === "idle" ? 0 : Math.min(state.currentStepIndex + 1, steps.length);
|
|
5
|
-
|
|
5
|
+
const currentStep = state.phase === "idle" || state.phase === "complete"
|
|
6
|
+
? undefined
|
|
7
|
+
: steps[state.currentStepIndex];
|
|
8
|
+
const focusLabel = currentStep ? formatStepNode(currentStep.nodeId, stepLabels) : "Ready";
|
|
9
|
+
const edgeLabel = formatStepEdge(currentStep, stepLabels);
|
|
10
|
+
const phaseLabel = formatReplayPhase(state.phase);
|
|
11
|
+
return (_jsxs("section", { className: "anlyx-replay", "aria-label": "Process Flow controls", children: [_jsxs("div", { className: "anlyx-replay__top", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Replay from scanned flow graph" }), _jsxs("div", { className: "anlyx-replay__buttons", "aria-label": "Process Flow actions", children: [_jsxs("button", { className: "anlyx-replay__button--primary", type: "button", disabled: disabled || state.isPlaying, onClick: onPlay, children: [_jsx(Play, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Play" })] }), _jsxs("button", { type: "button", disabled: disabled || !state.isPlaying, onClick: onPause, children: [_jsx(Pause, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Pause" })] }), _jsxs("button", { type: "button", disabled: disabled, onClick: onRestart, children: [_jsx(RefreshCw, { size: 14, strokeWidth: 2.5 }), _jsx("span", { children: "Restart" })] }), _jsxs("button", { type: "button", "aria-pressed": loop, disabled: disabled, onClick: onToggleLoop, children: [_jsx(Repeat2, { size: 14, strokeWidth: 2.5 }), _jsxs("span", { children: ["Loop ", loop ? "on" : "off"] })] })] })] }), _jsxs("div", { className: "anlyx-replay__state", children: [_jsxs("span", { children: ["Step ", currentStepNumber, "/", steps.length] }), _jsxs("span", { children: ["Phase: ", state.phase] }), _jsxs("label", { children: ["Speed", _jsxs("select", { "aria-label": "Replay speed", disabled: disabled, value: speed, onChange: (event) => onSpeedChange(Number(event.currentTarget.value)), children: [_jsx("option", { value: 1100, children: "0.75x" }), _jsx("option", { value: 800, children: "1x" }), _jsx("option", { value: 520, children: "1.5x" })] })] }), _jsx("span", { children: "Main Flow only" })] })] }), _jsxs("div", { className: "anlyx-replay__focus", role: "group", "aria-label": "Replay focus", children: [_jsxs("div", { children: [_jsx("span", { className: "anlyx-replay__phase", children: phaseLabel }), _jsx("strong", { children: focusLabel }), _jsx("p", { children: edgeLabel })] }), _jsx("ol", { className: "anlyx-replay__rail", "aria-label": "Replay step rail", children: steps.map((step, index) => (_jsx("li", { className: index === state.currentStepIndex ? "anlyx-replay__rail-step--active" : "", "aria-current": index === state.currentStepIndex ? "step" : undefined, title: `${formatReplayPhase(step.phase)}: ${formatStepNode(step.nodeId, stepLabels)}`, children: _jsx("span", { children: String(index + 1).padStart(2, "0") }) }, `${step.phase}:${step.nodeId}:${step.index}`))) })] }), disabled && unavailableReason ? (_jsx("p", { className: "anlyx-replay__empty", children: unavailableReason })) : null] }));
|
|
12
|
+
}
|
|
13
|
+
function formatReplayPhase(phase) {
|
|
14
|
+
switch (phase) {
|
|
15
|
+
case "request":
|
|
16
|
+
return "Request travel";
|
|
17
|
+
case "response":
|
|
18
|
+
return "Response return";
|
|
19
|
+
case "complete":
|
|
20
|
+
return "Response delivered";
|
|
21
|
+
case "idle":
|
|
22
|
+
return "Ready to replay";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function formatStepNode(nodeId, labels) {
|
|
26
|
+
return labels[nodeId] ?? nodeId;
|
|
27
|
+
}
|
|
28
|
+
function formatStepEdge(step, labels) {
|
|
29
|
+
if (!step?.fromNodeId || !step.toNodeId) {
|
|
30
|
+
return "Waiting at the first visible node in the scanned main path.";
|
|
31
|
+
}
|
|
32
|
+
return `${formatStepNode(step.fromNodeId, labels)} -> ${formatStepNode(step.toNodeId, labels)}`;
|
|
6
33
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Endpoint, PageStoryboard, ScanResult } from "@anlyx/core";
|
|
2
2
|
type SidebarProps = {
|
|
3
3
|
data: ScanResult;
|
|
4
|
-
activeView: "structure" | "frontend" | "process";
|
|
4
|
+
activeView: "flowStory" | "structure" | "frontend" | "process";
|
|
5
5
|
collapsed: boolean;
|
|
6
6
|
selectedEndpointId: string | undefined;
|
|
7
7
|
selectedPageId: string | undefined;
|
|
8
|
-
onSelectView: (view: "structure" | "frontend" | "process") => void;
|
|
8
|
+
onSelectView: (view: "flowStory" | "structure" | "frontend" | "process") => void;
|
|
9
9
|
onToggleCollapsed: () => void;
|
|
10
10
|
onSelectEndpoint: (endpoint: Endpoint) => void;
|
|
11
11
|
onSelectPage: (page: PageStoryboard) => void;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { PanelLeftClose, PanelLeftOpen, Search } from "lucide-react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, PanelLeftClose, PanelLeftOpen, Search } from "lucide-react";
|
|
3
3
|
import { EndpointList } from "./EndpointList.js";
|
|
4
4
|
import { PageList } from "./PageList.js";
|
|
5
5
|
export function Sidebar({ data, activeView, collapsed, selectedEndpointId, selectedPageId, onSelectView, onToggleCollapsed, onSelectEndpoint, onSelectPage }) {
|
|
6
6
|
if (collapsed) {
|
|
7
7
|
return (_jsxs("aside", { className: "anlyx-sidebar anlyx-sidebar--collapsed", "aria-label": "Primary navigation", children: [_jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Expand navigation panel", onClick: onToggleCollapsed, children: _jsx(PanelLeftOpen, { size: 15, strokeWidth: 2.4 }) }), _jsx("span", { className: "anlyx-collapsed-label", children: "Nav" })] }));
|
|
8
8
|
}
|
|
9
|
-
return (_jsxs("aside", { className: "anlyx-sidebar", "aria-label": "Primary navigation", children: [_jsxs("div", { className: "anlyx-brand", children: [_jsx("div", { className: "anlyx-brand__mark", "aria-hidden": "true", children: "A" }), _jsxs("div", { children: [_jsx("div", { className: "anlyx-brand__name", children: "Anlyx" }), _jsx("div", { className: "anlyx-brand__project", children:
|
|
9
|
+
return (_jsxs("aside", { className: "anlyx-sidebar", "aria-label": "Primary navigation", children: [_jsxs("div", { className: "anlyx-brand", children: [_jsx("div", { className: "anlyx-brand__mark", "aria-hidden": "true", children: "A" }), _jsxs("div", { children: [_jsx("div", { className: "anlyx-brand__name", children: "Anlyx" }), _jsx("div", { className: "anlyx-brand__project", children: "Interaction flow map" })] }), _jsx("button", { className: "anlyx-panel-toggle", type: "button", "aria-label": "Collapse navigation panel", onClick: onToggleCollapsed, children: _jsx(PanelLeftClose, { size: 15, strokeWidth: 2.4 }) })] }), _jsxs("button", { className: "anlyx-project-select", type: "button", "aria-label": `Project ${data.projectName}`, children: [_jsx(Box, { size: 15, strokeWidth: 2.4 }), _jsx("span", { children: data.projectName })] }), _jsxs("nav", { className: "anlyx-tabs", "aria-label": "Views", children: [_jsx("button", { className: activeView === "flowStory" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("flowStory"), children: "Flow Story" }), _jsx("button", { className: activeView === "structure" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("structure"), children: "Structure" }), _jsx("button", { className: activeView === "frontend" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("frontend"), children: "Captures" }), _jsx("button", { className: activeView === "process" ? "anlyx-tab anlyx-tab--active" : "anlyx-tab", type: "button", onClick: () => onSelectView("process"), children: "Process" })] }), _jsxs("label", { className: "anlyx-search", children: [_jsx("span", { className: "anlyx-search__label", children: "Search" }), _jsxs("span", { className: "anlyx-search__control", children: [_jsx(Search, { size: 14, strokeWidth: 2.4 }), _jsx("input", { placeholder: "Search pages, endpoints, or services", type: "search" })] })] }), _jsxs("div", { className: "anlyx-sidebar__list-region", children: [_jsx(PageList, { pages: data.pages, selectedPageId: selectedPageId, onSelectPage: onSelectPage }), _jsx(EndpointList, { endpoints: data.endpoints, selectedEndpointId: selectedEndpointId, onSelectEndpoint: onSelectEndpoint }), _jsx(BackendServiceList, { data: data })] })] }));
|
|
10
|
+
}
|
|
11
|
+
function BackendServiceList({ data }) {
|
|
12
|
+
const services = [
|
|
13
|
+
...new Map(data.flows
|
|
14
|
+
.flatMap((flow) => [...flow.nodes, ...flow.subFlows.flatMap((subFlow) => subFlow.nodes)])
|
|
15
|
+
.filter((node) => ["service", "repository", "mapper", "validator", "utility"].includes(node.type))
|
|
16
|
+
.map((node) => [node.id, node])).values()
|
|
17
|
+
];
|
|
18
|
+
if (services.length === 0) {
|
|
19
|
+
return _jsx(_Fragment, {});
|
|
20
|
+
}
|
|
21
|
+
return (_jsxs("section", { className: "anlyx-sidebar-section", "aria-labelledby": "anlyx-services-heading", children: [_jsx("div", { className: "anlyx-section-heading", id: "anlyx-services-heading", children: "Backend Services" }), _jsx("ul", { className: "anlyx-list anlyx-list--compact", "aria-label": "Backend service list", children: services.map((node) => (_jsxs("li", { className: "anlyx-service-row", children: [_jsx(Box, { size: 14, strokeWidth: 2.3 }), _jsx("span", { children: node.label })] }, node.id))) })] }));
|
|
10
22
|
}
|