causal-inspector 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/dist/causal-inspector.css +2899 -0
- package/dist/events.d.ts +1 -0
- package/dist/panes/CausalFlowPane.js +21 -9
- package/dist/panes/LogsPane.js +2 -1
- package/dist/panes/WorkflowExplorerPane.js +4 -1
- package/dist/reducer.js +37 -2
- package/dist/state.d.ts +3 -0
- package/dist/state.js +1 -0
- package/package.json +18 -3
- package/dist/panes/CorrelationExplorerPane.d.ts +0 -2
- package/dist/panes/CorrelationExplorerPane.js +0 -46
package/dist/events.d.ts
CHANGED
|
@@ -52,6 +52,7 @@ type EventSelected = BaseEvent<"ui/event_selected", {
|
|
|
52
52
|
type EventDeselected = BaseEvent<"ui/event_deselected">;
|
|
53
53
|
type FlowOpened = BaseEvent<"ui/flow_opened", {
|
|
54
54
|
workflowId: string;
|
|
55
|
+
focusError?: boolean;
|
|
55
56
|
}>;
|
|
56
57
|
type FlowClosed = BaseEvent<"ui/flow_closed">;
|
|
57
58
|
type FlowNodeSelected = BaseEvent<"ui/flow_node_selected", FlowSelection>;
|
|
@@ -373,9 +373,19 @@ function FitOnLoad() {
|
|
|
373
373
|
function FocusOnSelection({ nodes, flowData }) {
|
|
374
374
|
const selectedSeq = useSelector((s) => s.selectedSeq);
|
|
375
375
|
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
376
|
+
const flowSelection = useSelector((s) => s.flowSelection);
|
|
376
377
|
const { setCenter, getZoom } = useReactFlow();
|
|
377
378
|
const nodesRef = useRef(nodes);
|
|
378
379
|
nodesRef.current = nodes;
|
|
380
|
+
const centerOnNode = useCallback((nodeId) => {
|
|
381
|
+
const node = nodesRef.current.find(n => n.id === nodeId);
|
|
382
|
+
if (!node)
|
|
383
|
+
return;
|
|
384
|
+
const isReactor = node.id.startsWith("hdl:");
|
|
385
|
+
const w = isReactor ? REACTOR_WIDTH : NODE_WIDTH;
|
|
386
|
+
const h = isReactor ? estimateReactorHeight(node.data) : NODE_HEIGHT;
|
|
387
|
+
setCenter(node.position.x + w / 2, node.position.y + h / 2, { zoom: getZoom(), duration: 400 });
|
|
388
|
+
}, [setCenter, getZoom]);
|
|
379
389
|
useEffect(() => {
|
|
380
390
|
// Don't recenter while scrubber is active
|
|
381
391
|
if (scrubberEnd != null)
|
|
@@ -385,15 +395,17 @@ function FocusOnSelection({ nodes, flowData }) {
|
|
|
385
395
|
const evt = flowData.find(e => e.seq === selectedSeq);
|
|
386
396
|
if (!evt)
|
|
387
397
|
return;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
398
|
+
centerOnNode(`evt:${evt.name}`);
|
|
399
|
+
}, [selectedSeq, scrubberEnd, flowData, centerOnNode]);
|
|
400
|
+
// Pan to a selected reactor node (e.g. the failed reactor opened from the
|
|
401
|
+
// Workflows error pill), so it isn't left off-screen in a large graph.
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
if (scrubberEnd != null)
|
|
391
404
|
return;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}, [selectedSeq, scrubberEnd, flowData, setCenter, getZoom]);
|
|
405
|
+
if (flowSelection?.kind !== "reactor" || !flowData.length)
|
|
406
|
+
return;
|
|
407
|
+
centerOnNode(`hdl:${flowSelection.reactorId}`);
|
|
408
|
+
}, [flowSelection, scrubberEnd, flowData, centerOnNode]);
|
|
397
409
|
return null;
|
|
398
410
|
}
|
|
399
411
|
// ---------------------------------------------------------------------------
|
|
@@ -596,5 +608,5 @@ export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
|
|
|
596
608
|
if (flowLoading) {
|
|
597
609
|
return (_jsxs("div", { className: "h-full flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border shrink-0", children: [_jsx("div", { className: "h-3 w-10 bg-muted rounded animate-pulse" }), _jsx("div", { className: "h-3 w-48 bg-muted rounded animate-pulse" })] }), _jsx("div", { className: "flex-1 flex items-center justify-center", children: _jsxs("div", { className: "animate-pulse flex flex-col items-center gap-3", children: [_jsx("div", { className: "h-8 w-40 bg-muted rounded-md" }), _jsx("div", { className: "h-6 w-px bg-muted" }), _jsx("div", { className: "h-6 w-28 bg-muted rounded-full" }), _jsxs("div", { className: "flex items-start gap-8", children: [_jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx("div", { className: "h-6 w-px bg-muted" }), _jsx("div", { className: "h-8 w-36 bg-muted rounded-md" })] }), _jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx("div", { className: "h-6 w-px bg-muted" }), _jsx("div", { className: "h-8 w-36 bg-muted rounded-md" })] })] })] }) })] }));
|
|
598
610
|
}
|
|
599
|
-
return (_jsxs("div", { className: "h-full flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2 border-b border-border shrink-0", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("h3", { className: "text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-widest", children: "Flow" }), _jsx("span", { className: "text-[10px] font-mono text-foreground/80 truncate px-1.5 py-0.5 rounded bg-white/[0.03] border border-border", children: flowWorkflowId }), _jsxs("span", { className: "text-[10px] text-muted-foreground/50 tabular-nums", children: [flowData.length, " events \u00B7 ", nodes.length, " nodes"] }), headerExtra, _jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [_jsx("button", { onClick: () => setDirection(d => d === "LR" ? "TB" : "LR"), className: "text-[10px] text-muted-foreground/60 hover:text-foreground px-2 py-1 rounded-md border border-border hover:border-indigo-500/30 transition-all duration-150", title: direction === "LR" ? "Switch to vertical layout" : "Switch to horizontal layout", children: direction === "LR" ? _jsx(ArrowDown, { size: 11, className: "inline" }) : _jsx(ArrowRight, { size: 11, className: "inline" }) }), _jsx(ReactorFilter, { allReactorIds: allReactorIds, hiddenReactors: hiddenReactors, setHiddenReactors: setHiddenReactors })] })] }), _jsx("div", { className: "flex-1 relative", children: _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, defaultEdgeOptions: { type: "smoothstep" }, onNodesChange: onNodesChange, onNodeClick: onNodeClick, onPaneClick: onPaneClick, minZoom: 0.25, proOptions: { hideAttribution: true }, nodesDraggable: false, nodesConnectable: false, elevateNodesOnSelect: false, colorMode: "dark", children: [_jsx(FitOnLoad, {}), _jsx(FocusOnSelection, { nodes: nodes, flowData: flowData }), _jsx(Background, { color: "rgba(255,255,255,0.03)", gap: 24, size: 1 }), _jsx(Controls, { showInteractive: false })] }) })] }));
|
|
611
|
+
return (_jsxs("div", { className: "h-full flex flex-col", children: [_jsxs("div", { className: "relative z-20 flex items-center gap-2.5 px-3 py-2 border-b border-border shrink-0", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("h3", { className: "text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-widest", children: "Flow" }), _jsx("span", { className: "text-[10px] font-mono text-foreground/80 truncate px-1.5 py-0.5 rounded bg-white/[0.03] border border-border", children: flowWorkflowId }), _jsxs("span", { className: "text-[10px] text-muted-foreground/50 tabular-nums", children: [flowData.length, " events \u00B7 ", nodes.length, " nodes"] }), headerExtra, _jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [_jsx("button", { onClick: () => setDirection(d => d === "LR" ? "TB" : "LR"), className: "text-[10px] text-muted-foreground/60 hover:text-foreground px-2 py-1 rounded-md border border-border hover:border-indigo-500/30 transition-all duration-150", title: direction === "LR" ? "Switch to vertical layout" : "Switch to horizontal layout", children: direction === "LR" ? _jsx(ArrowDown, { size: 11, className: "inline" }) : _jsx(ArrowRight, { size: 11, className: "inline" }) }), _jsx(ReactorFilter, { allReactorIds: allReactorIds, hiddenReactors: hiddenReactors, setHiddenReactors: setHiddenReactors })] })] }), _jsx("div", { className: "flex-1 relative", children: _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, defaultEdgeOptions: { type: "smoothstep" }, onNodesChange: onNodesChange, onNodeClick: onNodeClick, onPaneClick: onPaneClick, minZoom: 0.25, proOptions: { hideAttribution: true }, nodesDraggable: false, nodesConnectable: false, elevateNodesOnSelect: false, colorMode: "dark", children: [_jsx(FitOnLoad, {}), _jsx(FocusOnSelection, { nodes: nodes, flowData: flowData }), _jsx(Background, { color: "rgba(255,255,255,0.03)", gap: 24, size: 1 }), _jsx(Controls, { showInteractive: false })] }) })] }));
|
|
600
612
|
}
|
package/dist/panes/LogsPane.js
CHANGED
|
@@ -10,7 +10,8 @@ import { Search, ChevronRight, ChevronDown } from "lucide-react";
|
|
|
10
10
|
function LogRow({ log, showReactor }) {
|
|
11
11
|
const [expanded, setExpanded] = useState(false);
|
|
12
12
|
const levelColor = LOG_LEVEL_COLORS[log.level] ?? "bg-zinc-600/20 text-zinc-400";
|
|
13
|
-
|
|
13
|
+
const hasPayload = log.data != null;
|
|
14
|
+
return (_jsxs("div", { onClick: hasPayload ? () => setExpanded((v) => !v) : undefined, className: `px-2 py-1.5 hover:bg-white/[0.02] rounded-md transition-colors duration-100 ${hasPayload ? "cursor-pointer" : ""}`, children: [_jsxs("div", { className: "flex items-start 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 leading-[1.4]", children: formatTs(log.loggedAt) }), showReactor && (_jsx("span", { className: "text-[10px] font-mono text-muted-foreground/40 shrink-0 leading-[1.4]", children: log.reactorId })), _jsx("span", { className: `text-[11px] text-foreground/80 leading-[1.4] ${expanded ? "break-words whitespace-pre-wrap" : "truncate"}`, children: log.message }), hasPayload && (_jsx("span", { className: "ml-auto shrink-0 text-muted-foreground/50", 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
|
}
|
|
15
16
|
export function LogsPane({ onInvestigate } = {}) {
|
|
16
17
|
const logs = useSelector((s) => s.logs);
|
|
@@ -42,5 +42,8 @@ export function WorkflowExplorerPane() {
|
|
|
42
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 workflow 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: "Workflow" }), _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 && workflows.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))) })) : workflows.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32 text-xs text-muted-foreground/50 tracking-wide", children: "No workflows found" })) : (_jsx("div", { className: "flex-1 overflow-y-auto", children: workflows.map((corr) => (_jsxs("button", { onClick: () => handleRowClick(corr.workflowId), 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
43
|
color: eventTextColor(corr.rootEventType),
|
|
44
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.workflowId}`, onClick: (e) => { e.stopPropagation(); handleCopy(corr.workflowId); }, children: corr.workflowId.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: "flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold shrink-0 bg-red-500/10 text-red-400/80 border border-red-500/20", style: { boxShadow: "0 0 6px rgba(239, 68, 68, 0.15)" }, title: "
|
|
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.workflowId}`, onClick: (e) => { e.stopPropagation(); handleCopy(corr.workflowId); }, children: corr.workflowId.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: "flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold shrink-0 bg-red-500/10 text-red-400/80 border border-red-500/20 cursor-pointer hover:bg-red-500/20 hover:text-red-400 transition-colors", style: { boxShadow: "0 0 6px rgba(239, 68, 68, 0.15)" }, title: "View error \u2014 open this workflow and select the failed reactor", onClick: (e) => {
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
dispatch({ type: "ui/flow_opened", payload: { workflowId: corr.workflowId, focusError: true } });
|
|
48
|
+
}, children: "error" }))] }, corr.workflowId))) }))] }));
|
|
46
49
|
}
|
package/dist/reducer.js
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* navigation (location/changed from popstate).
|
|
5
5
|
*/
|
|
6
6
|
function applyNavigation(draft, workflowId, handler) {
|
|
7
|
+
// Keep the timeline filter coupled to the active workflow. `?workflow=X` is
|
|
8
|
+
// the single source of truth, whether it was set from the Workflows tab, a
|
|
9
|
+
// timeline row, the timeline filter pill, or a shared/reloaded URL — so the
|
|
10
|
+
// workflow filter chip always reflects it (previously only popstate/initial
|
|
11
|
+
// load did this, so in-session navigation left the chip out of sync).
|
|
12
|
+
draft.filters.workflowId = workflowId;
|
|
13
|
+
// Any navigation clears a pending error focus; flow_opened re-arms it.
|
|
14
|
+
draft.pendingErrorFocus = null;
|
|
7
15
|
// Workflow changed → reset flow state
|
|
8
16
|
if (workflowId !== draft.flowWorkflowId) {
|
|
9
17
|
if (workflowId) {
|
|
@@ -42,6 +50,22 @@ function applyNavigation(draft, workflowId, handler) {
|
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Select the first errored reactor in a workflow as the active flow node
|
|
55
|
+
* (and scope the logs to it). Returns false when the workflow's outcomes
|
|
56
|
+
* haven't loaded yet or contain no error, so the caller can defer.
|
|
57
|
+
*/
|
|
58
|
+
function selectFirstError(draft, workflowId) {
|
|
59
|
+
const outcomes = draft.outcomes[workflowId];
|
|
60
|
+
if (!outcomes)
|
|
61
|
+
return false;
|
|
62
|
+
const errored = outcomes.find((o) => o.status === "error");
|
|
63
|
+
if (!errored)
|
|
64
|
+
return false;
|
|
65
|
+
draft.flowSelection = { kind: "reactor", reactorId: errored.reactorId };
|
|
66
|
+
draft.logsFilter = { scope: "reactor", reactorId: errored.reactorId, workflowId };
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
45
69
|
function applySubjectSelected(draft, aggregateType, aggregateId, mode) {
|
|
46
70
|
draft.subjectType = aggregateType;
|
|
47
71
|
draft.subjectId = aggregateId;
|
|
@@ -50,12 +74,14 @@ function applySubjectSelected(draft, aggregateType, aggregateId, mode) {
|
|
|
50
74
|
draft.subjectChainCursor = null;
|
|
51
75
|
draft.subjectDepthCapped = false;
|
|
52
76
|
draft.subjectChainLoading = true;
|
|
53
|
-
// Mutual exclusion: clear workflow state
|
|
77
|
+
// Mutual exclusion: clear workflow state (incl. the timeline workflow filter)
|
|
54
78
|
draft.flowWorkflowId = null;
|
|
79
|
+
draft.filters.workflowId = null;
|
|
55
80
|
draft.flowData = [];
|
|
56
81
|
draft.flowSelection = null;
|
|
57
82
|
draft.causalTree = null;
|
|
58
83
|
draft.logsFilter = { scope: "reactor", reactorId: null, workflowId: null };
|
|
84
|
+
draft.pendingErrorFocus = null;
|
|
59
85
|
}
|
|
60
86
|
export const reducer = (draft, event) => {
|
|
61
87
|
switch (event.type) {
|
|
@@ -122,6 +148,11 @@ export const reducer = (draft, event) => {
|
|
|
122
148
|
case "events/outcomes_loaded": {
|
|
123
149
|
const { workflowId, outcomes } = event.payload;
|
|
124
150
|
draft.outcomes[workflowId] = outcomes;
|
|
151
|
+
// Resolve a deferred error focus now that outcomes are available.
|
|
152
|
+
if (draft.pendingErrorFocus === workflowId) {
|
|
153
|
+
selectFirstError(draft, workflowId);
|
|
154
|
+
draft.pendingErrorFocus = null;
|
|
155
|
+
}
|
|
125
156
|
break;
|
|
126
157
|
}
|
|
127
158
|
case "events/attempts_loaded": {
|
|
@@ -154,6 +185,11 @@ export const reducer = (draft, event) => {
|
|
|
154
185
|
// ── Navigation (user facts + browser popstate) ──
|
|
155
186
|
case "ui/flow_opened":
|
|
156
187
|
applyNavigation(draft, event.payload.workflowId, null);
|
|
188
|
+
// Opened via the error pill: jump straight to the failed reactor. If its
|
|
189
|
+
// outcomes are already cached, select now; otherwise defer to outcomes_loaded.
|
|
190
|
+
if (event.payload.focusError && !selectFirstError(draft, event.payload.workflowId)) {
|
|
191
|
+
draft.pendingErrorFocus = event.payload.workflowId;
|
|
192
|
+
}
|
|
157
193
|
break;
|
|
158
194
|
case "ui/flow_closed":
|
|
159
195
|
applyNavigation(draft, null, null);
|
|
@@ -168,7 +204,6 @@ export const reducer = (draft, event) => {
|
|
|
168
204
|
}
|
|
169
205
|
else {
|
|
170
206
|
applyNavigation(draft, event.payload.workflowId, event.payload.handler);
|
|
171
|
-
draft.filters.workflowId = event.payload.workflowId;
|
|
172
207
|
}
|
|
173
208
|
break;
|
|
174
209
|
// ── UI ──
|
package/dist/state.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export type InspectorState = {
|
|
|
7
7
|
flowWorkflowId: string | null;
|
|
8
8
|
flowData: InspectorEvent[];
|
|
9
9
|
flowSelection: FlowSelection;
|
|
10
|
+
/** Workflow whose first errored reactor should be auto-selected once its
|
|
11
|
+
* outcomes load (set when a flow is opened via the Workflows error pill). */
|
|
12
|
+
pendingErrorFocus: string | null;
|
|
10
13
|
scrubberStart: number | null;
|
|
11
14
|
scrubberEnd: number | null;
|
|
12
15
|
scrubberPlaying: boolean;
|
package/dist/state.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "causal-inspector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./styles.css": "./dist/causal-inspector.css"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
8
17
|
"scripts": {
|
|
9
|
-
"build": "tsc && cp src
|
|
18
|
+
"build": "tsc && cp src/CausalInspector.css dist/ && npm run build:css",
|
|
19
|
+
"build:css": "postcss src/styles.css -o dist/causal-inspector.css",
|
|
10
20
|
"typecheck": "tsc --noEmit"
|
|
11
21
|
},
|
|
12
22
|
"dependencies": {
|
|
@@ -34,10 +44,15 @@
|
|
|
34
44
|
},
|
|
35
45
|
"devDependencies": {
|
|
36
46
|
"@dagrejs/dagre": "^2.0.4",
|
|
47
|
+
"@tailwindcss/postcss": "^4.3.1",
|
|
37
48
|
"@types/react": "^19.0.0",
|
|
38
49
|
"@types/react-dom": "^19.2.3",
|
|
39
50
|
"@xyflow/react": "^12.10.1",
|
|
40
51
|
"flexlayout-react": "^0.8.19",
|
|
52
|
+
"postcss": "^8.5.15",
|
|
53
|
+
"postcss-cli": "^11.0.1",
|
|
54
|
+
"postcss-prefix-selector": "^2.1.1",
|
|
55
|
+
"tailwindcss": "^4.3.1",
|
|
41
56
|
"typescript": "^5.0.0"
|
|
42
57
|
}
|
|
43
58
|
}
|
|
@@ -1,46 +0,0 @@
|
|
|
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 WorkflowExplorerPane() {
|
|
19
|
-
const workflows = useSelector((s) => s.workflows);
|
|
20
|
-
const loading = useSelector((s) => s.workflowsLoading);
|
|
21
|
-
const dispatch = useDispatch();
|
|
22
|
-
const [search, setSearch] = useState("");
|
|
23
|
-
const searchTimerRef = useRef(null);
|
|
24
|
-
// Request workflows on mount
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
dispatch({ type: "ui/workflows_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/workflows_requested", payload: { search: value || undefined } });
|
|
34
|
-
}, 300);
|
|
35
|
-
}, [dispatch]);
|
|
36
|
-
const handleRowClick = useCallback((workflowId) => {
|
|
37
|
-
dispatch({ type: "ui/flow_opened", payload: { workflowId } });
|
|
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 workflow 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: "Workflow" }), _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 && workflows.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))) })) : workflows.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32 text-xs text-muted-foreground/50 tracking-wide", children: "No workflows found" })) : (_jsx("div", { className: "flex-1 overflow-y-auto", children: workflows.map((corr) => (_jsxs("button", { onClick: () => handleRowClick(corr.workflowId), 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.workflowId}`, onClick: (e) => { e.stopPropagation(); handleCopy(corr.workflowId); }, children: corr.workflowId.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: "flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold shrink-0 bg-red-500/10 text-red-400/80 border border-red-500/20", style: { boxShadow: "0 0 6px rgba(239, 68, 68, 0.15)" }, title: "This workflow has errors", children: "error" }))] }, corr.workflowId))) }))] }));
|
|
46
|
-
}
|