causal-inspector 0.1.6 → 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/CausalInspector.css +20 -447
- package/dist/CausalInspector.d.ts +8 -1
- package/dist/CausalInspector.js +32 -9
- package/dist/causal-inspector.css +2899 -0
- package/dist/components/CopyablePayload.js +8 -8
- package/dist/components/EffectList.d.ts +4 -0
- package/dist/components/EffectList.js +15 -0
- package/dist/components/FilterBar.js +7 -10
- package/dist/components/GlobalScrubber.js +6 -6
- package/dist/components/JsonSyntax.js +8 -8
- package/dist/engines/query.js +131 -52
- 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 +39 -13
- 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 +39 -27
- package/dist/panes/CausalTreePane.js +43 -34
- package/dist/panes/LogsPane.js +9 -17
- package/dist/panes/SubjectChainPane.d.ts +1 -0
- package/dist/panes/SubjectChainPane.js +50 -0
- package/dist/panes/TimelinePane.js +33 -19
- package/dist/panes/WaterfallPane.js +5 -5
- package/dist/panes/WorkflowExplorerPane.d.ts +2 -0
- package/dist/panes/WorkflowExplorerPane.js +49 -0
- package/dist/queries.d.ts +16 -12
- package/dist/queries.js +103 -27
- package/dist/reducer.js +134 -38
- package/dist/state.d.ts +18 -5
- package/dist/state.js +17 -8
- package/dist/theme.js +4 -4
- package/dist/types.d.ts +52 -12
- package/dist/utils.js +1 -1
- package/package.json +18 -3
- package/dist/panes/CorrelationExplorerPane.d.ts +0 -2
- package/dist/panes/CorrelationExplorerPane.js +0 -51
package/dist/reducer.js
CHANGED
|
@@ -3,24 +3,32 @@
|
|
|
3
3
|
* (ui/flow_opened, ui/handler_selected) and browser-initiated
|
|
4
4
|
* navigation (location/changed from popstate).
|
|
5
5
|
*/
|
|
6
|
-
function applyNavigation(draft,
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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;
|
|
15
|
+
// Workflow changed → reset flow state
|
|
16
|
+
if (workflowId !== draft.flowWorkflowId) {
|
|
17
|
+
if (workflowId) {
|
|
18
|
+
draft.flowWorkflowId = workflowId;
|
|
11
19
|
draft.flowData = [];
|
|
12
20
|
draft.flowSelection = null;
|
|
13
21
|
draft.scrubberStart = null;
|
|
14
22
|
draft.scrubberEnd = null;
|
|
15
23
|
draft.scrubberPlaying = false;
|
|
16
24
|
draft.logsFilter = {
|
|
17
|
-
scope: "
|
|
25
|
+
scope: "workflow",
|
|
18
26
|
reactorId: null,
|
|
19
|
-
|
|
27
|
+
workflowId,
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
else {
|
|
23
|
-
draft.
|
|
31
|
+
draft.flowWorkflowId = null;
|
|
24
32
|
draft.flowData = [];
|
|
25
33
|
draft.flowSelection = null;
|
|
26
34
|
draft.scrubberStart = null;
|
|
@@ -29,7 +37,7 @@ function applyNavigation(draft, correlationId, handler) {
|
|
|
29
37
|
draft.logsFilter = {
|
|
30
38
|
scope: "reactor",
|
|
31
39
|
reactorId: null,
|
|
32
|
-
|
|
40
|
+
workflowId: null,
|
|
33
41
|
};
|
|
34
42
|
}
|
|
35
43
|
}
|
|
@@ -38,24 +46,57 @@ function applyNavigation(draft, correlationId, handler) {
|
|
|
38
46
|
draft.logsFilter = {
|
|
39
47
|
scope: "reactor",
|
|
40
48
|
reactorId: handler,
|
|
41
|
-
|
|
49
|
+
workflowId: draft.flowWorkflowId,
|
|
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
|
+
}
|
|
69
|
+
function applySubjectSelected(draft, aggregateType, aggregateId, mode) {
|
|
70
|
+
draft.subjectType = aggregateType;
|
|
71
|
+
draft.subjectId = aggregateId;
|
|
72
|
+
draft.subjectMode = mode;
|
|
73
|
+
draft.subjectChain = [];
|
|
74
|
+
draft.subjectChainCursor = null;
|
|
75
|
+
draft.subjectDepthCapped = false;
|
|
76
|
+
draft.subjectChainLoading = true;
|
|
77
|
+
// Mutual exclusion: clear workflow state (incl. the timeline workflow filter)
|
|
78
|
+
draft.flowWorkflowId = null;
|
|
79
|
+
draft.filters.workflowId = null;
|
|
80
|
+
draft.flowData = [];
|
|
81
|
+
draft.flowSelection = null;
|
|
82
|
+
draft.causalTree = null;
|
|
83
|
+
draft.logsFilter = { scope: "reactor", reactorId: null, workflowId: null };
|
|
84
|
+
draft.pendingErrorFocus = null;
|
|
85
|
+
}
|
|
45
86
|
export const reducer = (draft, event) => {
|
|
46
87
|
switch (event.type) {
|
|
47
88
|
// ── Subscription ──
|
|
48
89
|
case "events/received": {
|
|
49
90
|
const newEvents = event.payload;
|
|
50
91
|
const filtered = newEvents.filter((e) => {
|
|
51
|
-
if (draft.filters.
|
|
92
|
+
if (draft.filters.workflowId && e.workflowId !== draft.filters.workflowId) {
|
|
52
93
|
return false;
|
|
53
94
|
}
|
|
54
95
|
if (draft.filters.search) {
|
|
55
96
|
const s = draft.filters.search.toLowerCase();
|
|
56
97
|
const matches = e.name.toLowerCase().includes(s) ||
|
|
57
98
|
e.payload.toLowerCase().includes(s) ||
|
|
58
|
-
(e.
|
|
99
|
+
(e.workflowId ?? "").toLowerCase().includes(s);
|
|
59
100
|
if (!matches)
|
|
60
101
|
return false;
|
|
61
102
|
}
|
|
@@ -90,40 +131,45 @@ export const reducer = (draft, event) => {
|
|
|
90
131
|
draft.logs = event.payload;
|
|
91
132
|
break;
|
|
92
133
|
case "events/descriptions_loaded": {
|
|
93
|
-
const {
|
|
94
|
-
draft.descriptions[
|
|
134
|
+
const { workflowId, descriptions } = event.payload;
|
|
135
|
+
draft.descriptions[workflowId] = descriptions;
|
|
95
136
|
break;
|
|
96
137
|
}
|
|
97
138
|
case "events/description_snapshots_loaded": {
|
|
98
|
-
const {
|
|
99
|
-
draft.descriptionSnapshots[
|
|
139
|
+
const { workflowId, snapshots } = event.payload;
|
|
140
|
+
draft.descriptionSnapshots[workflowId] = snapshots;
|
|
100
141
|
break;
|
|
101
142
|
}
|
|
102
143
|
case "events/aggregate_timeline_loaded": {
|
|
103
|
-
const {
|
|
104
|
-
draft.aggregateTimeline[
|
|
144
|
+
const { workflowId, entries } = event.payload;
|
|
145
|
+
draft.aggregateTimeline[workflowId] = entries;
|
|
105
146
|
break;
|
|
106
147
|
}
|
|
107
148
|
case "events/outcomes_loaded": {
|
|
108
|
-
const {
|
|
109
|
-
draft.outcomes[
|
|
149
|
+
const { workflowId, outcomes } = event.payload;
|
|
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
|
+
}
|
|
110
156
|
break;
|
|
111
157
|
}
|
|
112
158
|
case "events/attempts_loaded": {
|
|
113
|
-
const {
|
|
114
|
-
draft.attempts[
|
|
159
|
+
const { workflowId, attempts } = event.payload;
|
|
160
|
+
draft.attempts[workflowId] = attempts;
|
|
115
161
|
break;
|
|
116
162
|
}
|
|
117
|
-
case "events/
|
|
118
|
-
const {
|
|
163
|
+
case "events/workflows_loaded": {
|
|
164
|
+
const { workflows, hasMore, append } = event.payload;
|
|
119
165
|
if (append) {
|
|
120
|
-
draft.
|
|
166
|
+
draft.workflows.push(...workflows);
|
|
121
167
|
}
|
|
122
168
|
else {
|
|
123
|
-
draft.
|
|
169
|
+
draft.workflows = workflows;
|
|
124
170
|
}
|
|
125
|
-
draft.
|
|
126
|
-
draft.
|
|
171
|
+
draft.workflowsHasMore = hasMore;
|
|
172
|
+
draft.workflowsLoading = false;
|
|
127
173
|
break;
|
|
128
174
|
}
|
|
129
175
|
case "events/reactor_dependencies_loaded":
|
|
@@ -138,17 +184,27 @@ export const reducer = (draft, event) => {
|
|
|
138
184
|
break;
|
|
139
185
|
// ── Navigation (user facts + browser popstate) ──
|
|
140
186
|
case "ui/flow_opened":
|
|
141
|
-
applyNavigation(draft, event.payload.
|
|
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
|
+
}
|
|
142
193
|
break;
|
|
143
194
|
case "ui/flow_closed":
|
|
144
195
|
applyNavigation(draft, null, null);
|
|
145
196
|
break;
|
|
146
197
|
case "ui/handler_selected":
|
|
147
|
-
applyNavigation(draft, draft.
|
|
198
|
+
applyNavigation(draft, draft.flowWorkflowId, event.payload.reactorId);
|
|
148
199
|
break;
|
|
149
200
|
case "location/changed":
|
|
150
|
-
|
|
151
|
-
|
|
201
|
+
if (event.payload.subject) {
|
|
202
|
+
const [subjectType, subjectId] = event.payload.subject.split(/:(.+)/);
|
|
203
|
+
applySubjectSelected(draft, subjectType, subjectId, event.payload.subjectMode ?? "both");
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
applyNavigation(draft, event.payload.workflowId, event.payload.handler);
|
|
207
|
+
}
|
|
152
208
|
break;
|
|
153
209
|
// ── UI ──
|
|
154
210
|
case "ui/event_selected":
|
|
@@ -163,9 +219,9 @@ export const reducer = (draft, event) => {
|
|
|
163
219
|
// Clear reactor filter when deselecting a node
|
|
164
220
|
if (event.payload == null && draft.logsFilter.reactorId != null) {
|
|
165
221
|
draft.logsFilter = {
|
|
166
|
-
scope: "
|
|
222
|
+
scope: "workflow",
|
|
167
223
|
reactorId: null,
|
|
168
|
-
|
|
224
|
+
workflowId: draft.flowWorkflowId,
|
|
169
225
|
};
|
|
170
226
|
}
|
|
171
227
|
break;
|
|
@@ -190,11 +246,51 @@ export const reducer = (draft, event) => {
|
|
|
190
246
|
case "ui/scrubber_speed_changed":
|
|
191
247
|
draft.scrubberSpeed = event.payload.speed;
|
|
192
248
|
break;
|
|
193
|
-
case "ui/
|
|
194
|
-
draft.
|
|
249
|
+
case "ui/workflows_requested":
|
|
250
|
+
draft.workflowsLoading = true;
|
|
251
|
+
break;
|
|
252
|
+
case "ui/load_more_workflows_requested":
|
|
253
|
+
draft.workflowsLoading = true;
|
|
254
|
+
break;
|
|
255
|
+
// ── Entity-scoped inspection ──
|
|
256
|
+
case "ui/subject_selected":
|
|
257
|
+
applySubjectSelected(draft, event.payload.aggregateType, event.payload.aggregateId, event.payload.mode ?? "both");
|
|
258
|
+
break;
|
|
259
|
+
case "ui/subject_mode_changed":
|
|
260
|
+
draft.subjectMode = event.payload.mode;
|
|
261
|
+
draft.subjectChain = [];
|
|
262
|
+
draft.subjectChainCursor = null;
|
|
263
|
+
draft.subjectChainLoading = true;
|
|
195
264
|
break;
|
|
196
|
-
case "ui/
|
|
197
|
-
draft.
|
|
265
|
+
case "ui/subject_chain_load_more":
|
|
266
|
+
draft.subjectChainLoading = true;
|
|
198
267
|
break;
|
|
268
|
+
case "ui/event_effects_requested": {
|
|
269
|
+
const { eventId } = event.payload;
|
|
270
|
+
if (!draft.loadingEffects.includes(eventId) && !(eventId in draft.expandedEffects)) {
|
|
271
|
+
draft.loadingEffects.push(eventId);
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "events/subject_chain_loaded": {
|
|
276
|
+
const { events, hasMore, cursor, depthCapped, append } = event.payload;
|
|
277
|
+
if (append) {
|
|
278
|
+
draft.subjectChain.push(...events);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
draft.subjectChain = events;
|
|
282
|
+
}
|
|
283
|
+
draft.subjectChainHasMore = hasMore;
|
|
284
|
+
draft.subjectChainCursor = cursor;
|
|
285
|
+
draft.subjectDepthCapped = depthCapped;
|
|
286
|
+
draft.subjectChainLoading = false;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case "events/event_effects_loaded": {
|
|
290
|
+
const { eventId, effects } = event.payload;
|
|
291
|
+
draft.expandedEffects[eventId] = effects;
|
|
292
|
+
draft.loadingEffects = draft.loadingEffects.filter(id => id !== eventId);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
199
295
|
}
|
|
200
296
|
};
|
package/dist/state.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import type { InspectorEvent,
|
|
1
|
+
import type { InspectorEvent, WorkflowSummary, ReactorDependency, AggregateLifecycleEntry, FilterState, FlowSelection, ReactorDescription, ReactorDescriptionSnapshot, AggregateTimelineEntry, ReactorLog, ReactorOutcome, ReactorAttempt, LogsFilter, PaneLayout, SubjectChainEvent, SubjectChainMode, InspectorEffect } from "./types";
|
|
2
2
|
export type InspectorState = {
|
|
3
3
|
events: InspectorEvent[];
|
|
4
4
|
hasMore: boolean;
|
|
5
5
|
loading: boolean;
|
|
6
6
|
selectedSeq: number | null;
|
|
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;
|
|
@@ -23,14 +26,24 @@ export type InspectorState = {
|
|
|
23
26
|
aggregateTimeline: Record<string, AggregateTimelineEntry[]>;
|
|
24
27
|
outcomes: Record<string, ReactorOutcome[]>;
|
|
25
28
|
attempts: Record<string, ReactorAttempt[]>;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
workflows: WorkflowSummary[];
|
|
30
|
+
workflowsLoading: boolean;
|
|
31
|
+
workflowsHasMore: boolean;
|
|
29
32
|
reactorDependencies: ReactorDependency[];
|
|
30
33
|
aggregateKeys: string[];
|
|
31
34
|
aggregateLifecycle: AggregateLifecycleEntry[];
|
|
32
35
|
aggregateLifecycleKey: string | null;
|
|
33
36
|
subscription: "connected" | "disconnected" | "error";
|
|
34
37
|
paneLayout: PaneLayout | null;
|
|
38
|
+
subjectType: string | null;
|
|
39
|
+
subjectId: string | null;
|
|
40
|
+
subjectMode: SubjectChainMode;
|
|
41
|
+
subjectChain: SubjectChainEvent[];
|
|
42
|
+
subjectChainLoading: boolean;
|
|
43
|
+
subjectChainHasMore: boolean;
|
|
44
|
+
subjectChainCursor: number | null;
|
|
45
|
+
subjectDepthCapped: boolean;
|
|
46
|
+
expandedEffects: Record<string, InspectorEffect[]>;
|
|
47
|
+
loadingEffects: string[];
|
|
35
48
|
};
|
|
36
49
|
export declare const initialState: InspectorState;
|
package/dist/state.js
CHANGED
|
@@ -3,9 +3,10 @@ export const initialState = {
|
|
|
3
3
|
hasMore: true,
|
|
4
4
|
loading: false,
|
|
5
5
|
selectedSeq: null,
|
|
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,
|
|
@@ -13,29 +14,37 @@ export const initialState = {
|
|
|
13
14
|
causalTree: null,
|
|
14
15
|
filters: {
|
|
15
16
|
search: "",
|
|
16
|
-
|
|
17
|
+
workflowId: null,
|
|
17
18
|
aggregateKey: null,
|
|
18
|
-
from: null,
|
|
19
|
-
to: null,
|
|
20
19
|
},
|
|
21
20
|
logs: [],
|
|
22
21
|
logsFilter: {
|
|
23
22
|
scope: "reactor",
|
|
24
23
|
reactorId: null,
|
|
25
|
-
|
|
24
|
+
workflowId: null,
|
|
26
25
|
},
|
|
27
26
|
descriptions: {},
|
|
28
27
|
descriptionSnapshots: {},
|
|
29
28
|
aggregateTimeline: {},
|
|
30
29
|
outcomes: {},
|
|
31
30
|
attempts: {},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
workflows: [],
|
|
32
|
+
workflowsLoading: false,
|
|
33
|
+
workflowsHasMore: true,
|
|
35
34
|
reactorDependencies: [],
|
|
36
35
|
aggregateKeys: [],
|
|
37
36
|
aggregateLifecycle: [],
|
|
38
37
|
aggregateLifecycleKey: null,
|
|
39
38
|
subscription: "disconnected",
|
|
40
39
|
paneLayout: null,
|
|
40
|
+
subjectType: null,
|
|
41
|
+
subjectId: null,
|
|
42
|
+
subjectMode: "both",
|
|
43
|
+
subjectChain: [],
|
|
44
|
+
subjectChainLoading: false,
|
|
45
|
+
subjectChainHasMore: false,
|
|
46
|
+
subjectChainCursor: null,
|
|
47
|
+
subjectDepthCapped: false,
|
|
48
|
+
expandedEffects: {},
|
|
49
|
+
loadingEffects: [],
|
|
41
50
|
};
|
package/dist/theme.js
CHANGED
|
@@ -28,8 +28,8 @@ function hslToRgb(h, s, l) {
|
|
|
28
28
|
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
|
|
29
29
|
}
|
|
30
30
|
export const LOG_LEVEL_COLORS = {
|
|
31
|
-
debug: "
|
|
32
|
-
info: "
|
|
33
|
-
warn: "
|
|
34
|
-
error: "
|
|
31
|
+
debug: "bg-zinc-600/20 text-zinc-400",
|
|
32
|
+
info: "bg-indigo-500/15 text-indigo-400",
|
|
33
|
+
warn: "bg-amber-500/15 text-amber-400",
|
|
34
|
+
error: "bg-red-500/15 text-red-400",
|
|
35
35
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -5,12 +5,12 @@ export type InspectorEvent = {
|
|
|
5
5
|
type: string;
|
|
6
6
|
name: string;
|
|
7
7
|
id: string | null;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
causationId: string | null;
|
|
9
|
+
workflowId: string | null;
|
|
10
10
|
reactorId: string | null;
|
|
11
11
|
aggregateType: string | null;
|
|
12
12
|
aggregateId: string | null;
|
|
13
|
-
|
|
13
|
+
streamRevision: number | null;
|
|
14
14
|
summary: string | null;
|
|
15
15
|
payload: string;
|
|
16
16
|
};
|
|
@@ -78,7 +78,7 @@ export type ReactorOutcome = {
|
|
|
78
78
|
export type ReactorAttempt = {
|
|
79
79
|
eventId: string;
|
|
80
80
|
reactorId: string;
|
|
81
|
-
|
|
81
|
+
workflowId: string;
|
|
82
82
|
attempt: number;
|
|
83
83
|
status: string;
|
|
84
84
|
error: string | null;
|
|
@@ -111,29 +111,31 @@ export type AggregateLifecycleEntry = {
|
|
|
111
111
|
eventId: string;
|
|
112
112
|
eventType: string;
|
|
113
113
|
ts: string;
|
|
114
|
-
|
|
114
|
+
workflowId: string;
|
|
115
115
|
aggregateKey: string;
|
|
116
116
|
state: unknown;
|
|
117
117
|
};
|
|
118
|
-
export type
|
|
119
|
-
|
|
118
|
+
export type WorkflowSummary = {
|
|
119
|
+
workflowId: string;
|
|
120
120
|
eventCount: number;
|
|
121
121
|
firstTs: string;
|
|
122
122
|
lastTs: string;
|
|
123
123
|
rootEventType: string;
|
|
124
124
|
hasErrors: boolean;
|
|
125
125
|
};
|
|
126
|
+
export type WorkflowSummaryPage = {
|
|
127
|
+
workflows: WorkflowSummary[];
|
|
128
|
+
nextCursor: string | null;
|
|
129
|
+
};
|
|
126
130
|
export type FilterState = {
|
|
127
131
|
search: string;
|
|
128
|
-
|
|
132
|
+
workflowId: string | null;
|
|
129
133
|
aggregateKey: string | null;
|
|
130
|
-
from: string | null;
|
|
131
|
-
to: string | null;
|
|
132
134
|
};
|
|
133
135
|
export type LogsFilter = {
|
|
134
|
-
scope: "reactor" | "
|
|
136
|
+
scope: "reactor" | "workflow";
|
|
135
137
|
reactorId: string | null;
|
|
136
|
-
|
|
138
|
+
workflowId: string | null;
|
|
137
139
|
};
|
|
138
140
|
export type FlowSelection = {
|
|
139
141
|
kind: "event-type";
|
|
@@ -148,3 +150,41 @@ export type FlowSelection = {
|
|
|
148
150
|
* just stores and round-trips it.
|
|
149
151
|
*/
|
|
150
152
|
export type PaneLayout = Record<string, unknown>;
|
|
153
|
+
/** Whether a subject-chain event came from the entity's own stream or a downstream descendant. */
|
|
154
|
+
export type SubjectChainSourceMode = "stream" | "descendant";
|
|
155
|
+
/** Query mode for subject_chain — which events to include. */
|
|
156
|
+
export type SubjectChainMode = "stream" | "descendants" | "both";
|
|
157
|
+
/** One event in a subject chain response, with display names applied. */
|
|
158
|
+
export type SubjectChainEvent = {
|
|
159
|
+
seq: number;
|
|
160
|
+
ts: string;
|
|
161
|
+
type: string;
|
|
162
|
+
name: string;
|
|
163
|
+
id: string | null;
|
|
164
|
+
causationId: string | null;
|
|
165
|
+
workflowId: string | null;
|
|
166
|
+
reactorId: string | null;
|
|
167
|
+
aggregateType: string | null;
|
|
168
|
+
aggregateId: string | null;
|
|
169
|
+
streamRevision: number | null;
|
|
170
|
+
summary: string | null;
|
|
171
|
+
payload: string;
|
|
172
|
+
sourceMode: SubjectChainSourceMode;
|
|
173
|
+
};
|
|
174
|
+
/** One `ctx.effect()` result for the effects inspector panel. */
|
|
175
|
+
export type InspectorEffect = {
|
|
176
|
+
consumer: string;
|
|
177
|
+
label: string;
|
|
178
|
+
value: unknown;
|
|
179
|
+
createdAt: string;
|
|
180
|
+
};
|
|
181
|
+
/** One entity entry from the aggregate keys listing. */
|
|
182
|
+
export type AggregateKeyEntry = {
|
|
183
|
+
aggregateId: string;
|
|
184
|
+
displayLabel: string | null;
|
|
185
|
+
};
|
|
186
|
+
/** Paginated entity listing response. */
|
|
187
|
+
export type AggregateKeysPage = {
|
|
188
|
+
entries: AggregateKeyEntry[];
|
|
189
|
+
nextCursor: string | null;
|
|
190
|
+
};
|
package/dist/utils.js
CHANGED
|
@@ -9,7 +9,7 @@ export function inScrubberRange(seq, start, end) {
|
|
|
9
9
|
/** Derive the walkable seq list based on current context (flow, selection, or global). */
|
|
10
10
|
export function getScrubberSequence(state) {
|
|
11
11
|
let events;
|
|
12
|
-
if (state.
|
|
12
|
+
if (state.flowWorkflowId) {
|
|
13
13
|
events = state.flowData;
|
|
14
14
|
const sel = state.flowSelection;
|
|
15
15
|
if (sel) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "causal-inspector",
|
|
3
|
-
"version": "0.1
|
|
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,51 +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 CorrelationExplorerPane() {
|
|
19
|
-
const correlations = useSelector((s) => s.correlations);
|
|
20
|
-
const loading = useSelector((s) => s.correlationsLoading);
|
|
21
|
-
const dispatch = useDispatch();
|
|
22
|
-
const [search, setSearch] = useState("");
|
|
23
|
-
const searchTimerRef = useRef(null);
|
|
24
|
-
// Request correlations on mount
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
dispatch({ type: "ui/correlations_requested", payload: {} });
|
|
27
|
-
}, [dispatch]);
|
|
28
|
-
const handleSearchChange = useCallback((value) => {
|
|
29
|
-
setSearch(value);
|
|
30
|
-
if (searchTimerRef.current)
|
|
31
|
-
clearTimeout(searchTimerRef.current);
|
|
32
|
-
searchTimerRef.current = setTimeout(() => {
|
|
33
|
-
dispatch({ type: "ui/correlations_requested", payload: { search: value || undefined } });
|
|
34
|
-
}, 300);
|
|
35
|
-
}, [dispatch]);
|
|
36
|
-
const handleRowClick = useCallback((correlationId) => {
|
|
37
|
-
dispatch({ type: "ui/flow_opened", payload: { correlationId } });
|
|
38
|
-
}, [dispatch]);
|
|
39
|
-
const handleCopy = useCallback((text) => {
|
|
40
|
-
navigator.clipboard.writeText(text).catch(() => { });
|
|
41
|
-
}, []);
|
|
42
|
-
return (_jsxs("div", { className: "ci-col", children: [_jsx("div", { className: "ci-surface", style: { padding: "10px 12px", borderBottom: "1px solid var(--ci-border)" }, children: _jsx("input", { type: "text", placeholder: "Search by correlation ID or event type...", value: search, onChange: (e) => handleSearchChange(e.target.value), className: "ci-input", style: { width: "100%", padding: "6px 12px", fontSize: 12 } }) }), _jsxs("div", { className: "ci-row ci-header", style: { gap: 8, padding: "8px 12px", borderBottom: "1px solid var(--ci-border)", letterSpacing: "0.08em", fontSize: 9 }, children: [_jsx("span", { style: { width: 112, flexShrink: 0 }, children: "Root Event" }), _jsx("span", { style: { width: 96, flexShrink: 0 }, children: "Correlation" }), _jsx("span", { style: { width: 48, flexShrink: 0, textAlign: "right" }, children: "Events" }), _jsx("span", { style: { width: 80, flexShrink: 0, textAlign: "right" }, children: "Duration" }), _jsx("span", { style: { flex: 1 }, children: "Last Activity" })] }), loading && correlations.length === 0 ? (_jsx("div", { className: "ci-pulse", style: { padding: 12 }, children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "ci-row", style: { gap: 8, padding: "10px 0" }, children: [_jsx("div", { className: "ci-skeleton", style: { height: 12, width: 112 } }), _jsx("div", { className: "ci-skeleton", style: { height: 12, width: 96 } }), _jsx("div", { className: "ci-skeleton", style: { height: 12, width: 48 } })] }, i))) })) : correlations.length === 0 ? (_jsx("div", { className: "ci-empty", style: { height: 128 }, children: "No correlations found" })) : (_jsx("div", { className: "ci-scroll", children: correlations.map((corr) => (_jsxs("button", { onClick: () => handleRowClick(corr.correlationId), className: "ci-list-row ci-row", style: { width: "100%", textAlign: "left", gap: 8, padding: "10px 12px", borderBottom: "1px solid var(--ci-border)", background: "none", border: "none", borderBottomStyle: "solid", cursor: "pointer" }, children: [_jsx("span", { className: "ci-mono ci-truncate", style: {
|
|
43
|
-
fontSize: 10,
|
|
44
|
-
flexShrink: 0,
|
|
45
|
-
width: 112,
|
|
46
|
-
padding: "2px 6px",
|
|
47
|
-
borderRadius: "var(--ci-radius-sm)",
|
|
48
|
-
color: eventTextColor(corr.rootEventType),
|
|
49
|
-
background: eventBg(corr.rootEventType),
|
|
50
|
-
}, title: corr.rootEventType, children: corr.rootEventType }), _jsx("span", { className: "ci-mono ci-truncate", style: { fontSize: 10, color: "var(--ci-purple-dim)", width: 96, flexShrink: 0, cursor: "pointer", transition: "color 150ms" }, title: `Click to copy: ${corr.correlationId}`, onClick: (e) => { e.stopPropagation(); handleCopy(corr.correlationId); }, children: corr.correlationId.slice(0, 8) }), _jsx("span", { className: "ci-mono ci-tabular", style: { fontSize: 11, color: "var(--ci-text)", opacity: 0.7, width: 48, flexShrink: 0, textAlign: "right" }, children: corr.eventCount }), _jsx("span", { className: "ci-mono ci-tabular", style: { fontSize: 10, color: "var(--ci-text-muted)", opacity: 0.5, width: 80, flexShrink: 0, textAlign: "right" }, children: _jsx(RelativeDuration, { firstTs: corr.firstTs, lastTs: corr.lastTs }) }), _jsx("span", { className: "ci-tabular ci-truncate", style: { fontSize: 10, color: "var(--ci-text-dimmer)", flex: 1 }, children: formatTs(corr.lastTs) }), corr.hasErrors && (_jsx("span", { className: "ci-error-dot", title: "This correlation has errors" }))] }, corr.correlationId))) }))] }));
|
|
51
|
-
}
|