causal-inspector 0.1.5 → 0.2.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.d.ts +8 -1
- package/dist/CausalInspector.js +32 -9
- package/dist/components/EffectList.d.ts +4 -0
- package/dist/components/EffectList.js +15 -0
- package/dist/components/FilterBar.js +2 -2
- package/dist/components/GlobalScrubber.js +6 -6
- package/dist/engines/query.js +134 -53
- package/dist/engines/scrubber.js +1 -1
- package/dist/engines/url.d.ts +5 -2
- package/dist/engines/url.js +50 -22
- package/dist/events.d.ts +41 -12
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/panes/AggregateTimelinePane.js +4 -4
- package/dist/panes/CausalFlowPane.js +16 -16
- package/dist/panes/CausalTreePane.js +51 -17
- package/dist/panes/CorrelationExplorerPane.d.ts +2 -2
- package/dist/panes/CorrelationExplorerPane.js +10 -10
- package/dist/panes/LogsPane.js +6 -6
- package/dist/panes/SubjectChainPane.d.ts +1 -0
- package/dist/panes/SubjectChainPane.js +50 -0
- package/dist/panes/TimelinePane.js +32 -16
- package/dist/panes/WaterfallPane.js +185 -72
- package/dist/panes/WorkflowExplorerPane.d.ts +2 -0
- package/dist/panes/WorkflowExplorerPane.js +46 -0
- package/dist/queries.d.ts +16 -11
- package/dist/queries.js +114 -24
- package/dist/reducer.js +102 -36
- package/dist/state.d.ts +16 -5
- package/dist/state.js +17 -6
- package/dist/theme.js +1 -0
- package/dist/types.d.ts +59 -11
- package/dist/utils.js +1 -1
- package/package.json +1 -1
package/dist/events.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseEvent } from "./machine";
|
|
2
|
-
import type { InspectorEvent,
|
|
2
|
+
import type { InspectorEvent, WorkflowSummary, ReactorDependency, AggregateLifecycleEntry, FilterState, FlowSelection, ReactorDescription, ReactorDescriptionSnapshot, AggregateTimelineEntry, ReactorLog, ReactorOutcome, ReactorAttempt, PaneLayout, SubjectChainEvent, SubjectChainMode, InspectorEffect } from "./types";
|
|
3
3
|
type EventsReceived = BaseEvent<"events/received", InspectorEvent[]>;
|
|
4
4
|
type SubscriptionConnected = BaseEvent<"events/subscription_connected">;
|
|
5
5
|
type SubscriptionError = BaseEvent<"events/subscription_error", {
|
|
@@ -16,23 +16,27 @@ type CausalTreeLoaded = BaseEvent<"events/causal_tree_loaded", {
|
|
|
16
16
|
type FlowLoaded = BaseEvent<"events/flow_loaded", InspectorEvent[]>;
|
|
17
17
|
type LogsLoaded = BaseEvent<"events/logs_loaded", ReactorLog[]>;
|
|
18
18
|
type DescriptionsLoaded = BaseEvent<"events/descriptions_loaded", {
|
|
19
|
-
|
|
19
|
+
workflowId: string;
|
|
20
20
|
descriptions: ReactorDescription[];
|
|
21
21
|
}>;
|
|
22
22
|
type DescriptionSnapshotsLoaded = BaseEvent<"events/description_snapshots_loaded", {
|
|
23
|
-
|
|
23
|
+
workflowId: string;
|
|
24
24
|
snapshots: ReactorDescriptionSnapshot[];
|
|
25
25
|
}>;
|
|
26
26
|
type AggregateTimelineLoaded = BaseEvent<"events/aggregate_timeline_loaded", {
|
|
27
|
-
|
|
27
|
+
workflowId: string;
|
|
28
28
|
entries: AggregateTimelineEntry[];
|
|
29
29
|
}>;
|
|
30
30
|
type OutcomesLoaded = BaseEvent<"events/outcomes_loaded", {
|
|
31
|
-
|
|
31
|
+
workflowId: string;
|
|
32
32
|
outcomes: ReactorOutcome[];
|
|
33
33
|
}>;
|
|
34
|
-
type
|
|
35
|
-
|
|
34
|
+
type AttemptsLoaded = BaseEvent<"events/attempts_loaded", {
|
|
35
|
+
workflowId: string;
|
|
36
|
+
attempts: ReactorAttempt[];
|
|
37
|
+
}>;
|
|
38
|
+
type WorkflowsLoaded = BaseEvent<"events/workflows_loaded", {
|
|
39
|
+
workflows: WorkflowSummary[];
|
|
36
40
|
hasMore: boolean;
|
|
37
41
|
append: boolean;
|
|
38
42
|
}>;
|
|
@@ -47,7 +51,7 @@ type EventSelected = BaseEvent<"ui/event_selected", {
|
|
|
47
51
|
}>;
|
|
48
52
|
type EventDeselected = BaseEvent<"ui/event_deselected">;
|
|
49
53
|
type FlowOpened = BaseEvent<"ui/flow_opened", {
|
|
50
|
-
|
|
54
|
+
workflowId: string;
|
|
51
55
|
}>;
|
|
52
56
|
type FlowClosed = BaseEvent<"ui/flow_closed">;
|
|
53
57
|
type FlowNodeSelected = BaseEvent<"ui/flow_node_selected", FlowSelection>;
|
|
@@ -64,7 +68,7 @@ type ScrubberPlayToggled = BaseEvent<"ui/scrubber_play_toggled">;
|
|
|
64
68
|
type ScrubberSpeedChanged = BaseEvent<"ui/scrubber_speed_changed", {
|
|
65
69
|
speed: number;
|
|
66
70
|
}>;
|
|
67
|
-
type
|
|
71
|
+
type WorkflowsRequested = BaseEvent<"ui/workflows_requested", {
|
|
68
72
|
search?: string;
|
|
69
73
|
}>;
|
|
70
74
|
type HandlerSelected = BaseEvent<"ui/handler_selected", {
|
|
@@ -73,10 +77,35 @@ type HandlerSelected = BaseEvent<"ui/handler_selected", {
|
|
|
73
77
|
type AggregateLifecycleRequested = BaseEvent<"ui/aggregate_lifecycle_requested", {
|
|
74
78
|
aggregateKey: string;
|
|
75
79
|
}>;
|
|
76
|
-
type
|
|
80
|
+
type LoadMoreWorkflowsRequested = BaseEvent<"ui/load_more_workflows_requested">;
|
|
77
81
|
type LocationChanged = BaseEvent<"location/changed", {
|
|
78
|
-
|
|
82
|
+
workflowId: string | null;
|
|
79
83
|
handler: string | null;
|
|
84
|
+
subject: string | null;
|
|
85
|
+
subjectMode: SubjectChainMode | null;
|
|
86
|
+
}>;
|
|
87
|
+
type SubjectSelected = BaseEvent<"ui/subject_selected", {
|
|
88
|
+
aggregateType: string;
|
|
89
|
+
aggregateId: string;
|
|
90
|
+
mode?: SubjectChainMode;
|
|
91
|
+
}>;
|
|
92
|
+
type SubjectModeChanged = BaseEvent<"ui/subject_mode_changed", {
|
|
93
|
+
mode: SubjectChainMode;
|
|
94
|
+
}>;
|
|
95
|
+
type SubjectChainLoadMore = BaseEvent<"ui/subject_chain_load_more">;
|
|
96
|
+
type EventEffectsRequested = BaseEvent<"ui/event_effects_requested", {
|
|
97
|
+
eventId: string;
|
|
98
|
+
}>;
|
|
99
|
+
type SubjectChainLoaded = BaseEvent<"events/subject_chain_loaded", {
|
|
100
|
+
events: SubjectChainEvent[];
|
|
101
|
+
hasMore: boolean;
|
|
102
|
+
cursor: number | null;
|
|
103
|
+
depthCapped: boolean;
|
|
104
|
+
append: boolean;
|
|
105
|
+
}>;
|
|
106
|
+
type EventEffectsLoaded = BaseEvent<"events/event_effects_loaded", {
|
|
107
|
+
eventId: string;
|
|
108
|
+
effects: InspectorEffect[];
|
|
80
109
|
}>;
|
|
81
|
-
export type InspectorMachineEvent = EventsReceived | SubscriptionConnected | SubscriptionError | PageLoaded | CausalTreeLoaded | FlowLoaded | LogsLoaded | DescriptionsLoaded | DescriptionSnapshotsLoaded | AggregateTimelineLoaded | OutcomesLoaded |
|
|
110
|
+
export type InspectorMachineEvent = EventsReceived | SubscriptionConnected | SubscriptionError | PageLoaded | CausalTreeLoaded | FlowLoaded | LogsLoaded | DescriptionsLoaded | DescriptionSnapshotsLoaded | AggregateTimelineLoaded | OutcomesLoaded | AttemptsLoaded | WorkflowsLoaded | EventSelected | EventDeselected | FlowOpened | FlowClosed | FlowNodeSelected | FilterChanged | LoadMoreRequested | LayoutChanged | ScrubberStartChanged | ScrubberEndChanged | ScrubberPlayToggled | ScrubberSpeedChanged | WorkflowsRequested | ReactorDependenciesLoaded | AggregateKeysLoaded | HandlerSelected | LocationChanged | AggregateLifecycleLoaded | AggregateLifecycleRequested | LoadMoreWorkflowsRequested | SubjectSelected | SubjectModeChanged | SubjectChainLoadMore | EventEffectsRequested | SubjectChainLoaded | EventEffectsLoaded;
|
|
82
111
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,10 @@ export { reducer } from "./reducer";
|
|
|
5
5
|
export { initialState } from "./state";
|
|
6
6
|
export type { InspectorState } from "./state";
|
|
7
7
|
export type { InspectorMachineEvent } from "./events";
|
|
8
|
-
export type { InspectorEvent, InspectorEventsPage, InspectorCausalTree, InspectorCausalFlow,
|
|
8
|
+
export type { InspectorEvent, InspectorEventsPage, InspectorCausalTree, InspectorCausalFlow, WorkflowSummary, ReactorDependency, AggregateLifecycleEntry, Block, ReactorLog, ReactorDescription, ReactorDescriptionSnapshot, AggregateStateEntry, AggregateTimelineEntry, ReactorOutcome, ReactorAttempt, FilterState, LogsFilter, FlowSelection, PaneLayout, SubjectChainSourceMode, SubjectChainMode, SubjectChainEvent, InspectorEffect, AggregateKeyEntry, AggregateKeysPage, } from "./types";
|
|
9
9
|
export { createInspectorEngine, createSubscriptionEngine, createQueryEngine, createStorageEngine, } from "./engines";
|
|
10
10
|
export type { InspectorTransport, SubscriptionTransport, QueryTransport, StorageTransport, } from "./engines";
|
|
11
|
-
export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW,
|
|
11
|
+
export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_WORKFLOWS, INSPECTOR_REACTOR_LOGS, INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, INSPECTOR_SUBJECT_CHAIN, INSPECTOR_EFFECTS_FOR_EVENT, INSPECTOR_AGGREGATE_TYPES, INSPECTOR_AGGREGATE_KEYS_BY_TYPE, } from "./queries";
|
|
12
12
|
export { eventHue, eventBg, eventBorder, eventTextColor, LOG_LEVEL_COLORS, } from "./theme";
|
|
13
13
|
export { formatTs, compactPayload, copyToClipboard } from "./utils";
|
|
14
14
|
export { CausalInspector } from "./CausalInspector";
|
|
@@ -17,6 +17,7 @@ export { FilterBar } from "./components/FilterBar";
|
|
|
17
17
|
export { CopyablePayload } from "./components/CopyablePayload";
|
|
18
18
|
export { JsonSyntax } from "./components/JsonSyntax";
|
|
19
19
|
export { GlobalScrubber } from "./components/GlobalScrubber";
|
|
20
|
+
export { EffectList } from "./components/EffectList";
|
|
20
21
|
export { TimelinePane } from "./panes/TimelinePane";
|
|
21
22
|
export type { TimelinePaneProps } from "./panes/TimelinePane";
|
|
22
23
|
export { CausalTreePane } from "./panes/CausalTreePane";
|
|
@@ -27,4 +28,5 @@ export { CausalFlowPane } from "./panes/CausalFlowPane";
|
|
|
27
28
|
export type { CausalFlowPaneProps } from "./panes/CausalFlowPane";
|
|
28
29
|
export { AggregateTimelinePane } from "./panes/AggregateTimelinePane";
|
|
29
30
|
export { WaterfallPane } from "./panes/WaterfallPane";
|
|
30
|
-
export {
|
|
31
|
+
export { WorkflowExplorerPane } from "./panes/WorkflowExplorerPane";
|
|
32
|
+
export { SubjectChainPane } from "./panes/SubjectChainPane";
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { initialState } from "./state";
|
|
|
7
7
|
// ── Engines ──
|
|
8
8
|
export { createInspectorEngine, createSubscriptionEngine, createQueryEngine, createStorageEngine, } from "./engines";
|
|
9
9
|
// ── Queries ──
|
|
10
|
-
export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW,
|
|
10
|
+
export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_WORKFLOWS, INSPECTOR_REACTOR_LOGS, INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, INSPECTOR_SUBJECT_CHAIN, INSPECTOR_EFFECTS_FOR_EVENT, INSPECTOR_AGGREGATE_TYPES, INSPECTOR_AGGREGATE_KEYS_BY_TYPE, } from "./queries";
|
|
11
11
|
// ── Theme ──
|
|
12
12
|
export { eventHue, eventBg, eventBorder, eventTextColor, LOG_LEVEL_COLORS, } from "./theme";
|
|
13
13
|
// ── Utilities ──
|
|
@@ -19,6 +19,7 @@ export { FilterBar } from "./components/FilterBar";
|
|
|
19
19
|
export { CopyablePayload } from "./components/CopyablePayload";
|
|
20
20
|
export { JsonSyntax } from "./components/JsonSyntax";
|
|
21
21
|
export { GlobalScrubber } from "./components/GlobalScrubber";
|
|
22
|
+
export { EffectList } from "./components/EffectList";
|
|
22
23
|
// ── Panes ──
|
|
23
24
|
export { TimelinePane } from "./panes/TimelinePane";
|
|
24
25
|
export { CausalTreePane } from "./panes/CausalTreePane";
|
|
@@ -26,4 +27,5 @@ export { LogsPane } from "./panes/LogsPane";
|
|
|
26
27
|
export { CausalFlowPane } from "./panes/CausalFlowPane";
|
|
27
28
|
export { AggregateTimelinePane } from "./panes/AggregateTimelinePane";
|
|
28
29
|
export { WaterfallPane } from "./panes/WaterfallPane";
|
|
29
|
-
export {
|
|
30
|
+
export { WorkflowExplorerPane } from "./panes/WorkflowExplorerPane";
|
|
31
|
+
export { SubjectChainPane } from "./panes/SubjectChainPane";
|
|
@@ -144,8 +144,8 @@ function AggregateCard({ aggregateKey, state, prevState, diffMode, }) {
|
|
|
144
144
|
}, children: [_jsx("span", { style: { fontSize: 9, color: "#52525b", transform: collapsed ? "rotate(-90deg)" : "rotate(0deg)", transition: "transform 100ms" }, children: "\u25BC" }), _jsx("span", { style: { fontSize: 11, fontWeight: 500, color: "#e4e4e7" }, children: aggType }), aggId && (_jsx("span", { style: { fontSize: 10, color: "#71717a", fontFamily: "monospace" }, children: aggId })), showDiff && diffs.length > 0 && (_jsxs("span", { style: { fontSize: 9, color: "#fbbf24", marginLeft: "auto" }, children: [diffs.length, " change", diffs.length !== 1 ? "s" : ""] })), showDiff && diffs.length === 0 && (_jsx("span", { style: { fontSize: 9, color: "#52525b", marginLeft: "auto" }, children: "unchanged" }))] }), !collapsed && (_jsx("div", { style: { padding: "4px 8px 6px" }, children: showDiff ? _jsx(DiffView, { diffs: diffs }) : _jsx(InlineJson, { value: state }) }))] }));
|
|
145
145
|
}
|
|
146
146
|
export function AggregateTimelinePane() {
|
|
147
|
-
const
|
|
148
|
-
const entries = useSelector((s) =>
|
|
147
|
+
const workflowId = useSelector((s) => s.flowWorkflowId);
|
|
148
|
+
const entries = useSelector((s) => workflowId ? s.aggregateTimeline[workflowId] ?? [] : []);
|
|
149
149
|
const scrubberStart = useSelector((s) => s.scrubberStart);
|
|
150
150
|
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
151
151
|
const logsFilter = useSelector((s) => s.logsFilter);
|
|
@@ -185,11 +185,11 @@ export function AggregateTimelinePane() {
|
|
|
185
185
|
const handleRowClick = useCallback((seq) => {
|
|
186
186
|
dispatch({ type: "ui/scrubber_end_changed", payload: { end: seq } });
|
|
187
187
|
}, [dispatch]);
|
|
188
|
-
if (!
|
|
188
|
+
if (!workflowId) {
|
|
189
189
|
return (_jsx("div", { style: { height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#50506a", fontSize: 12, letterSpacing: "0.03em" }, children: "Open a flow to see the aggregate state timeline" }));
|
|
190
190
|
}
|
|
191
191
|
if (entries.length === 0) {
|
|
192
|
-
return (_jsx("div", { style: { height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#50506a", fontSize: 12, letterSpacing: "0.03em" }, children: "No aggregate state snapshots for this
|
|
192
|
+
return (_jsx("div", { style: { height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#50506a", fontSize: 12, letterSpacing: "0.03em" }, children: "No aggregate state snapshots for this workflow" }));
|
|
193
193
|
}
|
|
194
194
|
return (_jsxs("div", { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, padding: "6px 12px", borderBottom: "1px solid rgba(255,255,255,0.06)", flexShrink: 0, background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("button", { onClick: () => setDiffMode(false), style: {
|
|
195
195
|
fontSize: 10,
|
|
@@ -126,10 +126,10 @@ function buildFlowGraph(events, descriptions, outcomes, hiddenReactors, directio
|
|
|
126
126
|
children.add(groupKey);
|
|
127
127
|
reactorToChildTypes.set(evt.reactorId, children);
|
|
128
128
|
}
|
|
129
|
-
if (evt.
|
|
130
|
-
const reactors = parentToReactor.get(evt.
|
|
129
|
+
if (evt.causationId && evt.reactorId) {
|
|
130
|
+
const reactors = parentToReactor.get(evt.causationId) ?? new Set();
|
|
131
131
|
reactors.add(evt.reactorId);
|
|
132
|
-
parentToReactor.set(evt.
|
|
132
|
+
parentToReactor.set(evt.causationId, reactors);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
const eventIdToGroup = new Map();
|
|
@@ -334,8 +334,8 @@ function computeVisibleIds(allEvents, start, end) {
|
|
|
334
334
|
edgeIds.add(`hdl:${evt.reactorId}->evt:${evt.name}`);
|
|
335
335
|
}
|
|
336
336
|
// Parent event -> reactor edge
|
|
337
|
-
if (evt.
|
|
338
|
-
const parentGroup = eventIdToGroup.get(evt.
|
|
337
|
+
if (evt.causationId && evt.reactorId) {
|
|
338
|
+
const parentGroup = eventIdToGroup.get(evt.causationId);
|
|
339
339
|
if (parentGroup) {
|
|
340
340
|
edgeIds.add(`evt:${parentGroup}->hdl:${evt.reactorId}`);
|
|
341
341
|
}
|
|
@@ -343,7 +343,7 @@ function computeVisibleIds(allEvents, start, end) {
|
|
|
343
343
|
// Root event -> reactor edges
|
|
344
344
|
if (!evt.reactorId && evt.id) {
|
|
345
345
|
for (const child of visible) {
|
|
346
|
-
if (child.
|
|
346
|
+
if (child.causationId === evt.id && child.reactorId) {
|
|
347
347
|
const rootGroup = eventIdToGroup.get(evt.id);
|
|
348
348
|
if (rootGroup) {
|
|
349
349
|
edgeIds.add(`evt:${rootGroup}->hdl:${child.reactorId}`);
|
|
@@ -430,7 +430,7 @@ function ReactorFilter({ allReactorIds, hiddenReactors, setHiddenReactors }) {
|
|
|
430
430
|
return (_jsxs("div", { ref: containerRef, className: "relative", children: [_jsxs("button", { onClick: () => setOpen(v => !v), 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", children: [_jsx(Filter, { size: 11, className: "inline mr-1 -mt-px" }), hiddenCount > 0 ? `${hiddenCount} hidden` : "Filter"] }), open && (_jsxs("div", { className: "absolute top-full right-0 mt-1 z-50 border border-border rounded-lg min-w-[240px]", style: { background: "rgba(17, 17, 22, 0.95)", backdropFilter: "blur(12px)", boxShadow: "0 8px 32px rgba(0, 0, 0, 0.5)" }, children: [_jsx("div", { className: "px-3 py-2 border-b border-border", children: _jsx("input", { autoFocus: true, type: "text", value: filter, onChange: e => setFilter(e.target.value), placeholder: "Search reactors...", className: "w-full text-xs bg-transparent border-none outline-none text-foreground placeholder:text-muted-foreground/50" }) }), _jsxs("div", { className: "max-h-64 overflow-y-auto py-1", children: [filtered.map(id => (_jsxs("label", { className: "flex items-center gap-2 px-3 py-1.5 hover:bg-white/[0.03] cursor-pointer transition-colors", children: [_jsx("input", { type: "checkbox", checked: !hiddenReactors.has(id), onChange: () => toggle(id), className: "rounded border-border accent-indigo-500" }), _jsx("span", { className: "text-[11px] font-mono text-foreground/80 truncate", children: id })] }, id))), filtered.length === 0 && (_jsx("div", { className: "text-xs text-muted-foreground/50 px-3 py-2", children: "No matches" }))] })] }))] }));
|
|
431
431
|
}
|
|
432
432
|
export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
|
|
433
|
-
const
|
|
433
|
+
const flowWorkflowId = useSelector((s) => s.flowWorkflowId);
|
|
434
434
|
const flowData = useSelector((s) => s.flowData);
|
|
435
435
|
const flowSelection = useSelector((s) => s.flowSelection);
|
|
436
436
|
const descriptionsMap = useSelector((s) => s.descriptions);
|
|
@@ -438,30 +438,30 @@ export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
|
|
|
438
438
|
const scrubberStart = useSelector((s) => s.scrubberStart);
|
|
439
439
|
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
440
440
|
const dispatch = useDispatch();
|
|
441
|
-
const flowLoading =
|
|
441
|
+
const flowLoading = flowWorkflowId != null && flowData.length === 0;
|
|
442
442
|
// Build typed maps from state
|
|
443
443
|
const descriptions = useMemo(() => {
|
|
444
|
-
if (!
|
|
444
|
+
if (!flowWorkflowId)
|
|
445
445
|
return undefined;
|
|
446
|
-
const raw = descriptionsMap[
|
|
446
|
+
const raw = descriptionsMap[flowWorkflowId];
|
|
447
447
|
if (!raw)
|
|
448
448
|
return undefined;
|
|
449
449
|
const map = new Map();
|
|
450
450
|
for (const d of raw)
|
|
451
451
|
map.set(d.reactorId, d.blocks);
|
|
452
452
|
return map;
|
|
453
|
-
}, [descriptionsMap,
|
|
453
|
+
}, [descriptionsMap, flowWorkflowId]);
|
|
454
454
|
const outcomes = useMemo(() => {
|
|
455
|
-
if (!
|
|
455
|
+
if (!flowWorkflowId)
|
|
456
456
|
return undefined;
|
|
457
|
-
const raw = outcomesMap[
|
|
457
|
+
const raw = outcomesMap[flowWorkflowId];
|
|
458
458
|
if (!raw)
|
|
459
459
|
return undefined;
|
|
460
460
|
const map = new Map();
|
|
461
461
|
for (const o of raw)
|
|
462
462
|
map.set(o.reactorId, o);
|
|
463
463
|
return map;
|
|
464
|
-
}, [outcomesMap,
|
|
464
|
+
}, [outcomesMap, flowWorkflowId]);
|
|
465
465
|
const [hiddenReactors, setHiddenReactors] = useState(() => defaultHiddenReactors ?? new Set());
|
|
466
466
|
const [direction, setDirection] = useState("LR");
|
|
467
467
|
const allReactorIds = useMemo(() => {
|
|
@@ -590,11 +590,11 @@ export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
|
|
|
590
590
|
dispatch({ type: "ui/flow_node_selected", payload: null });
|
|
591
591
|
}, [dispatch]);
|
|
592
592
|
const onNodesChange = useCallback((_changes) => { }, []);
|
|
593
|
-
if (!
|
|
593
|
+
if (!flowWorkflowId) {
|
|
594
594
|
return (_jsx("div", { className: "flex items-center justify-center h-full text-xs text-muted-foreground/50 tracking-wide", children: "Select an event to visualize its causal flow" }));
|
|
595
595
|
}
|
|
596
596
|
if (flowLoading) {
|
|
597
597
|
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
598
|
}
|
|
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:
|
|
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 })] }) })] }));
|
|
600
600
|
}
|
|
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
|
3
3
|
import { useSelector, useDispatch } from "../machine";
|
|
4
4
|
import { CopyablePayload } from "../components/CopyablePayload";
|
|
5
|
+
import { EffectList } from "../components/EffectList";
|
|
5
6
|
import { eventTextColor } from "../theme";
|
|
6
7
|
import { formatTs, compactPayload, copyToClipboard, inScrubberRange } from "../utils";
|
|
7
|
-
import { Copy, Check, Search, X, ChevronRight, ChevronDown } from "lucide-react";
|
|
8
|
+
import { Copy, Check, Search, X, ChevronRight, ChevronDown, AlertTriangle, Zap } from "lucide-react";
|
|
8
9
|
function buildTreeJson(roots, childrenMap) {
|
|
9
10
|
function toNode(evt) {
|
|
10
11
|
const children = evt.id ? (childrenMap.get(evt.id) ?? []) : [];
|
|
@@ -23,7 +24,7 @@ function buildTreeJson(roots, childrenMap) {
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// ReactorNode — intermediate node grouping children by reactor_id
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
|
-
function ReactorNode({ reactorId, parentEventId, children, childrenMap, depth, isHighlighted, onClickReactor, }) {
|
|
27
|
+
function ReactorNode({ reactorId, parentEventId, children, childrenMap, depth, isHighlighted, onClickReactor, outcome, outcomesByReactor, }) {
|
|
27
28
|
const [collapsed, setCollapsed] = useState(false);
|
|
28
29
|
const nodeRef = useRef(null);
|
|
29
30
|
useEffect(() => {
|
|
@@ -34,16 +35,25 @@ function ReactorNode({ reactorId, parentEventId, children, childrenMap, depth, i
|
|
|
34
35
|
const handleClick = useCallback(() => {
|
|
35
36
|
onClickReactor(reactorId, parentEventId);
|
|
36
37
|
}, [onClickReactor, parentEventId, reactorId]);
|
|
37
|
-
|
|
38
|
+
const isError = outcome?.status === "error";
|
|
39
|
+
const isRunning = outcome?.status === "running";
|
|
40
|
+
return (_jsxs("div", { className: depth > 0 ? "pl-6" : "", children: [_jsxs("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" : ""} ${isError ? "bg-red-500/8" : ""}`, 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 italic border ${isError
|
|
41
|
+
? "bg-red-500/10 text-red-400/80 border-red-500/20"
|
|
42
|
+
: isRunning
|
|
43
|
+
? "bg-yellow-500/10 text-yellow-400/80 border-yellow-500/20"
|
|
44
|
+
: "bg-white/[0.04] text-muted-foreground/60 border-border"}`, children: "reactor" }), _jsx("span", { className: "text-[10px] font-mono text-foreground/60 shrink-0", children: reactorId }), isError && (_jsxs("span", { className: "flex items-center gap-1 text-[9px] text-red-400/80 shrink-0", title: outcome.error ?? "Error", children: [_jsx(AlertTriangle, { size: 10 }), outcome.attempts > 1 && _jsxs("span", { children: ["x", outcome.attempts] })] })), collapsed && (_jsxs("span", { className: "text-[10px] text-muted-foreground shrink-0", children: ["(", children.length, ")"] }))] })] }), isError && outcome.error && (_jsx("div", { className: "mt-1 ml-7 text-[9px] text-red-400/70 truncate", title: outcome.error, children: outcome.error }))] }), !collapsed && children.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor, outcomesByReactor: outcomesByReactor }, child.seq)))] }));
|
|
38
45
|
}
|
|
39
46
|
// ---------------------------------------------------------------------------
|
|
40
47
|
// TreeNode (recursive)
|
|
41
48
|
// ---------------------------------------------------------------------------
|
|
42
|
-
function TreeNode({ event, childrenMap, depth, onClickReactor, onInvestigate, }) {
|
|
49
|
+
function TreeNode({ event, childrenMap, depth, onClickReactor, onInvestigate, outcomesByReactor, }) {
|
|
43
50
|
const selectedSeq = useSelector((s) => s.selectedSeq);
|
|
44
51
|
const flowSelection = useSelector((s) => s.flowSelection);
|
|
52
|
+
const expandedEffects = useSelector((s) => s.expandedEffects);
|
|
53
|
+
const loadingEffectsIds = useSelector((s) => s.loadingEffects);
|
|
45
54
|
const dispatch = useDispatch();
|
|
46
55
|
const [payloadOpen, setPayloadOpen] = useState(false);
|
|
56
|
+
const [effectsOpen, setEffectsOpen] = useState(false);
|
|
47
57
|
const [collapsed, setCollapsed] = useState(false);
|
|
48
58
|
const [copied, setCopied] = useState(false);
|
|
49
59
|
const isSelected = event.seq === selectedSeq;
|
|
@@ -74,17 +84,28 @@ function TreeNode({ event, childrenMap, depth, onClickReactor, onInvestigate, })
|
|
|
74
84
|
const highlightedReactorId = flowSelection?.kind === "reactor" ? flowSelection.reactorId : null;
|
|
75
85
|
return (_jsxs("div", { className: depth > 0 ? "pl-6" : "", children: [_jsxs("div", { ref: isSelected ? nodeRef : undefined, onClick: () => {
|
|
76
86
|
dispatch({ type: "ui/event_selected", payload: { seq: event.seq } });
|
|
77
|
-
if (event.
|
|
78
|
-
dispatch({ type: "ui/flow_opened", payload: {
|
|
87
|
+
if (event.workflowId) {
|
|
88
|
+
dispatch({ type: "ui/flow_opened", payload: { workflowId: event.workflowId } });
|
|
79
89
|
}
|
|
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, ")"] })),
|
|
90
|
+
}, 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, ")"] })), event.aggregateType && event.aggregateId && (_jsxs("button", { onClick: (e) => {
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
dispatch({ type: "ui/subject_selected", payload: { aggregateType: event.aggregateType, aggregateId: event.aggregateId, mode: "both" } });
|
|
93
|
+
}, className: "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: `View subject ${event.aggregateType}:${event.aggregateId}`, children: [event.aggregateType, ":", event.aggregateId.slice(0, 8)] })), _jsx("span", { className: "text-[10px] text-muted-foreground shrink-0", children: formatTs(event.ts) }), event.id && (_jsxs("button", { onClick: (e) => {
|
|
94
|
+
e.stopPropagation();
|
|
95
|
+
if (!effectsOpen && expandedEffects[event.id] === undefined) {
|
|
96
|
+
dispatch({ type: "ui/event_effects_requested", payload: { eventId: event.id } });
|
|
97
|
+
}
|
|
98
|
+
setEffectsOpen((v) => !v);
|
|
99
|
+
}, className: `opacity-0 group-hover/tree:opacity-100 transition-all duration-150 flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] ${effectsOpen
|
|
100
|
+
? "opacity-100 bg-indigo-500/10 text-indigo-400/70 border border-indigo-500/20"
|
|
101
|
+
: "hover:bg-white/[0.05] text-muted-foreground/50 border border-transparent"}`, title: "Show effects", children: [_jsx(Zap, { size: 9 }), expandedEffects[event.id] !== undefined && expandedEffects[event.id].length > 0 && (_jsx("span", { children: expandedEffects[event.id].length }))] })), _jsx("button", { onClick: (e) => {
|
|
81
102
|
e.stopPropagation();
|
|
82
103
|
const json = buildTreeJson([event], childrenMap);
|
|
83
104
|
const text = JSON.stringify(json[0], null, 2);
|
|
84
105
|
copyToClipboard(text);
|
|
85
106
|
setCopied(true);
|
|
86
107
|
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)))] }))] }));
|
|
108
|
+
}, 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" })), effectsOpen && (_jsx("div", { className: "mt-1 ml-3", children: loadingEffectsIds.includes(event.id ?? "") ? (_jsx("div", { className: "text-[9px] text-muted-foreground/40 italic py-1", children: "Loading\u2026" })) : (_jsx(EffectList, { effects: event.id ? (expandedEffects[event.id] ?? []) : [] })) }))] }), !collapsed && (_jsxs(_Fragment, { children: [directChildren.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor, onInvestigate: onInvestigate, outcomesByReactor: outcomesByReactor }, 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, outcome: outcomesByReactor?.get(hid), outcomesByReactor: outcomesByReactor }, hid)))] }))] }));
|
|
88
109
|
}
|
|
89
110
|
// ---------------------------------------------------------------------------
|
|
90
111
|
// matchesFlowSelection
|
|
@@ -100,10 +121,23 @@ export function CausalTreePane({ onInvestigate } = {}) {
|
|
|
100
121
|
const causalTree = useSelector((s) => s.causalTree);
|
|
101
122
|
const selectedSeq = useSelector((s) => s.selectedSeq);
|
|
102
123
|
const flowSelection = useSelector((s) => s.flowSelection);
|
|
103
|
-
const
|
|
124
|
+
const flowWorkflowId = useSelector((s) => s.flowWorkflowId);
|
|
104
125
|
const scrubberStart = useSelector((s) => s.scrubberStart);
|
|
105
126
|
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
127
|
+
const outcomesMap = useSelector((s) => s.outcomes);
|
|
106
128
|
const dispatch = useDispatch();
|
|
129
|
+
// Build reactor outcome lookup for current flow
|
|
130
|
+
const outcomesByReactor = useMemo(() => {
|
|
131
|
+
if (!flowWorkflowId)
|
|
132
|
+
return new Map();
|
|
133
|
+
const raw = outcomesMap[flowWorkflowId];
|
|
134
|
+
if (!raw)
|
|
135
|
+
return new Map();
|
|
136
|
+
const map = new Map();
|
|
137
|
+
for (const o of raw)
|
|
138
|
+
map.set(o.reactorId, o);
|
|
139
|
+
return map;
|
|
140
|
+
}, [outcomesMap, flowWorkflowId]);
|
|
107
141
|
const treeEvents = useMemo(() => {
|
|
108
142
|
const all = causalTree?.events ?? null;
|
|
109
143
|
if (all == null || (scrubberStart == null && scrubberEnd == null))
|
|
@@ -112,35 +146,35 @@ export function CausalTreePane({ onInvestigate } = {}) {
|
|
|
112
146
|
}, [causalTree?.events, scrubberStart, scrubberEnd]);
|
|
113
147
|
const treeLoading = selectedSeq != null && causalTree == null;
|
|
114
148
|
const onClickReactor = useCallback((reactorId, _parentEventId) => {
|
|
115
|
-
if (
|
|
149
|
+
if (flowWorkflowId) {
|
|
116
150
|
dispatch({ type: "ui/flow_node_selected", payload: { kind: "reactor", reactorId } });
|
|
117
151
|
}
|
|
118
152
|
dispatch({ type: "ui/handler_selected", payload: { reactorId } });
|
|
119
|
-
}, [
|
|
153
|
+
}, [flowWorkflowId, dispatch]);
|
|
120
154
|
const { roots, childrenMap, totalCount, filteredCount } = useMemo(() => {
|
|
121
155
|
if (!treeEvents || treeEvents.length === 0)
|
|
122
156
|
return { roots: [], childrenMap: new Map(), totalCount: 0, filteredCount: 0 };
|
|
123
157
|
const total = treeEvents.length;
|
|
124
|
-
const events = (
|
|
158
|
+
const events = (flowWorkflowId && flowSelection)
|
|
125
159
|
? treeEvents.filter(e => matchesFlowSelection(e, flowSelection))
|
|
126
160
|
: treeEvents;
|
|
127
161
|
const idSet = new Set(events.map(e => e.id).filter(Boolean));
|
|
128
162
|
const cMap = new Map();
|
|
129
163
|
const rootList = [];
|
|
130
164
|
for (const evt of events) {
|
|
131
|
-
if (evt.
|
|
165
|
+
if (evt.causationId == null || !idSet.has(evt.causationId)) {
|
|
132
166
|
rootList.push(evt);
|
|
133
167
|
}
|
|
134
168
|
else {
|
|
135
|
-
const siblings = cMap.get(evt.
|
|
169
|
+
const siblings = cMap.get(evt.causationId) ?? [];
|
|
136
170
|
siblings.push(evt);
|
|
137
|
-
cMap.set(evt.
|
|
171
|
+
cMap.set(evt.causationId, siblings);
|
|
138
172
|
}
|
|
139
173
|
}
|
|
140
174
|
rootList.sort((a, b) => a.seq - b.seq);
|
|
141
175
|
const filtered = rootList.length + [...cMap.values()].reduce((s, a) => s + a.length, 0);
|
|
142
176
|
return { roots: rootList, childrenMap: cMap, totalCount: total, filteredCount: filtered };
|
|
143
|
-
}, [treeEvents,
|
|
177
|
+
}, [treeEvents, flowWorkflowId, flowSelection]);
|
|
144
178
|
if (treeLoading) {
|
|
145
179
|
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
180
|
}
|
|
@@ -154,5 +188,5 @@ export function CausalTreePane({ onInvestigate } = {}) {
|
|
|
154
188
|
}
|
|
155
189
|
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
190
|
? 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)))] }));
|
|
191
|
+
: `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, outcomesByReactor: outcomesByReactor }, root.seq)))] }));
|
|
158
192
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type
|
|
2
|
-
export declare function
|
|
1
|
+
export type WorkflowExplorerPaneProps = Record<string, never>;
|
|
2
|
+
export declare function WorkflowExplorerPane(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -15,32 +15,32 @@ function RelativeDuration({ firstTs, lastTs }) {
|
|
|
15
15
|
return _jsxs("span", { children: [(diffMs / 60_000).toFixed(1), "m"] });
|
|
16
16
|
return _jsxs("span", { children: [(diffMs / 3_600_000).toFixed(1), "h"] });
|
|
17
17
|
}
|
|
18
|
-
export function
|
|
19
|
-
const
|
|
20
|
-
const loading = useSelector((s) => s.
|
|
18
|
+
export function WorkflowExplorerPane() {
|
|
19
|
+
const workflows = useSelector((s) => s.workflows);
|
|
20
|
+
const loading = useSelector((s) => s.workflowsLoading);
|
|
21
21
|
const dispatch = useDispatch();
|
|
22
22
|
const [search, setSearch] = useState("");
|
|
23
23
|
const searchTimerRef = useRef(null);
|
|
24
|
-
// Request
|
|
24
|
+
// Request workflows on mount
|
|
25
25
|
useEffect(() => {
|
|
26
|
-
dispatch({ type: "ui/
|
|
26
|
+
dispatch({ type: "ui/workflows_requested", payload: {} });
|
|
27
27
|
}, [dispatch]);
|
|
28
28
|
const handleSearchChange = useCallback((value) => {
|
|
29
29
|
setSearch(value);
|
|
30
30
|
if (searchTimerRef.current)
|
|
31
31
|
clearTimeout(searchTimerRef.current);
|
|
32
32
|
searchTimerRef.current = setTimeout(() => {
|
|
33
|
-
dispatch({ type: "ui/
|
|
33
|
+
dispatch({ type: "ui/workflows_requested", payload: { search: value || undefined } });
|
|
34
34
|
}, 300);
|
|
35
35
|
}, [dispatch]);
|
|
36
|
-
const handleRowClick = useCallback((
|
|
37
|
-
dispatch({ type: "ui/flow_opened", payload: {
|
|
36
|
+
const handleRowClick = useCallback((workflowId) => {
|
|
37
|
+
dispatch({ type: "ui/flow_opened", payload: { workflowId } });
|
|
38
38
|
}, [dispatch]);
|
|
39
39
|
const handleCopy = useCallback((text) => {
|
|
40
40
|
navigator.clipboard.writeText(text).catch(() => { });
|
|
41
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
|
|
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.
|
|
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
46
|
}
|
package/dist/panes/LogsPane.js
CHANGED
|
@@ -10,7 +10,7 @@ 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", { className: "px-2 py-1.5 hover:bg-white/[0.02] rounded-md transition-colors duration-100", children: [_jsxs("div", { className: "flex items-
|
|
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) }))] }));
|
|
14
14
|
}
|
|
15
15
|
export function LogsPane({ onInvestigate } = {}) {
|
|
16
16
|
const logs = useSelector((s) => s.logs);
|
|
@@ -18,10 +18,10 @@ export function LogsPane({ onInvestigate } = {}) {
|
|
|
18
18
|
const flowData = useSelector((s) => s.flowData);
|
|
19
19
|
const scrubberStart = useSelector((s) => s.scrubberStart);
|
|
20
20
|
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
21
|
-
const [levelFilter, setLevelFilter] = useState(new Set(["debug", "info", "warn"]));
|
|
21
|
+
const [levelFilter, setLevelFilter] = useState(new Set(["debug", "info", "warn", "error"]));
|
|
22
22
|
const [searchText, setSearchText] = useState("");
|
|
23
|
-
const
|
|
24
|
-
const hasFilter = logsFilter.reactorId != null ||
|
|
23
|
+
const isWorkflowScope = logsFilter.scope === "workflow" && logsFilter.workflowId != null;
|
|
24
|
+
const hasFilter = logsFilter.reactorId != null || isWorkflowScope;
|
|
25
25
|
// Set of event IDs visible within scrubber range
|
|
26
26
|
const visibleEventIds = useMemo(() => {
|
|
27
27
|
if (scrubberStart == null && scrubberEnd == null)
|
|
@@ -59,7 +59,7 @@ export function LogsPane({ onInvestigate } = {}) {
|
|
|
59
59
|
return next;
|
|
60
60
|
});
|
|
61
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)
|
|
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", "error"].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
63
|
? LOG_LEVEL_COLORS[level]
|
|
64
|
-
: "text-muted-foreground/30 line-through"}`, children: level }, level))) }), _jsxs("div", { className: "relative flex items-center", 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 }),
|
|
64
|
+
: "text-muted-foreground/30 line-through"}`, children: level }, level))) }), _jsxs("div", { className: "relative flex items-center", 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 }), isWorkflowScope && _jsx("span", { className: "ml-1", children: "(all reactors in workflow)" })] })) : (_jsx("span", { children: "All reactors in workflow" })), _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: isWorkflowScope }, i)))] })] }));
|
|
65
65
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function SubjectChainPane(): import("react/jsx-runtime").JSX.Element;
|