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.
Files changed (39) hide show
  1. package/README.md +71 -0
  2. package/dist/CausalInspector.css +20 -447
  3. package/dist/CausalInspector.d.ts +8 -1
  4. package/dist/CausalInspector.js +32 -9
  5. package/dist/causal-inspector.css +2899 -0
  6. package/dist/components/CopyablePayload.js +8 -8
  7. package/dist/components/EffectList.d.ts +4 -0
  8. package/dist/components/EffectList.js +15 -0
  9. package/dist/components/FilterBar.js +7 -10
  10. package/dist/components/GlobalScrubber.js +6 -6
  11. package/dist/components/JsonSyntax.js +8 -8
  12. package/dist/engines/query.js +131 -52
  13. package/dist/engines/scrubber.js +1 -1
  14. package/dist/engines/url.d.ts +5 -2
  15. package/dist/engines/url.js +50 -22
  16. package/dist/events.d.ts +39 -13
  17. package/dist/index.d.ts +5 -3
  18. package/dist/index.js +4 -2
  19. package/dist/panes/AggregateTimelinePane.js +4 -4
  20. package/dist/panes/CausalFlowPane.js +39 -27
  21. package/dist/panes/CausalTreePane.js +43 -34
  22. package/dist/panes/LogsPane.js +9 -17
  23. package/dist/panes/SubjectChainPane.d.ts +1 -0
  24. package/dist/panes/SubjectChainPane.js +50 -0
  25. package/dist/panes/TimelinePane.js +33 -19
  26. package/dist/panes/WaterfallPane.js +5 -5
  27. package/dist/panes/WorkflowExplorerPane.d.ts +2 -0
  28. package/dist/panes/WorkflowExplorerPane.js +49 -0
  29. package/dist/queries.d.ts +16 -12
  30. package/dist/queries.js +103 -27
  31. package/dist/reducer.js +134 -38
  32. package/dist/state.d.ts +18 -5
  33. package/dist/state.js +17 -8
  34. package/dist/theme.js +4 -4
  35. package/dist/types.d.ts +52 -12
  36. package/dist/utils.js +1 -1
  37. package/package.json +18 -3
  38. package/dist/panes/CorrelationExplorerPane.d.ts +0 -2
  39. 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, correlationId, handler) {
7
- // Correlation changed reset flow state
8
- if (correlationId !== draft.flowCorrelationId) {
9
- if (correlationId) {
10
- draft.flowCorrelationId = correlationId;
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: "correlation",
25
+ scope: "workflow",
18
26
  reactorId: null,
19
- correlationId,
27
+ workflowId,
20
28
  };
21
29
  }
22
30
  else {
23
- draft.flowCorrelationId = null;
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
- correlationId: null,
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
- correlationId: draft.flowCorrelationId,
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.correlationId && e.correlationId !== draft.filters.correlationId) {
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.correlationId ?? "").toLowerCase().includes(s);
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 { correlationId, descriptions } = event.payload;
94
- draft.descriptions[correlationId] = 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 { correlationId, snapshots } = event.payload;
99
- draft.descriptionSnapshots[correlationId] = snapshots;
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 { correlationId, entries } = event.payload;
104
- draft.aggregateTimeline[correlationId] = entries;
144
+ const { workflowId, entries } = event.payload;
145
+ draft.aggregateTimeline[workflowId] = entries;
105
146
  break;
106
147
  }
107
148
  case "events/outcomes_loaded": {
108
- const { correlationId, outcomes } = event.payload;
109
- draft.outcomes[correlationId] = 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 { correlationId, attempts } = event.payload;
114
- draft.attempts[correlationId] = attempts;
159
+ const { workflowId, attempts } = event.payload;
160
+ draft.attempts[workflowId] = attempts;
115
161
  break;
116
162
  }
117
- case "events/correlations_loaded": {
118
- const { correlations, hasMore, append } = event.payload;
163
+ case "events/workflows_loaded": {
164
+ const { workflows, hasMore, append } = event.payload;
119
165
  if (append) {
120
- draft.correlations.push(...correlations);
166
+ draft.workflows.push(...workflows);
121
167
  }
122
168
  else {
123
- draft.correlations = correlations;
169
+ draft.workflows = workflows;
124
170
  }
125
- draft.correlationsHasMore = hasMore;
126
- draft.correlationsLoading = false;
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.correlationId, null);
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.flowCorrelationId, event.payload.reactorId);
198
+ applyNavigation(draft, draft.flowWorkflowId, event.payload.reactorId);
148
199
  break;
149
200
  case "location/changed":
150
- applyNavigation(draft, event.payload.correlationId, event.payload.handler);
151
- draft.filters.correlationId = event.payload.correlationId;
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: "correlation",
222
+ scope: "workflow",
167
223
  reactorId: null,
168
- correlationId: draft.flowCorrelationId,
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/correlations_requested":
194
- draft.correlationsLoading = true;
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/load_more_correlations_requested":
197
- draft.correlationsLoading = true;
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, CorrelationSummary, ReactorDependency, AggregateLifecycleEntry, FilterState, FlowSelection, ReactorDescription, ReactorDescriptionSnapshot, AggregateTimelineEntry, ReactorLog, ReactorOutcome, ReactorAttempt, LogsFilter, PaneLayout } from "./types";
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
- flowCorrelationId: string | null;
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
- correlations: CorrelationSummary[];
27
- correlationsLoading: boolean;
28
- correlationsHasMore: boolean;
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
- flowCorrelationId: null,
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
- correlationId: null,
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
- correlationId: null,
24
+ workflowId: null,
26
25
  },
27
26
  descriptions: {},
28
27
  descriptionSnapshots: {},
29
28
  aggregateTimeline: {},
30
29
  outcomes: {},
31
30
  attempts: {},
32
- correlations: [],
33
- correlationsLoading: false,
34
- correlationsHasMore: true,
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: "ci-log-debug",
32
- info: "ci-log-info",
33
- warn: "ci-log-warn",
34
- error: "ci-log-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
- parentId: string | null;
9
- correlationId: string | null;
8
+ causationId: string | null;
9
+ workflowId: string | null;
10
10
  reactorId: string | null;
11
11
  aggregateType: string | null;
12
12
  aggregateId: string | null;
13
- streamVersion: number | null;
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
- correlationId: string;
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
- correlationId: string;
114
+ workflowId: string;
115
115
  aggregateKey: string;
116
116
  state: unknown;
117
117
  };
118
- export type CorrelationSummary = {
119
- correlationId: string;
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
- correlationId: string | null;
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" | "correlation";
136
+ scope: "reactor" | "workflow";
135
137
  reactorId: string | null;
136
- correlationId: string | null;
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.flowCorrelationId) {
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.6",
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 CorrelationExplorerPaneProps = Record<string, never>;
2
- export declare function CorrelationExplorerPane(): import("react/jsx-runtime").JSX.Element;
@@ -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
- }