causal-inspector 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CausalInspector.css +124 -0
- package/dist/CausalInspector.d.ts +13 -0
- package/dist/CausalInspector.js +257 -0
- package/dist/components/CopyablePayload.d.ts +4 -0
- package/dist/components/CopyablePayload.js +44 -0
- package/dist/components/FilterBar.d.ts +1 -0
- package/dist/components/FilterBar.js +23 -0
- package/dist/components/GlobalScrubber.d.ts +1 -0
- package/dist/components/GlobalScrubber.js +148 -0
- package/dist/components/JsonSyntax.d.ts +3 -0
- package/dist/components/JsonSyntax.js +40 -0
- package/dist/context.d.ts +26 -0
- package/dist/context.js +28 -0
- package/dist/engines/index.d.ts +22 -0
- package/dist/engines/index.js +29 -0
- package/dist/engines/query.d.ts +14 -0
- package/dist/engines/query.js +241 -0
- package/dist/engines/scrubber.d.ts +12 -0
- package/dist/engines/scrubber.js +69 -0
- package/dist/engines/storage.d.ts +15 -0
- package/dist/engines/storage.js +16 -0
- package/dist/engines/subscription.d.ts +17 -0
- package/dist/engines/subscription.js +44 -0
- package/dist/engines/url.d.ts +13 -0
- package/dist/engines/url.js +64 -0
- package/dist/events.d.ts +77 -0
- package/dist/events.js +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +29 -0
- package/dist/machine/core.d.ts +22 -0
- package/dist/machine/core.js +34 -0
- package/dist/machine/engine.d.ts +17 -0
- package/dist/machine/engine.js +21 -0
- package/dist/machine/events.d.ts +18 -0
- package/dist/machine/events.js +1 -0
- package/dist/machine/hooks.d.ts +23 -0
- package/dist/machine/hooks.js +52 -0
- package/dist/machine/index.d.ts +5 -0
- package/dist/machine/index.js +4 -0
- package/dist/machine/store.d.ts +27 -0
- package/dist/machine/store.js +42 -0
- package/dist/panes/AggregateTimelinePane.d.ts +2 -0
- package/dist/panes/AggregateTimelinePane.js +224 -0
- package/dist/panes/CausalFlowPane.d.ts +7 -0
- package/dist/panes/CausalFlowPane.js +596 -0
- package/dist/panes/CausalTreePane.d.ts +5 -0
- package/dist/panes/CausalTreePane.js +158 -0
- package/dist/panes/CorrelationExplorerPane.d.ts +2 -0
- package/dist/panes/CorrelationExplorerPane.js +46 -0
- package/dist/panes/LogsPane.d.ts +6 -0
- package/dist/panes/LogsPane.js +65 -0
- package/dist/panes/TimelinePane.d.ts +6 -0
- package/dist/panes/TimelinePane.js +121 -0
- package/dist/panes/WaterfallPane.d.ts +2 -0
- package/dist/panes/WaterfallPane.js +202 -0
- package/dist/queries.d.ts +15 -0
- package/dist/queries.js +175 -0
- package/dist/reducer.d.ts +4 -0
- package/dist/reducer.js +177 -0
- package/dist/state.d.ts +34 -0
- package/dist/state.js +39 -0
- package/dist/theme.d.ts +7 -0
- package/dist/theme.js +34 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +91 -0
- package/package.json +43 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
|
3
|
+
import { useSelector, useDispatch } from "../machine";
|
|
4
|
+
import { CopyablePayload } from "../components/CopyablePayload";
|
|
5
|
+
import { eventTextColor } from "../theme";
|
|
6
|
+
import { formatTs, compactPayload, copyToClipboard, inScrubberRange } from "../utils";
|
|
7
|
+
import { Copy, Check, Search, X, ChevronRight, ChevronDown } from "lucide-react";
|
|
8
|
+
function buildTreeJson(roots, childrenMap) {
|
|
9
|
+
function toNode(evt) {
|
|
10
|
+
const children = evt.id ? (childrenMap.get(evt.id) ?? []) : [];
|
|
11
|
+
const node = {
|
|
12
|
+
name: evt.name,
|
|
13
|
+
reactorId: evt.reactorId,
|
|
14
|
+
summary: evt.summary,
|
|
15
|
+
};
|
|
16
|
+
if (children.length > 0) {
|
|
17
|
+
node.children = children.map(toNode);
|
|
18
|
+
}
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
21
|
+
return roots.map(toNode);
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// ReactorNode — intermediate node grouping children by reactor_id
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function ReactorNode({ reactorId, parentEventId, children, childrenMap, depth, isHighlighted, onClickReactor, }) {
|
|
27
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
28
|
+
const nodeRef = useRef(null);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (isHighlighted && nodeRef.current) {
|
|
31
|
+
nodeRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
32
|
+
}
|
|
33
|
+
}, [isHighlighted]);
|
|
34
|
+
const handleClick = useCallback(() => {
|
|
35
|
+
onClickReactor(reactorId, parentEventId);
|
|
36
|
+
}, [onClickReactor, parentEventId, reactorId]);
|
|
37
|
+
return (_jsxs("div", { className: depth > 0 ? "pl-6" : "", children: [_jsx("div", { ref: isHighlighted ? nodeRef : undefined, className: `group/tree w-full text-left px-2 py-1.5 rounded-md transition-all duration-150 hover:bg-white/[0.03] ${isHighlighted ? "bg-indigo-500/15 ring-1 ring-indigo-500/25" : ""}`, children: _jsxs("div", { className: "flex items-center gap-1.5 min-w-0", children: [_jsx("button", { onClick: (e) => { e.stopPropagation(); setCollapsed(v => !v); }, className: "text-[10px] text-muted-foreground hover:text-foreground shrink-0 w-3 text-center", children: collapsed ? _jsx(ChevronRight, { size: 10 }) : _jsx(ChevronDown, { size: 10 }) }), _jsxs("button", { onClick: handleClick, className: "flex items-center gap-1.5 min-w-0", children: [_jsx("span", { className: "px-1.5 py-0.5 rounded text-[9px] font-medium shrink-0 bg-white/[0.04] text-muted-foreground/60 italic border border-border", children: "reactor" }), _jsx("span", { className: "text-[10px] font-mono text-foreground/60 shrink-0", children: reactorId }), collapsed && (_jsxs("span", { className: "text-[10px] text-muted-foreground shrink-0", children: ["(", children.length, ")"] }))] })] }) }), !collapsed && children.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor }, child.seq)))] }));
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// TreeNode (recursive)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function TreeNode({ event, childrenMap, depth, onClickReactor, onInvestigate, }) {
|
|
43
|
+
const selectedSeq = useSelector((s) => s.selectedSeq);
|
|
44
|
+
const flowSelection = useSelector((s) => s.flowSelection);
|
|
45
|
+
const dispatch = useDispatch();
|
|
46
|
+
const [payloadOpen, setPayloadOpen] = useState(false);
|
|
47
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
48
|
+
const [copied, setCopied] = useState(false);
|
|
49
|
+
const isSelected = event.seq === selectedSeq;
|
|
50
|
+
const children = event.id ? (childrenMap.get(event.id) ?? []) : [];
|
|
51
|
+
const hasChildren = children.length > 0;
|
|
52
|
+
const nodeRef = useRef(null);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (isSelected && nodeRef.current) {
|
|
55
|
+
nodeRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
56
|
+
}
|
|
57
|
+
}, [isSelected]);
|
|
58
|
+
// Group children by reactor_id
|
|
59
|
+
const { reactorGroups, directChildren } = useMemo(() => {
|
|
60
|
+
const groups = new Map();
|
|
61
|
+
const direct = [];
|
|
62
|
+
for (const child of children) {
|
|
63
|
+
if (child.reactorId) {
|
|
64
|
+
const group = groups.get(child.reactorId) ?? [];
|
|
65
|
+
group.push(child);
|
|
66
|
+
groups.set(child.reactorId, group);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
direct.push(child);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { reactorGroups: groups, directChildren: direct };
|
|
73
|
+
}, [children]);
|
|
74
|
+
const highlightedReactorId = flowSelection?.kind === "reactor" ? flowSelection.reactorId : null;
|
|
75
|
+
return (_jsxs("div", { className: depth > 0 ? "pl-6" : "", children: [_jsxs("div", { ref: isSelected ? nodeRef : undefined, onClick: () => {
|
|
76
|
+
dispatch({ type: "ui/event_selected", payload: { seq: event.seq } });
|
|
77
|
+
if (event.correlationId) {
|
|
78
|
+
dispatch({ type: "ui/flow_opened", payload: { correlationId: event.correlationId } });
|
|
79
|
+
}
|
|
80
|
+
}, className: `group/tree w-full text-left px-2 py-1.5 rounded-md transition-all duration-150 cursor-pointer hover:bg-white/[0.03] ${isSelected ? "bg-indigo-500/15 ring-1 ring-indigo-500/25" : ""}`, children: [_jsxs("div", { className: "flex items-center gap-1.5 min-w-0", children: [hasChildren ? (_jsx("button", { onClick: (e) => { e.stopPropagation(); setCollapsed((v) => !v); }, className: "text-[10px] text-muted-foreground hover:text-foreground shrink-0 w-3 text-center", children: collapsed ? _jsx(ChevronRight, { size: 10 }) : _jsx(ChevronDown, { size: 10 }) })) : (_jsx("span", { className: "w-3 shrink-0" })), _jsx("span", { className: "text-[10px] font-mono shrink-0", style: { color: eventTextColor(event.name) }, children: event.name }), collapsed && hasChildren && (_jsxs("span", { className: "text-[10px] text-muted-foreground shrink-0", children: ["(", children.length, ")"] })), _jsx("span", { className: "text-[10px] text-muted-foreground shrink-0", children: formatTs(event.ts) }), _jsx("button", { onClick: (e) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
const json = buildTreeJson([event], childrenMap);
|
|
83
|
+
const text = JSON.stringify(json[0], null, 2);
|
|
84
|
+
copyToClipboard(text);
|
|
85
|
+
setCopied(true);
|
|
86
|
+
setTimeout(() => setCopied(false), 1500);
|
|
87
|
+
}, className: "opacity-0 group-hover/tree:opacity-100 transition-all duration-150 ml-auto p-1 rounded-md hover:bg-white/[0.05] shrink-0 text-[10px] text-muted-foreground/50", title: "Copy subtree as JSON", children: copied ? _jsx(Check, { size: 12 }) : _jsx(Copy, { size: 12 }) }), onInvestigate && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onInvestigate(event); }, className: "opacity-0 group-hover/tree:opacity-100 transition-all duration-150 p-1 rounded-md hover:bg-white/[0.05] shrink-0 text-muted-foreground/50", title: "Investigate", children: _jsx(Search, { size: 12 }) }))] }), _jsx("button", { onClick: (e) => { e.stopPropagation(); setPayloadOpen((v) => !v); }, className: "mt-0.5 ml-3 text-[10px] font-mono text-muted-foreground hover:text-foreground truncate text-left max-w-full block", title: "Click to expand payload", children: event.summary ?? compactPayload(event.payload) }), payloadOpen && (_jsx(CopyablePayload, { payload: event.payload, className: "mt-1 ml-3 max-h-48" }))] }), !collapsed && (_jsxs(_Fragment, { children: [directChildren.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor, onInvestigate: onInvestigate }, child.seq))), [...reactorGroups.entries()].map(([hid, group]) => (_jsx(ReactorNode, { reactorId: hid, parentEventId: event.id, children: group, childrenMap: childrenMap, depth: depth + 1, isHighlighted: hid === highlightedReactorId, onClickReactor: onClickReactor }, hid)))] }))] }));
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// matchesFlowSelection
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
function matchesFlowSelection(event, sel) {
|
|
93
|
+
if (!sel)
|
|
94
|
+
return true;
|
|
95
|
+
if (sel.kind === "event-type")
|
|
96
|
+
return event.name === sel.name;
|
|
97
|
+
return event.reactorId === sel.reactorId;
|
|
98
|
+
}
|
|
99
|
+
export function CausalTreePane({ onInvestigate } = {}) {
|
|
100
|
+
const causalTree = useSelector((s) => s.causalTree);
|
|
101
|
+
const selectedSeq = useSelector((s) => s.selectedSeq);
|
|
102
|
+
const flowSelection = useSelector((s) => s.flowSelection);
|
|
103
|
+
const flowCorrelationId = useSelector((s) => s.flowCorrelationId);
|
|
104
|
+
const scrubberStart = useSelector((s) => s.scrubberStart);
|
|
105
|
+
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
106
|
+
const dispatch = useDispatch();
|
|
107
|
+
const treeEvents = useMemo(() => {
|
|
108
|
+
const all = causalTree?.events ?? null;
|
|
109
|
+
if (all == null || (scrubberStart == null && scrubberEnd == null))
|
|
110
|
+
return all;
|
|
111
|
+
return all.filter((e) => inScrubberRange(e.seq, scrubberStart, scrubberEnd));
|
|
112
|
+
}, [causalTree?.events, scrubberStart, scrubberEnd]);
|
|
113
|
+
const treeLoading = selectedSeq != null && causalTree == null;
|
|
114
|
+
const onClickReactor = useCallback((reactorId, _parentEventId) => {
|
|
115
|
+
if (flowCorrelationId) {
|
|
116
|
+
dispatch({ type: "ui/flow_node_selected", payload: { kind: "reactor", reactorId } });
|
|
117
|
+
}
|
|
118
|
+
dispatch({ type: "ui/handler_selected", payload: { reactorId } });
|
|
119
|
+
}, [flowCorrelationId, dispatch]);
|
|
120
|
+
const { roots, childrenMap, totalCount, filteredCount } = useMemo(() => {
|
|
121
|
+
if (!treeEvents || treeEvents.length === 0)
|
|
122
|
+
return { roots: [], childrenMap: new Map(), totalCount: 0, filteredCount: 0 };
|
|
123
|
+
const total = treeEvents.length;
|
|
124
|
+
const events = (flowCorrelationId && flowSelection)
|
|
125
|
+
? treeEvents.filter(e => matchesFlowSelection(e, flowSelection))
|
|
126
|
+
: treeEvents;
|
|
127
|
+
const idSet = new Set(events.map(e => e.id).filter(Boolean));
|
|
128
|
+
const cMap = new Map();
|
|
129
|
+
const rootList = [];
|
|
130
|
+
for (const evt of events) {
|
|
131
|
+
if (evt.parentId == null || !idSet.has(evt.parentId)) {
|
|
132
|
+
rootList.push(evt);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const siblings = cMap.get(evt.parentId) ?? [];
|
|
136
|
+
siblings.push(evt);
|
|
137
|
+
cMap.set(evt.parentId, siblings);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
rootList.sort((a, b) => a.seq - b.seq);
|
|
141
|
+
const filtered = rootList.length + [...cMap.values()].reduce((s, a) => s + a.length, 0);
|
|
142
|
+
return { roots: rootList, childrenMap: cMap, totalCount: total, filteredCount: filtered };
|
|
143
|
+
}, [treeEvents, flowCorrelationId, flowSelection]);
|
|
144
|
+
if (treeLoading) {
|
|
145
|
+
return (_jsxs("div", { className: "p-3 space-y-1.5 animate-pulse", children: [_jsx("div", { className: "h-3 w-32 bg-muted rounded mb-3" }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-4 w-12 bg-muted rounded" }), _jsx("div", { className: "h-4 w-36 bg-muted rounded" }), _jsx("div", { className: "h-3 w-24 bg-muted rounded" })] }), _jsxs("div", { className: "pl-6 space-y-1.5", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-4 w-14 bg-muted rounded" }), _jsx("div", { className: "h-4 w-44 bg-muted rounded" }), _jsx("div", { className: "h-3 w-24 bg-muted rounded" })] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-4 w-10 bg-muted rounded" }), _jsx("div", { className: "h-4 w-32 bg-muted rounded" }), _jsx("div", { className: "h-3 w-24 bg-muted rounded" })] })] })] }));
|
|
146
|
+
}
|
|
147
|
+
if (!treeEvents) {
|
|
148
|
+
return (_jsx("div", { className: "flex items-center justify-center h-full text-xs text-muted-foreground/50 tracking-wide", children: "Select an event to view its causal tree" }));
|
|
149
|
+
}
|
|
150
|
+
if (roots.length === 0 && flowSelection) {
|
|
151
|
+
return (_jsxs("div", { className: "h-full overflow-y-auto p-3", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2 px-2.5 py-1.5 rounded-md bg-indigo-500/8 border border-indigo-500/15 text-xs text-indigo-400", children: [_jsx("span", { children: flowSelection.kind === "event-type"
|
|
152
|
+
? flowSelection.name
|
|
153
|
+
: `outputs of ${flowSelection.reactorId}` }), _jsx("button", { onClick: () => dispatch({ type: "ui/flow_node_selected", payload: null }), className: "ml-auto hover:text-foreground", children: _jsx(X, { size: 12 }) })] }), _jsx("div", { className: "flex items-center justify-center h-32 text-sm text-muted-foreground", children: "No events match the current filter" })] }));
|
|
154
|
+
}
|
|
155
|
+
return (_jsxs("div", { className: "h-full overflow-y-auto p-3", children: [flowSelection && (_jsxs("div", { className: "flex items-center gap-2 mb-2 px-2.5 py-1.5 rounded-md bg-indigo-500/8 border border-indigo-500/15 text-xs text-indigo-400", children: [_jsx("span", { children: flowSelection.kind === "event-type"
|
|
156
|
+
? flowSelection.name
|
|
157
|
+
: `outputs of ${flowSelection.reactorId}` }), _jsx("button", { onClick: () => dispatch({ type: "ui/flow_node_selected", payload: null }), className: "ml-auto hover:text-foreground", children: _jsx(X, { size: 12 }) })] })), _jsxs("h3", { className: "text-[10px] font-semibold text-muted-foreground/50 mb-2 uppercase tracking-widest", children: ["Causal Tree (", flowSelection ? `${filteredCount} of ${totalCount}` : totalCount, " events)"] }), roots.map(root => (_jsx(TreeNode, { event: root, childrenMap: childrenMap, depth: 0, onClickReactor: onClickReactor, onInvestigate: onInvestigate }, root.seq)))] }));
|
|
158
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import { useSelector, useDispatch } from "../machine";
|
|
4
|
+
import { eventTextColor, eventBg } from "../theme";
|
|
5
|
+
import { formatTs } from "../utils";
|
|
6
|
+
function RelativeDuration({ firstTs, lastTs }) {
|
|
7
|
+
const first = new Date(firstTs).getTime();
|
|
8
|
+
const last = new Date(lastTs).getTime();
|
|
9
|
+
const diffMs = last - first;
|
|
10
|
+
if (diffMs < 1000)
|
|
11
|
+
return _jsxs("span", { children: [diffMs, "ms"] });
|
|
12
|
+
if (diffMs < 60_000)
|
|
13
|
+
return _jsxs("span", { children: [(diffMs / 1000).toFixed(1), "s"] });
|
|
14
|
+
if (diffMs < 3_600_000)
|
|
15
|
+
return _jsxs("span", { children: [(diffMs / 60_000).toFixed(1), "m"] });
|
|
16
|
+
return _jsxs("span", { children: [(diffMs / 3_600_000).toFixed(1), "h"] });
|
|
17
|
+
}
|
|
18
|
+
export function CorrelationExplorerPane() {
|
|
19
|
+
const correlations = useSelector((s) => s.correlations);
|
|
20
|
+
const loading = useSelector((s) => s.correlationsLoading);
|
|
21
|
+
const dispatch = useDispatch();
|
|
22
|
+
const [search, setSearch] = useState("");
|
|
23
|
+
const searchTimerRef = useRef(null);
|
|
24
|
+
// Request correlations on mount
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
dispatch({ type: "ui/correlations_requested", payload: {} });
|
|
27
|
+
}, [dispatch]);
|
|
28
|
+
const handleSearchChange = useCallback((value) => {
|
|
29
|
+
setSearch(value);
|
|
30
|
+
if (searchTimerRef.current)
|
|
31
|
+
clearTimeout(searchTimerRef.current);
|
|
32
|
+
searchTimerRef.current = setTimeout(() => {
|
|
33
|
+
dispatch({ type: "ui/correlations_requested", payload: { search: value || undefined } });
|
|
34
|
+
}, 300);
|
|
35
|
+
}, [dispatch]);
|
|
36
|
+
const handleRowClick = useCallback((correlationId) => {
|
|
37
|
+
dispatch({ type: "ui/flow_opened", payload: { correlationId } });
|
|
38
|
+
}, [dispatch]);
|
|
39
|
+
const handleCopy = useCallback((text) => {
|
|
40
|
+
navigator.clipboard.writeText(text).catch(() => { });
|
|
41
|
+
}, []);
|
|
42
|
+
return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsx("div", { className: "px-3 py-2.5 border-b border-border", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: _jsx("input", { type: "text", placeholder: "Search by correlation ID or event type...", value: search, onChange: (e) => handleSearchChange(e.target.value), className: "w-full px-3 py-1.5 text-xs bg-background/50 border border-border rounded-md text-foreground placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 transition-all" }) }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-border text-[9px] font-semibold text-muted-foreground/40 uppercase tracking-widest", children: [_jsx("span", { className: "w-28 shrink-0", children: "Root Event" }), _jsx("span", { className: "w-24 shrink-0", children: "Correlation" }), _jsx("span", { className: "w-12 shrink-0 text-right", children: "Events" }), _jsx("span", { className: "w-20 shrink-0 text-right", children: "Duration" }), _jsx("span", { className: "flex-1", children: "Last Activity" })] }), loading && correlations.length === 0 ? (_jsx("div", { className: "animate-pulse p-3", children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-2 py-2.5", children: [_jsx("div", { className: "h-3 w-28 bg-white/[0.03] rounded" }), _jsx("div", { className: "h-3 w-24 bg-white/[0.03] rounded" }), _jsx("div", { className: "h-3 w-12 bg-white/[0.03] rounded" })] }, i))) })) : correlations.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32 text-xs text-muted-foreground/50 tracking-wide", children: "No correlations found" })) : (_jsx("div", { className: "flex-1 overflow-y-auto", children: correlations.map((corr) => (_jsxs("button", { onClick: () => handleRowClick(corr.correlationId), className: "group w-full text-left flex items-center gap-2 px-3 py-2.5 border-b border-border hover:bg-indigo-500/8 transition-all duration-150", children: [_jsx("span", { className: "text-[10px] font-mono shrink-0 w-28 truncate px-1.5 py-0.5 rounded", style: {
|
|
43
|
+
color: eventTextColor(corr.rootEventType),
|
|
44
|
+
background: eventBg(corr.rootEventType),
|
|
45
|
+
}, title: corr.rootEventType, children: corr.rootEventType }), _jsx("span", { className: "text-[10px] font-mono text-purple-400/70 w-24 shrink-0 truncate cursor-pointer hover:text-purple-400 transition-colors", title: `Click to copy: ${corr.correlationId}`, onClick: (e) => { e.stopPropagation(); handleCopy(corr.correlationId); }, children: corr.correlationId.slice(0, 8) }), _jsx("span", { className: "text-[11px] font-mono text-foreground/70 w-12 shrink-0 text-right tabular-nums", children: corr.eventCount }), _jsx("span", { className: "text-[10px] text-muted-foreground/50 w-20 shrink-0 text-right font-mono tabular-nums", children: _jsx(RelativeDuration, { firstTs: corr.firstTs, lastTs: corr.lastTs }) }), _jsx("span", { className: "text-[10px] text-muted-foreground/40 flex-1 truncate tabular-nums", children: formatTs(corr.lastTs) }), corr.hasErrors && (_jsx("span", { className: "w-2 h-2 rounded-full bg-red-500/80 shrink-0", style: { boxShadow: "0 0 6px rgba(239, 68, 68, 0.3)" }, title: "This correlation has errors" }))] }, corr.correlationId))) }))] }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LogsFilter } from "../types";
|
|
2
|
+
export type LogsPaneProps = {
|
|
3
|
+
/** Optional callback when user wants to investigate logs. */
|
|
4
|
+
onInvestigate?: (filter: LogsFilter) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function LogsPane({ onInvestigate }?: LogsPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from "react";
|
|
3
|
+
import { useSelector } from "../machine";
|
|
4
|
+
import { LOG_LEVEL_COLORS } from "../theme";
|
|
5
|
+
import { formatTs, inScrubberRange } from "../utils";
|
|
6
|
+
import { Search, ChevronRight, ChevronDown } from "lucide-react";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// LogRow
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
function LogRow({ log, showReactor }) {
|
|
11
|
+
const [expanded, setExpanded] = useState(false);
|
|
12
|
+
const levelColor = LOG_LEVEL_COLORS[log.level] ?? "bg-zinc-600/20 text-zinc-400";
|
|
13
|
+
return (_jsxs("div", { className: "px-2 py-1.5 hover:bg-white/[0.02] rounded-md transition-colors duration-100", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("span", { className: `px-1.5 py-0.5 rounded text-[9px] font-semibold uppercase shrink-0 ${levelColor}`, children: log.level }), _jsx("span", { className: "text-[10px] text-muted-foreground/50 shrink-0 tabular-nums", children: formatTs(log.loggedAt) }), showReactor && (_jsx("span", { className: "text-[10px] font-mono text-muted-foreground/40 shrink-0", children: log.reactorId })), _jsx("span", { className: "text-[11px] text-foreground/80 truncate", children: log.message }), log.data != null && (_jsx("button", { onClick: () => setExpanded((v) => !v), className: "ml-auto text-[10px] p-1 rounded-md hover:bg-white/[0.05] shrink-0 text-muted-foreground/50 hover:text-foreground transition-colors", children: expanded ? _jsx(ChevronDown, { size: 10 }) : _jsx(ChevronRight, { size: 10 }) }))] }), expanded && log.data != null && (_jsx("pre", { className: "mt-1.5 ml-4 text-[10px] font-mono text-muted-foreground/70 bg-white/[0.02] rounded-md p-2.5 max-h-32 overflow-auto whitespace-pre-wrap border border-border", children: typeof log.data === "string" ? log.data : JSON.stringify(log.data, null, 2) }))] }));
|
|
14
|
+
}
|
|
15
|
+
export function LogsPane({ onInvestigate } = {}) {
|
|
16
|
+
const logs = useSelector((s) => s.logs);
|
|
17
|
+
const logsFilter = useSelector((s) => s.logsFilter);
|
|
18
|
+
const flowData = useSelector((s) => s.flowData);
|
|
19
|
+
const scrubberStart = useSelector((s) => s.scrubberStart);
|
|
20
|
+
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
21
|
+
const [levelFilter, setLevelFilter] = useState(new Set(["debug", "info", "warn"]));
|
|
22
|
+
const [searchText, setSearchText] = useState("");
|
|
23
|
+
const isCorrelationScope = logsFilter.scope === "correlation" && logsFilter.correlationId != null;
|
|
24
|
+
const hasFilter = logsFilter.reactorId != null || isCorrelationScope;
|
|
25
|
+
// Set of event IDs visible within scrubber range
|
|
26
|
+
const visibleEventIds = useMemo(() => {
|
|
27
|
+
if (scrubberStart == null && scrubberEnd == null)
|
|
28
|
+
return null;
|
|
29
|
+
return new Set(flowData.filter((e) => inScrubberRange(e.seq, scrubberStart, scrubberEnd)).map((e) => e.id).filter(Boolean));
|
|
30
|
+
}, [flowData, scrubberStart, scrubberEnd]);
|
|
31
|
+
// Client-side filtering
|
|
32
|
+
const filteredLogs = useMemo(() => {
|
|
33
|
+
let filtered = logs.filter((l) => levelFilter.has(l.level));
|
|
34
|
+
// Filter by reactor when handler is selected
|
|
35
|
+
if (logsFilter.scope === "reactor" && logsFilter.reactorId) {
|
|
36
|
+
filtered = filtered.filter((l) => l.reactorId === logsFilter.reactorId);
|
|
37
|
+
}
|
|
38
|
+
if (visibleEventIds != null) {
|
|
39
|
+
filtered = filtered.filter((l) => visibleEventIds.has(l.eventId));
|
|
40
|
+
}
|
|
41
|
+
if (searchText) {
|
|
42
|
+
const lower = searchText.toLowerCase();
|
|
43
|
+
filtered = filtered.filter((l) => l.message.toLowerCase().includes(lower) ||
|
|
44
|
+
l.reactorId.toLowerCase().includes(lower) ||
|
|
45
|
+
(l.data && String(l.data).toLowerCase().includes(lower)));
|
|
46
|
+
}
|
|
47
|
+
return filtered;
|
|
48
|
+
}, [logs, logsFilter, levelFilter, searchText, visibleEventIds]);
|
|
49
|
+
if (!hasFilter) {
|
|
50
|
+
return (_jsx("div", { className: "flex items-center justify-center h-full text-xs text-muted-foreground/50 tracking-wide", children: "Click a reactor node in the causal tree to view logs" }));
|
|
51
|
+
}
|
|
52
|
+
const toggleLevel = (level) => {
|
|
53
|
+
setLevelFilter((prev) => {
|
|
54
|
+
const next = new Set(prev);
|
|
55
|
+
if (next.has(level))
|
|
56
|
+
next.delete(level);
|
|
57
|
+
else
|
|
58
|
+
next.add(level);
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return (_jsxs("div", { className: "h-full flex flex-col", children: [_jsxs("div", { className: "px-3 py-2 border-b border-border flex items-center gap-3 flex-wrap", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("div", { className: "flex items-center gap-1 text-[10px]", children: ["debug", "info", "warn"].map((level) => (_jsx("button", { onClick: () => toggleLevel(level), className: `px-2 py-0.5 rounded-md uppercase font-semibold transition-all duration-150 ${levelFilter.has(level)
|
|
63
|
+
? LOG_LEVEL_COLORS[level]
|
|
64
|
+
: "text-muted-foreground/30 line-through"}`, children: level }, level))) }), _jsxs("div", { className: "relative flex items-center ml-auto", children: [_jsx(Search, { size: 10, className: "absolute left-2.5 text-muted-foreground/40 pointer-events-none" }), _jsx("input", { type: "text", placeholder: "Search logs...", value: searchText, onChange: (e) => setSearchText(e.target.value), className: "pl-7 pr-2 text-[11px] bg-background/50 border border-border rounded-md py-1 w-44 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 text-foreground placeholder:text-muted-foreground/40 transition-all" })] }), onInvestigate && (_jsx("button", { onClick: () => onInvestigate(logsFilter), className: "flex items-center gap-1 px-2 py-1 rounded-md text-[11px] text-muted-foreground/50 hover:text-foreground hover:bg-white/[0.04] transition-all duration-150", title: "Investigate logs", children: _jsx(Search, { size: 12 }) }))] }), _jsxs("div", { className: "px-3 py-1.5 text-[10px] text-muted-foreground/50", children: [logsFilter.reactorId ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-mono text-foreground/60", children: logsFilter.reactorId }), isCorrelationScope && _jsx("span", { className: "ml-1", children: "(all reactors in correlation)" })] })) : (_jsx("span", { children: "All reactors in correlation" })), _jsxs("span", { className: "ml-2 tabular-nums", children: [filteredLogs.length, " logs"] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-1", children: [filteredLogs.length === 0 && (_jsx("div", { className: "p-3 text-[11px] text-muted-foreground/40", children: "No logs match filters" })), filteredLogs.map((log, i) => (_jsx(LogRow, { log: log, showReactor: isCorrelationScope }, i)))] })] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { InspectorEvent } from "../types";
|
|
2
|
+
export type TimelinePaneProps = {
|
|
3
|
+
/** Optional callback when user wants to investigate an event. */
|
|
4
|
+
onInvestigate?: (event: InspectorEvent) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function TimelinePane({ onInvestigate }?: TimelinePaneProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
3
|
+
import { useSelector, useDispatch } from "../machine";
|
|
4
|
+
import { FilterBar } from "../components/FilterBar";
|
|
5
|
+
import { CopyablePayload } from "../components/CopyablePayload";
|
|
6
|
+
import { eventTextColor, eventBg } from "../theme";
|
|
7
|
+
import { formatTs, compactPayload, aggregateKey, inScrubberRange } from "../utils";
|
|
8
|
+
import { Search, ChevronRight, ChevronDown } from "lucide-react";
|
|
9
|
+
function EventRow({ event, isSelected, onClick, onFilterCorrelation, onFilterStream, onInvestigate, indent, }) {
|
|
10
|
+
const [payloadOpen, setPayloadOpen] = useState(false);
|
|
11
|
+
return (_jsxs("div", { className: `group w-full text-left px-3 py-2 border-b border-border transition-all duration-150 ${isSelected
|
|
12
|
+
? "bg-indigo-500/15"
|
|
13
|
+
: "hover:bg-white/[0.02]"}`, style: indent ? { paddingLeft: 28 } : undefined, children: [_jsx("div", { onClick: onClick, role: "button", tabIndex: 0, className: "w-full text-left cursor-pointer", children: _jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [_jsx("span", { className: "text-[10px] font-mono text-muted-foreground/60 w-10 shrink-0 text-right tabular-nums", children: event.seq }), _jsx("span", { className: "text-[10px] text-muted-foreground/70 shrink-0 w-32 tabular-nums", children: formatTs(event.ts) }), event.correlationId && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onFilterCorrelation(event.correlationId); }, className: "px-1.5 py-0.5 rounded-full text-[9px] font-mono bg-purple-500/8 text-purple-400/80 hover:bg-purple-500/15 hover:text-purple-400 shrink-0 transition-all border border-purple-500/10", title: `Filter by correlation ${event.correlationId}`, children: event.correlationId.slice(0, 8) })), event.aggregateType && event.aggregateId && (_jsxs("button", { onClick: (e) => { e.stopPropagation(); onFilterStream(aggregateKey(event)); }, className: "opacity-0 group-hover:opacity-100 px-1.5 py-0.5 rounded-full text-[9px] font-mono bg-teal-500/8 text-teal-400/80 hover:bg-teal-500/15 hover:text-teal-400 shrink-0 transition-all border border-teal-500/10", title: `Filter by stream ${event.aggregateType}:${event.aggregateId}`, children: [event.aggregateType, ":", event.aggregateId.slice(0, 8)] })), _jsx("span", { className: "text-xs font-mono shrink-0 px-1.5 py-0.5 rounded", style: {
|
|
14
|
+
color: eventTextColor(event.name),
|
|
15
|
+
background: eventBg(event.name),
|
|
16
|
+
}, children: event.name }), _jsxs("button", { onClick: (e) => { e.stopPropagation(); setPayloadOpen((v) => !v); }, className: "flex items-center gap-1 text-[10px] font-mono text-muted-foreground/60 hover:text-muted-foreground truncate text-left min-w-0 transition-colors", title: "Click to expand payload", children: [_jsx(ChevronRight, { size: 10, className: `shrink-0 transition-transform duration-150 ${payloadOpen ? "rotate-90" : ""}` }), _jsx("span", { className: "truncate", children: event.summary ?? compactPayload(event.payload) })] }), onInvestigate && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onInvestigate(); }, className: "opacity-0 group-hover:opacity-100 transition-opacity duration-150 ml-auto p-1 rounded-md hover:bg-white/[0.05] shrink-0 text-muted-foreground", title: "Investigate", children: _jsx(Search, { size: 12 }) }))] }) }), payloadOpen && (_jsx(CopyablePayload, { payload: event.payload, className: "mt-2 ml-12 max-h-64" }))] }));
|
|
17
|
+
}
|
|
18
|
+
function groupEventsByCorrelation(events) {
|
|
19
|
+
const result = [];
|
|
20
|
+
let i = 0;
|
|
21
|
+
while (i < events.length) {
|
|
22
|
+
const event = events[i];
|
|
23
|
+
if (!event.correlationId) {
|
|
24
|
+
result.push(event);
|
|
25
|
+
i++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Collect consecutive events with the same correlationId
|
|
29
|
+
const cid = event.correlationId;
|
|
30
|
+
const groupEvents = [event];
|
|
31
|
+
let j = i + 1;
|
|
32
|
+
while (j < events.length && events[j].correlationId === cid) {
|
|
33
|
+
groupEvents.push(events[j]);
|
|
34
|
+
j++;
|
|
35
|
+
}
|
|
36
|
+
if (groupEvents.length === 1) {
|
|
37
|
+
// Single event — render flat
|
|
38
|
+
result.push(event);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Find the root event (no reactorId) or fall back to first
|
|
42
|
+
const rootIdx = groupEvents.findIndex((e) => !e.reactorId);
|
|
43
|
+
const root = rootIdx >= 0 ? groupEvents[rootIdx] : groupEvents[0];
|
|
44
|
+
const children = groupEvents.filter((e) => e !== root);
|
|
45
|
+
result.push({ correlationId: cid, root, children });
|
|
46
|
+
}
|
|
47
|
+
i = j;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function EventGroupRow({ group, selectedSeq, collapsed, onToggle, onSelect, onFilterCorrelation, onFilterStream, onInvestigate, }) {
|
|
52
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "relative", children: [_jsx("button", { onClick: onToggle, className: "absolute left-1 top-2.5 z-10 p-0.5 rounded hover:bg-white/[0.05] text-muted-foreground/60 hover:text-muted-foreground transition-colors", title: collapsed ? "Expand group" : "Collapse group", children: collapsed ? _jsx(ChevronRight, { size: 12 }) : _jsx(ChevronDown, { size: 12 }) }), _jsxs("div", { className: "relative", children: [_jsx(EventRow, { event: group.root, isSelected: group.root.seq === selectedSeq, onClick: () => onSelect(group.root), onFilterCorrelation: onFilterCorrelation, onFilterStream: onFilterStream, onInvestigate: onInvestigate ? () => onInvestigate(group.root) : undefined }), collapsed && (_jsxs("span", { className: "absolute right-3 top-2.5 text-[9px] font-mono px-1.5 py-0.5 rounded-full bg-white/[0.04] text-muted-foreground/60 border border-border", children: ["+", group.children.length] }))] })] }), !collapsed &&
|
|
53
|
+
group.children.map((child) => (_jsx(EventRow, { event: child, isSelected: child.seq === selectedSeq, onClick: () => onSelect(child), onFilterCorrelation: onFilterCorrelation, onFilterStream: onFilterStream, onInvestigate: onInvestigate ? () => onInvestigate(child) : undefined, indent: true }, child.seq)))] }));
|
|
54
|
+
}
|
|
55
|
+
function InfiniteScrollSentinel({ onVisible, loading }) {
|
|
56
|
+
const ref = useRef(null);
|
|
57
|
+
const onVisibleRef = useRef(onVisible);
|
|
58
|
+
onVisibleRef.current = onVisible;
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const el = ref.current;
|
|
61
|
+
if (!el)
|
|
62
|
+
return;
|
|
63
|
+
const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting)
|
|
64
|
+
onVisibleRef.current(); }, { rootMargin: "200px" });
|
|
65
|
+
observer.observe(el);
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
}, []);
|
|
68
|
+
return (_jsx("div", { ref: ref, className: "flex items-center justify-center py-4", children: loading && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-indigo-500/50 animate-pulse" }), _jsx("span", { className: "text-[10px] text-muted-foreground/60", children: "Loading" })] })) }));
|
|
69
|
+
}
|
|
70
|
+
export function TimelinePane({ onInvestigate } = {}) {
|
|
71
|
+
const events = useSelector((s) => s.events);
|
|
72
|
+
const loading = useSelector((s) => s.loading);
|
|
73
|
+
const hasMore = useSelector((s) => s.hasMore);
|
|
74
|
+
const selectedSeq = useSelector((s) => s.selectedSeq);
|
|
75
|
+
const scrubberStart = useSelector((s) => s.scrubberStart);
|
|
76
|
+
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
77
|
+
const dispatch = useDispatch();
|
|
78
|
+
// Default collapsed — this set tracks which groups are *expanded*
|
|
79
|
+
const [expandedGroups, setExpandedGroups] = useState(() => new Set());
|
|
80
|
+
const displayedEvents = useMemo(() => {
|
|
81
|
+
if (scrubberStart == null && scrubberEnd == null)
|
|
82
|
+
return events;
|
|
83
|
+
return events.filter(e => inScrubberRange(e.seq, scrubberStart, scrubberEnd));
|
|
84
|
+
}, [events, scrubberStart, scrubberEnd]);
|
|
85
|
+
const grouped = useMemo(() => groupEventsByCorrelation(displayedEvents), [displayedEvents]);
|
|
86
|
+
const toggleGroup = useCallback((correlationId) => {
|
|
87
|
+
setExpandedGroups((prev) => {
|
|
88
|
+
const next = new Set(prev);
|
|
89
|
+
if (next.has(correlationId))
|
|
90
|
+
next.delete(correlationId);
|
|
91
|
+
else
|
|
92
|
+
next.add(correlationId);
|
|
93
|
+
return next;
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
const handleSelect = useCallback((event) => {
|
|
97
|
+
dispatch({ type: "ui/event_selected", payload: { seq: event.seq } });
|
|
98
|
+
if (event.correlationId) {
|
|
99
|
+
dispatch({ type: "ui/flow_opened", payload: { correlationId: event.correlationId } });
|
|
100
|
+
}
|
|
101
|
+
}, [dispatch]);
|
|
102
|
+
const handleFilterCorrelation = useCallback((correlationId) => {
|
|
103
|
+
dispatch({ type: "ui/filter_changed", payload: { correlationId } });
|
|
104
|
+
}, [dispatch]);
|
|
105
|
+
const handleFilterStream = useCallback((aggregateKey) => {
|
|
106
|
+
dispatch({ type: "ui/filter_changed", payload: { aggregateKey } });
|
|
107
|
+
}, [dispatch]);
|
|
108
|
+
const handleLoadMore = useCallback(() => {
|
|
109
|
+
dispatch({ type: "ui/load_more_requested" });
|
|
110
|
+
}, [dispatch]);
|
|
111
|
+
return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsx(FilterBar, {}), loading && events.length === 0 ? (_jsx("div", { className: "animate-pulse p-1", children: Array.from({ length: 12 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2.5 border-b border-border", children: [_jsx("div", { className: "h-3 w-10 bg-white/[0.03] rounded shrink-0" }), _jsx("div", { className: "h-3 w-32 bg-white/[0.03] rounded shrink-0" }), _jsx("div", { className: "h-3 bg-white/[0.03] rounded flex-1", style: { maxWidth: `${150 + (i * 37) % 200}px` } })] }, i))) })) : events.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32 text-sm text-muted-foreground/60", children: "No events found" })) : (_jsxs("div", { className: "flex-1 overflow-y-auto", children: [grouped.map((item) => {
|
|
112
|
+
if ("correlationId" in item && "root" in item) {
|
|
113
|
+
// EventGroup
|
|
114
|
+
const group = item;
|
|
115
|
+
return (_jsx(EventGroupRow, { group: group, selectedSeq: selectedSeq, collapsed: !expandedGroups.has(group.correlationId), onToggle: () => toggleGroup(group.correlationId), onSelect: handleSelect, onFilterCorrelation: handleFilterCorrelation, onFilterStream: handleFilterStream, onInvestigate: onInvestigate }, `group-${group.correlationId}-${group.root.seq}`));
|
|
116
|
+
}
|
|
117
|
+
// Single event
|
|
118
|
+
const event = item;
|
|
119
|
+
return (_jsx(EventRow, { event: event, isSelected: event.seq === selectedSeq, onClick: () => handleSelect(event), onFilterCorrelation: handleFilterCorrelation, onFilterStream: handleFilterStream, onInvestigate: onInvestigate ? () => onInvestigate(event) : undefined }, event.seq));
|
|
120
|
+
}), hasMore && (_jsx(InfiniteScrollSentinel, { onVisible: handleLoadMore, loading: loading }))] }))] }));
|
|
121
|
+
}
|