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/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
- const nodeId = `evt:${evt.name}`;
389
- const node = nodesRef.current.find(n => n.id === nodeId);
390
- if (!node)
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
- const isReactor = node.id.startsWith("hdl:");
393
- const w = isReactor ? REACTOR_WIDTH : NODE_WIDTH;
394
- const h = isReactor ? estimateReactorHeight(node.data) : NODE_HEIGHT;
395
- setCenter(node.position.x + w / 2, node.position.y + h / 2, { zoom: getZoom(), duration: 400 });
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
  }
@@ -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
- return (_jsxs("div", { onClick: () => setExpanded((v) => !v), className: "px-2 py-1.5 hover:bg-white/[0.02] rounded-md transition-colors duration-100 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 }), _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) }))] }));
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: "This workflow has errors", children: "error" }))] }, corr.workflowId))) }))] }));
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
@@ -6,6 +6,7 @@ export const initialState = {
6
6
  flowWorkflowId: null,
7
7
  flowData: [],
8
8
  flowSelection: null,
9
+ pendingErrorFocus: null,
9
10
  scrubberStart: null,
10
11
  scrubberEnd: null,
11
12
  scrubberPlaying: false,
package/package.json CHANGED
@@ -1,12 +1,22 @@
1
1
  {
2
2
  "name": "causal-inspector",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
- "files": ["dist"],
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/*.css dist/",
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,2 +0,0 @@
1
- export type WorkflowExplorerPaneProps = Record<string, never>;
2
- export declare function WorkflowExplorerPane(): import("react/jsx-runtime").JSX.Element;
@@ -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
- }