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
|
@@ -22,23 +22,23 @@ function PayloadModal({ formatted, onClose, }) {
|
|
|
22
22
|
document.addEventListener("keydown", handleKey);
|
|
23
23
|
return () => document.removeEventListener("keydown", handleKey);
|
|
24
24
|
}, [onClose]);
|
|
25
|
-
return createPortal(_jsx("div", { className: "
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
return createPortal(_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center", style: { background: "rgba(0, 0, 0, 0.6)", backdropFilter: "blur(4px)" }, onClick: onClose, children: _jsxs("div", { className: "relative w-[90vw] max-h-[90vh] overflow-auto rounded-xl border border-border p-5", style: { background: "rgba(15, 15, 20, 0.95)", boxShadow: "0 24px 64px rgba(0, 0, 0, 0.5)" }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "absolute top-3 right-3 flex gap-1", children: [_jsx("button", { onClick: async () => {
|
|
26
|
+
await copyToClipboard(formatted);
|
|
27
|
+
setCopied(true);
|
|
28
|
+
setTimeout(() => setCopied(false), 1500);
|
|
29
|
+
}, className: "p-1.5 rounded-md hover:bg-white/[0.05] transition-colors text-xs text-muted-foreground/50 hover:text-foreground", title: "Copy payload", children: copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }), _jsx("button", { onClick: onClose, className: "p-1.5 rounded-md hover:bg-white/[0.05] transition-colors text-xs text-muted-foreground/50 hover:text-foreground", title: "Close", children: _jsx(X, { size: 14 }) })] }), _jsx("pre", { className: "text-xs whitespace-pre-wrap", children: _jsx(JsonSyntax, { json: formatted }) })] }) }), document.body);
|
|
30
30
|
}
|
|
31
31
|
export function CopyablePayload({ payload, className = "", }) {
|
|
32
32
|
const [copied, setCopied] = useState(false);
|
|
33
33
|
const [modalOpen, setModalOpen] = useState(false);
|
|
34
34
|
const formatted = formatPayload(payload);
|
|
35
|
-
return (_jsxs("div", {
|
|
35
|
+
return (_jsxs("div", { className: `relative ${className}`, children: [_jsx("pre", { className: "p-2.5 text-[10px] rounded-md border border-border overflow-auto whitespace-pre-wrap resize-y min-h-24 max-h-[80vh]", style: { background: "rgba(255, 255, 255, 0.015)" }, children: _jsx(JsonSyntax, { json: formatted }) }), _jsxs("div", { className: "absolute top-2 right-2 z-10 flex gap-1", children: [_jsx("button", { onClick: (e) => {
|
|
36
36
|
e.stopPropagation();
|
|
37
37
|
setModalOpen(true);
|
|
38
|
-
}, className: "
|
|
38
|
+
}, className: "p-1 rounded-md border border-border hover:bg-white/[0.05] transition-all text-[10px] text-muted-foreground/40 hover:text-muted-foreground", style: { background: "rgba(10, 10, 15, 0.8)", backdropFilter: "blur(4px)" }, title: "Expand", children: _jsx(Maximize2, { size: 12 }) }), _jsx("button", { onClick: async (e) => {
|
|
39
39
|
e.stopPropagation();
|
|
40
40
|
await copyToClipboard(formatted);
|
|
41
41
|
setCopied(true);
|
|
42
42
|
setTimeout(() => setCopied(false), 1500);
|
|
43
|
-
}, className: "
|
|
43
|
+
}, className: "p-1 rounded-md border border-border hover:bg-white/[0.05] transition-all text-[10px] text-muted-foreground/40 hover:text-muted-foreground", style: { background: "rgba(10, 10, 15, 0.8)", backdropFilter: "blur(4px)" }, title: "Copy", children: copied ? _jsx(Check, { size: 12 }) : _jsx(Copy, { size: 12 }) })] }), modalOpen && (_jsx(PayloadModal, { formatted: formatted, onClose: () => setModalOpen(false) }))] }));
|
|
44
44
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
4
|
+
function EffectRow({ effect }) {
|
|
5
|
+
const [expanded, setExpanded] = useState(false);
|
|
6
|
+
const valueStr = JSON.stringify(effect.value, null, 2);
|
|
7
|
+
const preview = valueStr.length > 120 ? valueStr.slice(0, 120) + "…" : valueStr;
|
|
8
|
+
return (_jsxs("div", { className: "py-1", children: [_jsxs("button", { onClick: () => setExpanded((v) => !v), className: "flex items-start gap-1.5 w-full text-left group/effect", children: [_jsx("span", { className: "mt-0.5 text-muted-foreground/40 shrink-0", children: expanded ? _jsx(ChevronDown, { size: 10 }) : _jsx(ChevronRight, { size: 10 }) }), _jsx("span", { className: "text-[9px] font-mono text-indigo-400/70 shrink-0", children: effect.consumer }), _jsx("span", { className: "text-[9px] text-muted-foreground/40 shrink-0", children: "\u00B7" }), _jsx("span", { className: "text-[9px] font-mono text-foreground/70 shrink-0", children: effect.label }), !expanded && (_jsx("span", { className: "text-[9px] font-mono text-muted-foreground/50 truncate min-w-0", children: preview }))] }), expanded && (_jsx("pre", { className: "mt-1 ml-4 text-[9px] font-mono text-foreground/60 whitespace-pre-wrap break-all bg-white/[0.02] rounded p-1.5 border border-border/50", children: valueStr }))] }));
|
|
9
|
+
}
|
|
10
|
+
export function EffectList({ effects }) {
|
|
11
|
+
if (effects.length === 0) {
|
|
12
|
+
return (_jsx("div", { className: "py-1 text-[9px] text-muted-foreground/40 italic ml-3", children: "No effects" }));
|
|
13
|
+
}
|
|
14
|
+
return (_jsx("div", { className: "ml-3 border-l border-indigo-500/15 pl-2", children: effects.map((e, i) => (_jsx(EffectRow, { effect: e }, `${e.consumer}:${e.label}:${i}`))) }));
|
|
15
|
+
}
|
|
@@ -4,17 +4,14 @@ import { Search, X } from "lucide-react";
|
|
|
4
4
|
export function FilterBar() {
|
|
5
5
|
const filters = useSelector((s) => s.filters);
|
|
6
6
|
const dispatch = useDispatch();
|
|
7
|
-
return (_jsxs("div", { className: "
|
|
7
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2 px-3 py-2 border-b border-border", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsxs("div", { className: "relative flex items-center", children: [_jsx(Search, { size: 12, className: "absolute left-2.5 text-muted-foreground pointer-events-none" }), _jsx("input", { type: "text", placeholder: "search events...", value: filters.search, onChange: (e) => dispatch({
|
|
8
8
|
type: "ui/filter_changed",
|
|
9
9
|
payload: { search: e.target.value },
|
|
10
|
-
}), className: "
|
|
11
|
-
type: "ui/filter_changed",
|
|
12
|
-
payload: { from: e.target.value || null },
|
|
13
|
-
}), className: "ci-input", style: { padding: "6px 8px", fontSize: 12, width: 128 } }), _jsx("label", { style: { fontSize: 10, color: "var(--ci-text-muted)", textTransform: "uppercase", letterSpacing: "0.05em" }, children: "To" }), _jsx("input", { type: "date", value: filters.to ?? "", onChange: (e) => dispatch({
|
|
14
|
-
type: "ui/filter_changed",
|
|
15
|
-
payload: { to: e.target.value || null },
|
|
16
|
-
}), className: "ci-input", style: { padding: "6px 8px", fontSize: 12, width: 128 } }), filters.correlationId && (_jsxs(_Fragment, { children: [_jsx("span", { className: "ci-divider-h" }), _jsxs("span", { className: "ci-badge ci-badge--purple", children: [filters.correlationId.slice(0, 8), _jsx("button", { onClick: () => dispatch({
|
|
10
|
+
}), className: "pl-7 pr-2 py-1.5 text-xs rounded-md bg-background/50 border border-border text-foreground placeholder:text-muted-foreground w-64 focus:outline-none focus:ring-1 focus:ring-indigo-500/40 focus:border-indigo-500/30 transition-all" })] }), filters.workflowId && (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-px h-4 bg-border" }), _jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-400 text-[10px] font-mono", children: [filters.workflowId.slice(0, 8), _jsx("button", { onClick: () => dispatch({
|
|
17
11
|
type: "ui/filter_changed",
|
|
18
|
-
payload: {
|
|
19
|
-
}),
|
|
12
|
+
payload: { workflowId: null },
|
|
13
|
+
}), className: "hover:text-foreground transition-colors", children: _jsx(X, { size: 10 }) })] })] })), filters.aggregateKey && (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-px h-4 bg-border" }), _jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-teal-500/10 border border-teal-500/20 text-teal-400 text-[10px] font-mono", children: [filters.aggregateKey.split(":")[0], ":", filters.aggregateKey.split(":").slice(1).join(":").slice(0, 8), _jsx("button", { onClick: () => dispatch({
|
|
14
|
+
type: "ui/filter_changed",
|
|
15
|
+
payload: { aggregateKey: null },
|
|
16
|
+
}), className: "hover:text-foreground transition-colors", children: _jsx(X, { size: 10 }) })] })] }))] }));
|
|
20
17
|
}
|
|
@@ -58,15 +58,15 @@ function RangeSlider({ seqs, start, end, onStartChange, onEndChange, }) {
|
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
59
59
|
// Context label
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
|
-
function getContextLabel(
|
|
62
|
-
if (!
|
|
61
|
+
function getContextLabel(flowWorkflowId, flowSelection) {
|
|
62
|
+
if (!flowWorkflowId)
|
|
63
63
|
return "All events";
|
|
64
64
|
if (flowSelection) {
|
|
65
65
|
if (flowSelection.kind === "event-type")
|
|
66
66
|
return `${flowSelection.name} events`;
|
|
67
67
|
return `Reactor ${flowSelection.reactorId}`;
|
|
68
68
|
}
|
|
69
|
-
return `Flow ${
|
|
69
|
+
return `Flow ${flowWorkflowId.slice(0, 8)}`;
|
|
70
70
|
}
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
72
|
// GlobalScrubber
|
|
@@ -76,12 +76,12 @@ export function GlobalScrubber() {
|
|
|
76
76
|
const scrubberEnd = useSelector((s) => s.scrubberEnd);
|
|
77
77
|
const playing = useSelector((s) => s.scrubberPlaying);
|
|
78
78
|
const speed = useSelector((s) => s.scrubberSpeed);
|
|
79
|
-
const
|
|
79
|
+
const flowWorkflowId = useSelector((s) => s.flowWorkflowId);
|
|
80
80
|
const flowSelection = useSelector((s) => s.flowSelection);
|
|
81
81
|
const flowData = useSelector((s) => s.flowData);
|
|
82
82
|
const events = useSelector((s) => s.events);
|
|
83
83
|
const dispatch = useDispatch();
|
|
84
|
-
const seqs = useMemo(() => getScrubberSequence({
|
|
84
|
+
const seqs = useMemo(() => getScrubberSequence({ flowWorkflowId, flowData, flowSelection, events }), [flowWorkflowId, flowData, flowSelection, events]);
|
|
85
85
|
const endVal = scrubberEnd ?? (seqs[seqs.length - 1] ?? 0);
|
|
86
86
|
const isAtEnd = scrubberEnd == null || (seqs.length > 0 && scrubberEnd >= seqs[seqs.length - 1]);
|
|
87
87
|
// Count events in range
|
|
@@ -139,7 +139,7 @@ export function GlobalScrubber() {
|
|
|
139
139
|
const disabled = seqs.length < 2;
|
|
140
140
|
const speedLabel = speed <= 50 ? "4x" : speed <= 150 ? "2x" : speed <= 300 ? "1x" : "0.5x";
|
|
141
141
|
const btnClass = `p-1.5 rounded-md transition-all duration-150 ${disabled ? "text-muted-foreground/20 cursor-default" : "text-muted-foreground/60 hover:text-foreground hover:bg-white/[0.05]"}`;
|
|
142
|
-
const contextLabel = getContextLabel(
|
|
142
|
+
const contextLabel = getContextLabel(flowWorkflowId, flowSelection);
|
|
143
143
|
return (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-t border-border shrink-0", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("span", { className: "text-[9px] font-medium text-muted-foreground/40 uppercase tracking-wider shrink-0 max-w-[120px] truncate", title: contextLabel, children: contextLabel }), _jsx("button", { onClick: disabled ? undefined : reset, className: btnClass, title: "Reset to start", children: _jsx(RotateCcw, { size: 12 }) }), _jsx("button", { onClick: disabled ? undefined : stepBack, className: btnClass, title: "Step back", children: _jsx(SkipBack, { size: 12 }) }), _jsx("button", { onClick: disabled ? undefined : togglePlay, className: `p-1.5 rounded-md transition-all duration-150 ${disabled
|
|
144
144
|
? "text-muted-foreground/20 cursor-default"
|
|
145
145
|
: playing
|
|
@@ -9,23 +9,23 @@ function tokenizeJson(json) {
|
|
|
9
9
|
tokens.push({ text: json.slice(lastIndex, match.index), color: "" });
|
|
10
10
|
}
|
|
11
11
|
if (match[1] !== undefined) {
|
|
12
|
-
tokens.push({ text: match[1], color: "
|
|
13
|
-
tokens.push({ text: ":", color: "
|
|
12
|
+
tokens.push({ text: match[1], color: "text-blue-400" });
|
|
13
|
+
tokens.push({ text: ":", color: "text-zinc-500" });
|
|
14
14
|
}
|
|
15
15
|
else if (match[2] !== undefined) {
|
|
16
|
-
tokens.push({ text: match[2], color: "
|
|
16
|
+
tokens.push({ text: match[2], color: "text-green-400" });
|
|
17
17
|
}
|
|
18
18
|
else if (match[3] !== undefined) {
|
|
19
|
-
tokens.push({ text: match[3], color: "
|
|
19
|
+
tokens.push({ text: match[3], color: "text-amber-400" });
|
|
20
20
|
}
|
|
21
21
|
else if (match[4] !== undefined) {
|
|
22
|
-
tokens.push({ text: match[4], color: "
|
|
22
|
+
tokens.push({ text: match[4], color: "text-purple-400" });
|
|
23
23
|
}
|
|
24
24
|
else if (match[5] !== undefined) {
|
|
25
|
-
tokens.push({ text: match[5], color: "
|
|
25
|
+
tokens.push({ text: match[5], color: "text-zinc-500" });
|
|
26
26
|
}
|
|
27
27
|
else if (match[6] !== undefined) {
|
|
28
|
-
tokens.push({ text: match[6], color: "
|
|
28
|
+
tokens.push({ text: match[6], color: "text-zinc-500" });
|
|
29
29
|
}
|
|
30
30
|
lastIndex = re.lastIndex;
|
|
31
31
|
}
|
|
@@ -36,5 +36,5 @@ function tokenizeJson(json) {
|
|
|
36
36
|
}
|
|
37
37
|
export function JsonSyntax({ json }) {
|
|
38
38
|
const tokens = tokenizeJson(json);
|
|
39
|
-
return (_jsx(_Fragment, { children: tokens.map((t, i) => (_jsx("span", { className: t.color
|
|
39
|
+
return (_jsx(_Fragment, { children: tokens.map((t, i) => (_jsx("span", { className: t.color, children: t.text }, i))) }));
|
|
40
40
|
}
|
package/dist/engines/query.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW,
|
|
1
|
+
import { INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_WORKFLOWS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_SUBJECT_CHAIN, INSPECTOR_EFFECTS_FOR_EVENT, } from "../queries";
|
|
2
2
|
/**
|
|
3
3
|
* Query engine — fetches data in response to state transitions.
|
|
4
4
|
*
|
|
@@ -8,10 +8,11 @@ import { INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECT
|
|
|
8
8
|
export const createQueryEngine = (transport) => {
|
|
9
9
|
return (dispatch, getState) => {
|
|
10
10
|
let flowPollTimer = null;
|
|
11
|
-
let
|
|
11
|
+
let workflowPollTimer = null;
|
|
12
12
|
// Stale-response guards
|
|
13
13
|
let activeCausalSeq = null;
|
|
14
|
-
let
|
|
14
|
+
let activeFlowWorkflowId = null;
|
|
15
|
+
let activeSubjectKey = null;
|
|
15
16
|
const fetchEvents = async () => {
|
|
16
17
|
const state = getState();
|
|
17
18
|
const cursor = state.events.length > 0
|
|
@@ -22,7 +23,7 @@ export const createQueryEngine = (transport) => {
|
|
|
22
23
|
limit: 50,
|
|
23
24
|
cursor,
|
|
24
25
|
search: state.filters.search || undefined,
|
|
25
|
-
|
|
26
|
+
workflowId: state.filters.workflowId || undefined,
|
|
26
27
|
aggregateKey: state.filters.aggregateKey || undefined,
|
|
27
28
|
});
|
|
28
29
|
dispatch({
|
|
@@ -52,11 +53,11 @@ export const createQueryEngine = (transport) => {
|
|
|
52
53
|
console.error("[causal-inspector] fetch causal tree failed:", e);
|
|
53
54
|
}
|
|
54
55
|
};
|
|
55
|
-
const fetchFlow = async (
|
|
56
|
-
|
|
56
|
+
const fetchFlow = async (workflowId) => {
|
|
57
|
+
activeFlowWorkflowId = workflowId;
|
|
57
58
|
try {
|
|
58
|
-
const data = await transport.query(INSPECTOR_CAUSAL_FLOW, {
|
|
59
|
-
if (
|
|
59
|
+
const data = await transport.query(INSPECTOR_CAUSAL_FLOW, { workflowId });
|
|
60
|
+
if (activeFlowWorkflowId !== workflowId)
|
|
60
61
|
return; // stale
|
|
61
62
|
dispatch({
|
|
62
63
|
type: "events/flow_loaded",
|
|
@@ -67,49 +68,49 @@ export const createQueryEngine = (transport) => {
|
|
|
67
68
|
console.error("[causal-inspector] fetch flow failed:", e);
|
|
68
69
|
}
|
|
69
70
|
};
|
|
70
|
-
const fetchFlowMetadata = async (
|
|
71
|
+
const fetchFlowMetadata = async (workflowId) => {
|
|
71
72
|
try {
|
|
72
73
|
const [descData, snapshotData, aggTimelineData, outcomeData, attemptData] = await Promise.all([
|
|
73
|
-
transport.query(INSPECTOR_REACTOR_DESCRIPTIONS, {
|
|
74
|
-
transport.query(INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, {
|
|
75
|
-
transport.query(INSPECTOR_AGGREGATE_TIMELINE, {
|
|
76
|
-
transport.query(INSPECTOR_REACTOR_OUTCOMES, {
|
|
77
|
-
transport.query(INSPECTOR_REACTOR_ATTEMPTS, {
|
|
74
|
+
transport.query(INSPECTOR_REACTOR_DESCRIPTIONS, { workflowId }),
|
|
75
|
+
transport.query(INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, { workflowId }),
|
|
76
|
+
transport.query(INSPECTOR_AGGREGATE_TIMELINE, { workflowId }),
|
|
77
|
+
transport.query(INSPECTOR_REACTOR_OUTCOMES, { workflowId }),
|
|
78
|
+
transport.query(INSPECTOR_REACTOR_ATTEMPTS, { workflowId }),
|
|
78
79
|
]);
|
|
79
|
-
if (
|
|
80
|
+
if (activeFlowWorkflowId !== workflowId)
|
|
80
81
|
return; // stale
|
|
81
82
|
dispatch({
|
|
82
83
|
type: "events/descriptions_loaded",
|
|
83
84
|
payload: {
|
|
84
|
-
|
|
85
|
+
workflowId,
|
|
85
86
|
descriptions: descData.inspectorReactorDescriptions,
|
|
86
87
|
},
|
|
87
88
|
});
|
|
88
89
|
dispatch({
|
|
89
90
|
type: "events/description_snapshots_loaded",
|
|
90
91
|
payload: {
|
|
91
|
-
|
|
92
|
+
workflowId,
|
|
92
93
|
snapshots: snapshotData.inspectorReactorDescriptionSnapshots,
|
|
93
94
|
},
|
|
94
95
|
});
|
|
95
96
|
dispatch({
|
|
96
97
|
type: "events/aggregate_timeline_loaded",
|
|
97
98
|
payload: {
|
|
98
|
-
|
|
99
|
+
workflowId,
|
|
99
100
|
entries: aggTimelineData.inspectorAggregateTimeline,
|
|
100
101
|
},
|
|
101
102
|
});
|
|
102
103
|
dispatch({
|
|
103
104
|
type: "events/outcomes_loaded",
|
|
104
105
|
payload: {
|
|
105
|
-
|
|
106
|
+
workflowId,
|
|
106
107
|
outcomes: outcomeData.inspectorReactorOutcomes,
|
|
107
108
|
},
|
|
108
109
|
});
|
|
109
110
|
dispatch({
|
|
110
111
|
type: "events/attempts_loaded",
|
|
111
112
|
payload: {
|
|
112
|
-
|
|
113
|
+
workflowId,
|
|
113
114
|
attempts: attemptData.inspectorReactorAttempts,
|
|
114
115
|
},
|
|
115
116
|
});
|
|
@@ -118,32 +119,37 @@ export const createQueryEngine = (transport) => {
|
|
|
118
119
|
console.error("[causal-inspector] fetch flow metadata failed:", e);
|
|
119
120
|
}
|
|
120
121
|
};
|
|
121
|
-
const fetchLogs = async (
|
|
122
|
+
const fetchLogs = async (workflowId) => {
|
|
122
123
|
try {
|
|
123
|
-
const data = await transport.query(
|
|
124
|
-
dispatch({ type: "events/logs_loaded", payload: data.
|
|
124
|
+
const data = await transport.query(INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, { workflowId });
|
|
125
|
+
dispatch({ type: "events/logs_loaded", payload: data.inspectorReactorLogsByWorkflow });
|
|
125
126
|
}
|
|
126
127
|
catch (e) {
|
|
127
128
|
console.error("[causal-inspector] fetch logs failed:", e);
|
|
128
129
|
}
|
|
129
130
|
};
|
|
130
|
-
|
|
131
|
+
let workflowCursor = null;
|
|
132
|
+
const fetchWorkflows = async (opts) => {
|
|
133
|
+
const append = opts?.append ?? false;
|
|
134
|
+
const cursor = append ? workflowCursor : undefined;
|
|
131
135
|
try {
|
|
132
|
-
const data = await transport.query(
|
|
136
|
+
const data = await transport.query(INSPECTOR_WORKFLOWS, {
|
|
133
137
|
search: opts?.search || undefined,
|
|
134
|
-
limit:
|
|
138
|
+
limit: 50,
|
|
139
|
+
cursor: cursor || undefined,
|
|
135
140
|
});
|
|
141
|
+
workflowCursor = data.inspectorWorkflows.nextCursor;
|
|
136
142
|
dispatch({
|
|
137
|
-
type: "events/
|
|
143
|
+
type: "events/workflows_loaded",
|
|
138
144
|
payload: {
|
|
139
|
-
|
|
140
|
-
hasMore:
|
|
141
|
-
append
|
|
145
|
+
workflows: data.inspectorWorkflows.workflows,
|
|
146
|
+
hasMore: data.inspectorWorkflows.nextCursor != null,
|
|
147
|
+
append,
|
|
142
148
|
},
|
|
143
149
|
});
|
|
144
150
|
}
|
|
145
151
|
catch (e) {
|
|
146
|
-
console.error("[causal-inspector] fetch
|
|
152
|
+
console.error("[causal-inspector] fetch workflows failed:", e);
|
|
147
153
|
}
|
|
148
154
|
};
|
|
149
155
|
const fetchReactorDependencies = async () => {
|
|
@@ -182,10 +188,54 @@ export const createQueryEngine = (transport) => {
|
|
|
182
188
|
console.error("[causal-inspector] fetch aggregate lifecycle failed:", e);
|
|
183
189
|
}
|
|
184
190
|
};
|
|
185
|
-
const
|
|
191
|
+
const fetchSubjectChain = async (aggregateType, aggregateId, mode, cursor) => {
|
|
192
|
+
const key = `${aggregateType}:${aggregateId}:${mode}`;
|
|
193
|
+
activeSubjectKey = key;
|
|
194
|
+
try {
|
|
195
|
+
const data = await transport.query(INSPECTOR_SUBJECT_CHAIN, {
|
|
196
|
+
aggregateType,
|
|
197
|
+
aggregateId,
|
|
198
|
+
mode: mode.toUpperCase(),
|
|
199
|
+
limit: 50,
|
|
200
|
+
cursor: cursor ?? undefined,
|
|
201
|
+
});
|
|
202
|
+
if (activeSubjectKey !== key)
|
|
203
|
+
return; // navigated away
|
|
204
|
+
const page = data.inspectorSubjectChain;
|
|
205
|
+
dispatch({
|
|
206
|
+
type: "events/subject_chain_loaded",
|
|
207
|
+
payload: {
|
|
208
|
+
events: page.events,
|
|
209
|
+
hasMore: page.nextCursor != null,
|
|
210
|
+
cursor: page.nextCursor,
|
|
211
|
+
depthCapped: page.depthCapReached,
|
|
212
|
+
append: cursor != null,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
console.error("[causal-inspector] fetch subject chain failed:", e);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const fetchEventEffects = async (eventId) => {
|
|
221
|
+
const state = getState();
|
|
222
|
+
if (eventId in state.expandedEffects)
|
|
223
|
+
return; // already loaded
|
|
224
|
+
try {
|
|
225
|
+
const data = await transport.query(INSPECTOR_EFFECTS_FOR_EVENT, { eventId });
|
|
226
|
+
dispatch({
|
|
227
|
+
type: "events/event_effects_loaded",
|
|
228
|
+
payload: { eventId, effects: data.inspectorEffectsForEvent },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
console.error("[causal-inspector] fetch event effects failed:", e);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const startFlowPolling = (workflowId) => {
|
|
186
236
|
stopFlowPolling();
|
|
187
|
-
fetchFlowMetadata(
|
|
188
|
-
flowPollTimer = setInterval(() => fetchFlowMetadata(
|
|
237
|
+
fetchFlowMetadata(workflowId);
|
|
238
|
+
flowPollTimer = setInterval(() => fetchFlowMetadata(workflowId), 5000);
|
|
189
239
|
};
|
|
190
240
|
const stopFlowPolling = () => {
|
|
191
241
|
if (flowPollTimer) {
|
|
@@ -193,27 +243,33 @@ export const createQueryEngine = (transport) => {
|
|
|
193
243
|
flowPollTimer = null;
|
|
194
244
|
}
|
|
195
245
|
};
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
198
|
-
clearInterval(
|
|
199
|
-
|
|
246
|
+
const stopWorkflowPolling = () => {
|
|
247
|
+
if (workflowPollTimer) {
|
|
248
|
+
clearInterval(workflowPollTimer);
|
|
249
|
+
workflowPollTimer = null;
|
|
200
250
|
}
|
|
201
251
|
};
|
|
202
252
|
// Initial load
|
|
203
253
|
fetchEvents();
|
|
204
|
-
|
|
254
|
+
fetchWorkflows();
|
|
205
255
|
fetchReactorDependencies();
|
|
206
256
|
fetchAggregateKeys();
|
|
207
257
|
return {
|
|
208
258
|
handleEvent: (event, curr, prev) => {
|
|
209
259
|
// ── State-reactive: navigation transitions ──
|
|
210
|
-
|
|
211
|
-
|
|
260
|
+
// Subject changed → fetch first page
|
|
261
|
+
if (curr.subjectType !== prev.subjectType || curr.subjectId !== prev.subjectId) {
|
|
262
|
+
if (curr.subjectType && curr.subjectId) {
|
|
263
|
+
fetchSubjectChain(curr.subjectType, curr.subjectId, curr.subjectMode, null);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (curr.flowWorkflowId !== prev.flowWorkflowId) {
|
|
267
|
+
if (curr.flowWorkflowId) {
|
|
212
268
|
// Flow opened
|
|
213
|
-
fetchFlow(curr.
|
|
214
|
-
startFlowPolling(curr.
|
|
215
|
-
fetchLogs(curr.
|
|
216
|
-
|
|
269
|
+
fetchFlow(curr.flowWorkflowId);
|
|
270
|
+
startFlowPolling(curr.flowWorkflowId);
|
|
271
|
+
fetchLogs(curr.flowWorkflowId);
|
|
272
|
+
stopWorkflowPolling();
|
|
217
273
|
}
|
|
218
274
|
else {
|
|
219
275
|
// Flow closed
|
|
@@ -231,22 +287,45 @@ export const createQueryEngine = (transport) => {
|
|
|
231
287
|
case "ui/filter_changed":
|
|
232
288
|
fetchEvents();
|
|
233
289
|
break;
|
|
234
|
-
case "ui/
|
|
235
|
-
|
|
290
|
+
case "ui/load_more_workflows_requested":
|
|
291
|
+
fetchWorkflows({ append: true });
|
|
236
292
|
break;
|
|
237
|
-
case "ui/
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
293
|
+
case "ui/workflows_requested":
|
|
294
|
+
workflowCursor = null;
|
|
295
|
+
fetchWorkflows({ search: event.payload.search });
|
|
296
|
+
stopWorkflowPolling();
|
|
297
|
+
workflowPollTimer = setInterval(() => fetchWorkflows({ search: event.payload.search }), 5000);
|
|
241
298
|
break;
|
|
242
299
|
case "ui/aggregate_lifecycle_requested":
|
|
243
300
|
fetchAggregateLifecycle(event.payload.aggregateKey);
|
|
244
301
|
break;
|
|
302
|
+
case "ui/subject_selected": {
|
|
303
|
+
const { aggregateType, aggregateId, mode = "both" } = event.payload;
|
|
304
|
+
fetchSubjectChain(aggregateType, aggregateId, mode, null);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case "ui/subject_mode_changed": {
|
|
308
|
+
const state = getState();
|
|
309
|
+
if (state.subjectType && state.subjectId) {
|
|
310
|
+
fetchSubjectChain(state.subjectType, state.subjectId, event.payload.mode, null);
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "ui/subject_chain_load_more": {
|
|
315
|
+
const state = getState();
|
|
316
|
+
if (state.subjectType && state.subjectId) {
|
|
317
|
+
fetchSubjectChain(state.subjectType, state.subjectId, state.subjectMode, state.subjectChainCursor);
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "ui/event_effects_requested":
|
|
322
|
+
fetchEventEffects(event.payload.eventId);
|
|
323
|
+
break;
|
|
245
324
|
}
|
|
246
325
|
},
|
|
247
326
|
dispose: () => {
|
|
248
327
|
stopFlowPolling();
|
|
249
|
-
|
|
328
|
+
stopWorkflowPolling();
|
|
250
329
|
},
|
|
251
330
|
};
|
|
252
331
|
};
|
package/dist/engines/scrubber.js
CHANGED
package/dist/engines/url.d.ts
CHANGED
|
@@ -4,8 +4,11 @@ import type { InspectorState } from "../state";
|
|
|
4
4
|
/**
|
|
5
5
|
* URL engine — keeps the browser URL in sync with navigation state.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Mutual exclusion: `?subject=` and `?workflow=` never coexist in the URL.
|
|
8
|
+
* Navigating to a subject clears the workflow param and vice versa.
|
|
9
|
+
*
|
|
10
|
+
* For user-initiated actions, the reducer updates state directly.
|
|
11
|
+
* This engine writes the URL as a side effect.
|
|
9
12
|
*
|
|
10
13
|
* For browser-initiated navigation (back/forward), this engine dispatches
|
|
11
14
|
* location/changed so the reducer can update state from the URL.
|
package/dist/engines/url.js
CHANGED
|
@@ -1,44 +1,57 @@
|
|
|
1
1
|
function parseUrl() {
|
|
2
2
|
const params = new URLSearchParams(window.location.search);
|
|
3
|
+
const subject = params.get("subject");
|
|
4
|
+
const subjectModeRaw = params.get("subjectMode");
|
|
5
|
+
const subjectMode = subjectModeRaw === "stream" || subjectModeRaw === "descendants" || subjectModeRaw === "both"
|
|
6
|
+
? subjectModeRaw
|
|
7
|
+
: null;
|
|
8
|
+
if (subject) {
|
|
9
|
+
return { workflowId: null, handler: null, subject, subjectMode };
|
|
10
|
+
}
|
|
3
11
|
return {
|
|
4
|
-
|
|
12
|
+
workflowId: params.get("workflow"),
|
|
5
13
|
handler: params.get("handler"),
|
|
14
|
+
subject: null,
|
|
15
|
+
subjectMode: null,
|
|
6
16
|
};
|
|
7
17
|
}
|
|
8
|
-
function buildSearch(
|
|
9
|
-
const params = new URLSearchParams(
|
|
10
|
-
if (
|
|
11
|
-
params.set("
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
params.delete("handler");
|
|
18
|
+
function buildSearch(workflowId, handler) {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
if (workflowId) {
|
|
21
|
+
params.set("workflow", workflowId);
|
|
22
|
+
if (handler)
|
|
23
|
+
params.set("handler", handler);
|
|
15
24
|
}
|
|
16
|
-
if (handler && correlationId)
|
|
17
|
-
params.set("handler", handler);
|
|
18
|
-
else
|
|
19
|
-
params.delete("handler");
|
|
20
25
|
const search = params.toString();
|
|
21
26
|
return search ? `${window.location.pathname}?${search}` : window.location.pathname;
|
|
22
27
|
}
|
|
28
|
+
function buildSubjectSearch(aggregateType, aggregateId, mode) {
|
|
29
|
+
const params = new URLSearchParams();
|
|
30
|
+
params.set("subject", `${aggregateType}:${aggregateId}`);
|
|
31
|
+
if (mode !== "both")
|
|
32
|
+
params.set("subjectMode", mode);
|
|
33
|
+
return `${window.location.pathname}?${params.toString()}`;
|
|
34
|
+
}
|
|
23
35
|
/**
|
|
24
36
|
* URL engine — keeps the browser URL in sync with navigation state.
|
|
25
37
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
38
|
+
* Mutual exclusion: `?subject=` and `?workflow=` never coexist in the URL.
|
|
39
|
+
* Navigating to a subject clears the workflow param and vice versa.
|
|
40
|
+
*
|
|
41
|
+
* For user-initiated actions, the reducer updates state directly.
|
|
42
|
+
* This engine writes the URL as a side effect.
|
|
28
43
|
*
|
|
29
44
|
* For browser-initiated navigation (back/forward), this engine dispatches
|
|
30
45
|
* location/changed so the reducer can update state from the URL.
|
|
31
46
|
*/
|
|
32
47
|
export const createUrlEngine = (dispatch, _getState) => {
|
|
33
|
-
// Popstate — browser back/forward
|
|
34
48
|
const onPopState = () => {
|
|
35
49
|
dispatch({ type: "location/changed", payload: parseUrl() });
|
|
36
50
|
};
|
|
37
51
|
window.addEventListener("popstate", onPopState);
|
|
38
|
-
// Seed from current URL on init — deferred so the Machine constructor finishes first.
|
|
39
52
|
queueMicrotask(() => {
|
|
40
53
|
const initial = parseUrl();
|
|
41
|
-
if (initial.
|
|
54
|
+
if (initial.workflowId || initial.handler || initial.subject) {
|
|
42
55
|
dispatch({ type: "location/changed", payload: initial });
|
|
43
56
|
}
|
|
44
57
|
});
|
|
@@ -46,21 +59,36 @@ export const createUrlEngine = (dispatch, _getState) => {
|
|
|
46
59
|
handleEvent: (event) => {
|
|
47
60
|
switch (event.type) {
|
|
48
61
|
case "ui/flow_opened":
|
|
49
|
-
window.history.pushState(null, "", buildSearch(event.payload.
|
|
62
|
+
window.history.pushState(null, "", buildSearch(event.payload.workflowId, null));
|
|
50
63
|
break;
|
|
51
64
|
case "ui/flow_closed":
|
|
52
65
|
window.history.pushState(null, "", buildSearch(null, null));
|
|
53
66
|
break;
|
|
54
67
|
case "ui/filter_changed": {
|
|
55
68
|
const payload = event.payload;
|
|
56
|
-
if (payload.
|
|
57
|
-
window.history.pushState(null, "", buildSearch(payload.
|
|
69
|
+
if (payload.workflowId !== undefined) {
|
|
70
|
+
window.history.pushState(null, "", buildSearch(payload.workflowId, null));
|
|
58
71
|
}
|
|
59
72
|
break;
|
|
60
73
|
}
|
|
61
74
|
case "ui/handler_selected":
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
window.history.replaceState(null, "", buildSearch(new URLSearchParams(window.location.search).get("workflow"), event.payload.reactorId));
|
|
76
|
+
break;
|
|
77
|
+
case "ui/subject_selected":
|
|
78
|
+
window.history.pushState(null, "", buildSubjectSearch(event.payload.aggregateType, event.payload.aggregateId, event.payload.mode ?? "both"));
|
|
79
|
+
break;
|
|
80
|
+
case "ui/subject_mode_changed":
|
|
81
|
+
window.history.replaceState(null, "", (() => {
|
|
82
|
+
const params = new URLSearchParams(window.location.search);
|
|
83
|
+
const existing = params.get("subject");
|
|
84
|
+
if (!existing)
|
|
85
|
+
return window.location.href;
|
|
86
|
+
if (event.payload.mode === "both")
|
|
87
|
+
params.delete("subjectMode");
|
|
88
|
+
else
|
|
89
|
+
params.set("subjectMode", event.payload.mode);
|
|
90
|
+
return `${window.location.pathname}?${params.toString()}`;
|
|
91
|
+
})());
|
|
64
92
|
break;
|
|
65
93
|
}
|
|
66
94
|
},
|