@telorun/debug-ui 0.3.0 → 0.5.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/app-dist/assets/{index-CWG3ZSzH.css → index-C9ZwjlYK.css} +1 -1
- package/app-dist/assets/index-D2M9P905.js +126 -0
- package/app-dist/index.html +2 -2
- package/app-single/index.html +29 -29
- package/package.json +2 -2
- package/src/components/EventGraph.tsx +223 -17
- package/src/components/styles.css +165 -1
- package/src/filter.ts +6 -2
- package/src/graph.test.ts +270 -39
- package/src/graph.ts +302 -79
- package/app-dist/assets/index-DS4SK8dD.js +0 -126
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/debug-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Browser-safe debug event UI for Telo: wire types, filtering, SSE client, and React components for watching a running app's event stream.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@dagrejs/dagre": "^3.0.0",
|
|
47
47
|
"@xyflow/react": "^12.10.2",
|
|
48
48
|
"lucide-react": "^1.8.0",
|
|
49
|
-
"@telorun/debug-wire": "0.
|
|
49
|
+
"@telorun/debug-wire": "0.3.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": ">=18",
|
|
@@ -2,6 +2,7 @@ import Dagre from "@dagrejs/dagre";
|
|
|
2
2
|
import {
|
|
3
3
|
Background,
|
|
4
4
|
Controls,
|
|
5
|
+
type Edge,
|
|
5
6
|
Handle,
|
|
6
7
|
type Node,
|
|
7
8
|
type NodeProps,
|
|
@@ -10,12 +11,17 @@ import {
|
|
|
10
11
|
ReactFlow,
|
|
11
12
|
} from "@xyflow/react";
|
|
12
13
|
import "@xyflow/react/dist/style.css";
|
|
13
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
14
|
+
import { type CSSProperties, useEffect, useMemo, useRef, useState } from "react";
|
|
14
15
|
import {
|
|
16
|
+
collapseTopology,
|
|
15
17
|
deriveGraph,
|
|
16
18
|
deriveInvocations,
|
|
19
|
+
type GraphEdge,
|
|
17
20
|
type GraphNode,
|
|
21
|
+
type GraphState,
|
|
22
|
+
type GroupedGraphNode,
|
|
18
23
|
type Invocation,
|
|
24
|
+
subtreeGraph,
|
|
19
25
|
type TraceNode,
|
|
20
26
|
traceSubgraph,
|
|
21
27
|
} from "../graph.js";
|
|
@@ -38,12 +44,14 @@ const badgeClass = (outcome: Invocation["outcome"]) =>
|
|
|
38
44
|
// ── Custom nodes ────────────────────────────────────────────────────────────
|
|
39
45
|
|
|
40
46
|
interface TopologyNodeData extends Record<string, unknown> {
|
|
41
|
-
node:
|
|
47
|
+
node: GroupedGraphNode;
|
|
42
48
|
selected: boolean;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/** Live-topology node: status dot, name + kind, invocation badge; pulses on each
|
|
46
|
-
* new invocation, tinted by outcome.
|
|
52
|
+
* new invocation, tinted by outcome. A node that owns spawned children (a
|
|
53
|
+
* templated kind) shows an "n internal" badge — clicking it opens the drill-down
|
|
54
|
+
* pane with that resource and the children it spawned. */
|
|
47
55
|
function TopologyNode({ data }: NodeProps<Node<TopologyNodeData>>) {
|
|
48
56
|
const { node, selected } = data;
|
|
49
57
|
const [pulse, setPulse] = useState<"" | "ok" | "bad">("");
|
|
@@ -61,6 +69,7 @@ function TopologyNode({ data }: NodeProps<Node<TopologyNodeData>>) {
|
|
|
61
69
|
const cls = [
|
|
62
70
|
"tdbg-node",
|
|
63
71
|
`tdbg-node-${node.status}`,
|
|
72
|
+
node.childCount > 0 ? "tdbg-node-owner" : "",
|
|
64
73
|
selected ? "tdbg-node-sel" : "",
|
|
65
74
|
pulse ? `tdbg-node-pulse-${pulse}` : "",
|
|
66
75
|
]
|
|
@@ -84,6 +93,9 @@ function TopologyNode({ data }: NodeProps<Node<TopologyNodeData>>) {
|
|
|
84
93
|
)}
|
|
85
94
|
</div>
|
|
86
95
|
<div className="tdbg-node-kind">{node.kind}</div>
|
|
96
|
+
{node.childCount > 0 && (
|
|
97
|
+
<span className="tdbg-node-internal">{node.childCount} internal ›</span>
|
|
98
|
+
)}
|
|
87
99
|
<Handle type="source" position={Position.Right} className="tdbg-node-handle" />
|
|
88
100
|
</div>
|
|
89
101
|
);
|
|
@@ -114,12 +126,12 @@ function TraceFlowNode({ data }: NodeProps<Node<TraceNodeData>>) {
|
|
|
114
126
|
<Handle type="target" position={Position.Left} className="tdbg-node-handle" />
|
|
115
127
|
<div className="tdbg-node-head">
|
|
116
128
|
{node.isRoot && <span className="tdbg-node-roottag">root</span>}
|
|
117
|
-
<span className="tdbg-node-name">{node.name}</span>
|
|
129
|
+
<span className="tdbg-node-name">{node.label ?? node.name}</span>
|
|
118
130
|
<span className={badgeClass(last.outcome)}>
|
|
119
131
|
{node.invocations.length > 1 ? `${node.invocations.length}×` : last.suffix}
|
|
120
132
|
</span>
|
|
121
133
|
</div>
|
|
122
|
-
<div className="tdbg-node-kind">{node.kind}</div>
|
|
134
|
+
<div className="tdbg-node-kind">{node.label ? `${node.kind} · ${node.name}` : node.kind}</div>
|
|
123
135
|
<Handle type="source" position={Position.Right} className="tdbg-node-handle" />
|
|
124
136
|
</div>
|
|
125
137
|
);
|
|
@@ -149,6 +161,29 @@ function layoutPositions(graph: {
|
|
|
149
161
|
return pos;
|
|
150
162
|
}
|
|
151
163
|
|
|
164
|
+
/** Map graph edges to ReactFlow edges, dropping any whose endpoints aren't shown.
|
|
165
|
+
* Ownership edges (owner → child) render dashed + muted so they read as
|
|
166
|
+
* containment, not data flow. */
|
|
167
|
+
function buildFlowEdges(edges: readonly GraphEdge[], present: ReadonlySet<string>): Edge[] {
|
|
168
|
+
return edges
|
|
169
|
+
.filter((e) => present.has(e.source) && present.has(e.target))
|
|
170
|
+
.map((e) => ({
|
|
171
|
+
id: e.id,
|
|
172
|
+
source: e.source,
|
|
173
|
+
target: e.target,
|
|
174
|
+
...(e.ownership
|
|
175
|
+
? {
|
|
176
|
+
className: "tdbg-edge-owner",
|
|
177
|
+
style: { stroke: "var(--tdbg-muted)", strokeDasharray: "5 4", opacity: 0.65 },
|
|
178
|
+
}
|
|
179
|
+
: {}),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The main canvas shows the topology fully collapsed — templated resources are
|
|
184
|
+
* one node each; their children are reached through the drill-down pane. */
|
|
185
|
+
const NO_EXPANSION: ReadonlySet<string> = new Set();
|
|
186
|
+
|
|
152
187
|
// ── The view ────────────────────────────────────────────────────────────────
|
|
153
188
|
|
|
154
189
|
/**
|
|
@@ -162,6 +197,10 @@ export function EventGraph({ events, resolveUrl }: EventGraphProps) {
|
|
|
162
197
|
const [traceId, setTraceId] = useState<number | null>(null);
|
|
163
198
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
164
199
|
|
|
200
|
+
// Drill-down stack of owner ids: clicking a templated resource pushes it and
|
|
201
|
+
// opens the nested pane; the breadcrumb pops back. Empty = pane closed.
|
|
202
|
+
const [drilldown, setDrilldown] = useState<string[]>([]);
|
|
203
|
+
|
|
165
204
|
// Default on: standalone services / providers with no dependency wiring are
|
|
166
205
|
// usually noise in the topology view.
|
|
167
206
|
const [hideUnconnected, setHideUnconnected] = useState(true);
|
|
@@ -176,19 +215,23 @@ export function EventGraph({ events, resolveUrl }: EventGraphProps) {
|
|
|
176
215
|
[trace, activeTraceId],
|
|
177
216
|
);
|
|
178
217
|
|
|
218
|
+
// The main canvas is the fully-collapsed topology: a templated resource is one
|
|
219
|
+
// node (absorbing its children's edges); the children are reached by clicking it.
|
|
220
|
+
const collapsed = useMemo(() => collapseTopology(topology, NO_EXPANSION), [topology]);
|
|
221
|
+
|
|
179
222
|
// Topology with isolated nodes (no incoming/outgoing edge) dropped when the
|
|
180
223
|
// toggle is on. Only meaningful for the live topology — trace subgraphs are all
|
|
181
224
|
// connected by call edges.
|
|
182
225
|
const displayTopology = useMemo(() => {
|
|
183
|
-
if (!hideUnconnected) return
|
|
226
|
+
if (!hideUnconnected) return collapsed;
|
|
184
227
|
const connected = new Set<string>();
|
|
185
|
-
for (const e of
|
|
228
|
+
for (const e of collapsed.edges) {
|
|
186
229
|
connected.add(e.source);
|
|
187
230
|
connected.add(e.target);
|
|
188
231
|
}
|
|
189
|
-
return { nodes:
|
|
190
|
-
}, [
|
|
191
|
-
const hiddenCount =
|
|
232
|
+
return { nodes: collapsed.nodes.filter((n) => connected.has(n.id)), edges: collapsed.edges };
|
|
233
|
+
}, [collapsed, hideUnconnected]);
|
|
234
|
+
const hiddenCount = collapsed.nodes.length - displayTopology.nodes.length;
|
|
192
235
|
|
|
193
236
|
const graph = scoped ?? displayTopology;
|
|
194
237
|
const topoKey = useMemo(
|
|
@@ -219,16 +262,26 @@ export function EventGraph({ events, resolveUrl }: EventGraphProps) {
|
|
|
219
262
|
}, [scoped, displayTopology, positions, selectedNodeId]);
|
|
220
263
|
|
|
221
264
|
const flowEdges = useMemo(
|
|
222
|
-
() =>
|
|
223
|
-
graph.edges
|
|
224
|
-
.filter((e) => present.has(e.source) && present.has(e.target))
|
|
225
|
-
.map((e) => ({ id: e.id, source: e.source, target: e.target })),
|
|
265
|
+
() => buildFlowEdges(graph.edges as readonly GraphEdge[], present),
|
|
226
266
|
[graph, present],
|
|
227
267
|
);
|
|
228
268
|
|
|
229
269
|
const selectTrace = (id: number | null) => {
|
|
230
270
|
setTraceId(id);
|
|
231
271
|
setSelectedNodeId(null);
|
|
272
|
+
setDrilldown([]);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Clicking a templated resource (one that owns children) opens the drill-down
|
|
276
|
+
// pane; any other node opens its inputs/outputs detail.
|
|
277
|
+
const onTopologyNodeClick = (id: string) => {
|
|
278
|
+
const node = displayTopology.nodes.find((n) => n.id === id);
|
|
279
|
+
if (node && node.childCount > 0) {
|
|
280
|
+
setDrilldown([id]);
|
|
281
|
+
setSelectedNodeId(null);
|
|
282
|
+
} else {
|
|
283
|
+
setSelectedNodeId(id);
|
|
284
|
+
}
|
|
232
285
|
};
|
|
233
286
|
|
|
234
287
|
return (
|
|
@@ -244,8 +297,8 @@ export function EventGraph({ events, resolveUrl }: EventGraphProps) {
|
|
|
244
297
|
<div className="tdbg-empty">
|
|
245
298
|
{activeTraceId !== null
|
|
246
299
|
? "No resources in this trace."
|
|
247
|
-
: hideUnconnected &&
|
|
248
|
-
? `All ${
|
|
300
|
+
: hideUnconnected && collapsed.nodes.length > 0
|
|
301
|
+
? `All ${collapsed.nodes.length} resource(s) are unconnected — turn off "Hide unconnected" to show them.`
|
|
249
302
|
: "No resources yet — waiting for the stream…"}
|
|
250
303
|
</div>
|
|
251
304
|
) : (
|
|
@@ -262,7 +315,9 @@ export function EventGraph({ events, resolveUrl }: EventGraphProps) {
|
|
|
262
315
|
fitView
|
|
263
316
|
minZoom={0.2}
|
|
264
317
|
proOptions={{ hideAttribution: true }}
|
|
265
|
-
onNodeClick={(_e, node) =>
|
|
318
|
+
onNodeClick={(_e, node) =>
|
|
319
|
+
scoped ? setSelectedNodeId(node.id) : onTopologyNodeClick(node.id)
|
|
320
|
+
}
|
|
266
321
|
onPaneClick={() => setSelectedNodeId(null)}
|
|
267
322
|
>
|
|
268
323
|
<Background />
|
|
@@ -299,10 +354,146 @@ export function EventGraph({ events, resolveUrl }: EventGraphProps) {
|
|
|
299
354
|
onClose={() => setSelectedNodeId(null)}
|
|
300
355
|
/>
|
|
301
356
|
))}
|
|
357
|
+
{!scoped && drilldown.length > 0 && (
|
|
358
|
+
<>
|
|
359
|
+
<div className="tdbg-nested-backdrop" onClick={() => setDrilldown([])} />
|
|
360
|
+
{drilldown.map((ownerId, i) => (
|
|
361
|
+
<NestedGraphPane
|
|
362
|
+
key={`${ownerId}@${i}`}
|
|
363
|
+
topology={topology}
|
|
364
|
+
ownerId={ownerId}
|
|
365
|
+
depth={i}
|
|
366
|
+
isTop={i === drilldown.length - 1}
|
|
367
|
+
onDrill={(childId) => setDrilldown((d) => [...d.slice(0, i + 1), childId])}
|
|
368
|
+
onReveal={() => setDrilldown((d) => d.slice(0, i + 1))}
|
|
369
|
+
onClose={() => setDrilldown((d) => d.slice(0, i))}
|
|
370
|
+
resolveUrl={resolveUrl}
|
|
371
|
+
/>
|
|
372
|
+
))}
|
|
373
|
+
</>
|
|
374
|
+
)}
|
|
302
375
|
</div>
|
|
303
376
|
);
|
|
304
377
|
}
|
|
305
378
|
|
|
379
|
+
/**
|
|
380
|
+
* One panel in the drill-down stack — a templated resource and the children it
|
|
381
|
+
* spawned (`subtreeGraph`), wired by dashed ownership edges plus the dependency
|
|
382
|
+
* edges among them. Each drill-in pushes a *new* panel (cascaded to the right);
|
|
383
|
+
* clicking a child that owns its own children opens the next panel, recursive to
|
|
384
|
+
* any depth. Only the top panel is interactive; clicking a panel beneath pops the
|
|
385
|
+
* stack back to it, and clicking a leaf (or the parent) opens its detail.
|
|
386
|
+
*/
|
|
387
|
+
function NestedGraphPane({
|
|
388
|
+
topology,
|
|
389
|
+
ownerId,
|
|
390
|
+
depth,
|
|
391
|
+
isTop,
|
|
392
|
+
onDrill,
|
|
393
|
+
onReveal,
|
|
394
|
+
onClose,
|
|
395
|
+
resolveUrl,
|
|
396
|
+
}: {
|
|
397
|
+
topology: GraphState;
|
|
398
|
+
ownerId: string;
|
|
399
|
+
depth: number;
|
|
400
|
+
isTop: boolean;
|
|
401
|
+
onDrill: (childId: string) => void;
|
|
402
|
+
onReveal: () => void;
|
|
403
|
+
onClose: () => void;
|
|
404
|
+
resolveUrl: (rel: string) => string;
|
|
405
|
+
}) {
|
|
406
|
+
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
407
|
+
|
|
408
|
+
const nodeById = useMemo(() => new Map(topology.nodes.map((n) => [n.id, n])), [topology]);
|
|
409
|
+
const sub = useMemo(() => subtreeGraph(topology, ownerId), [topology, ownerId]);
|
|
410
|
+
const positions = useMemo(() => layoutPositions(sub), [sub]);
|
|
411
|
+
const present = useMemo(() => new Set(sub.nodes.map((n) => n.id)), [sub]);
|
|
412
|
+
|
|
413
|
+
const flowNodes: Node[] = useMemo(
|
|
414
|
+
() =>
|
|
415
|
+
sub.nodes.map((n) => ({
|
|
416
|
+
id: n.id,
|
|
417
|
+
type: "topology",
|
|
418
|
+
position: positions.get(n.id) ?? { x: 0, y: 0 },
|
|
419
|
+
data: { node: n, selected: n.id === selectedNodeId } satisfies TopologyNodeData,
|
|
420
|
+
})),
|
|
421
|
+
[sub, positions, selectedNodeId],
|
|
422
|
+
);
|
|
423
|
+
const flowEdges = useMemo(() => buildFlowEdges(sub.edges, present), [sub, present]);
|
|
424
|
+
|
|
425
|
+
const onNodeClick = (id: string) => {
|
|
426
|
+
const node = sub.nodes.find((n) => n.id === id);
|
|
427
|
+
// A child that owns its own children opens the next panel; the parent itself
|
|
428
|
+
// or a leaf opens its detail in this panel.
|
|
429
|
+
if (id !== ownerId && node && node.childCount > 0) onDrill(id);
|
|
430
|
+
else setSelectedNodeId(id);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const owner = nodeById.get(ownerId);
|
|
434
|
+
const detailNode = selectedNodeId ? nodeById.get(selectedNodeId) : undefined;
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<aside
|
|
438
|
+
className={`tdbg-nested-pane${isTop ? "" : " tdbg-nested-pane-under"}`}
|
|
439
|
+
// `--tdbg-depth` drives the cascade offset (`left`) in CSS, so a media query
|
|
440
|
+
// can tighten it on a narrow screen; z-index stacks the panels.
|
|
441
|
+
style={{ zIndex: 20 + depth, ["--tdbg-depth"]: depth } as CSSProperties}
|
|
442
|
+
// A panel beneath the top acts as a single "go back here" surface.
|
|
443
|
+
onClick={isTop ? undefined : onReveal}
|
|
444
|
+
>
|
|
445
|
+
<div className="tdbg-nested-head">
|
|
446
|
+
<span className="tdbg-nested-title">
|
|
447
|
+
<span className="tdbg-node-name">{owner?.name ?? ownerId}</span>
|
|
448
|
+
{owner && <span className="tdbg-muted">{owner.kind}</span>}
|
|
449
|
+
</span>
|
|
450
|
+
<button
|
|
451
|
+
className="tdbg-btn tdbg-icon-btn"
|
|
452
|
+
onClick={(e) => {
|
|
453
|
+
e.stopPropagation();
|
|
454
|
+
onClose();
|
|
455
|
+
}}
|
|
456
|
+
title="Close"
|
|
457
|
+
>
|
|
458
|
+
✕
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
<div className="tdbg-nested-body" style={{ pointerEvents: isTop ? "auto" : "none" }}>
|
|
462
|
+
<div className="tdbg-nested-canvas">
|
|
463
|
+
{sub.nodes.length === 0 ? (
|
|
464
|
+
<div className="tdbg-empty">This resource spawned no children.</div>
|
|
465
|
+
) : (
|
|
466
|
+
<ReactFlow
|
|
467
|
+
key={ownerId}
|
|
468
|
+
nodes={flowNodes}
|
|
469
|
+
edges={flowEdges}
|
|
470
|
+
nodeTypes={nodeTypes}
|
|
471
|
+
nodesDraggable={false}
|
|
472
|
+
nodesConnectable={false}
|
|
473
|
+
edgesFocusable={false}
|
|
474
|
+
fitView
|
|
475
|
+
minZoom={0.2}
|
|
476
|
+
proOptions={{ hideAttribution: true }}
|
|
477
|
+
onNodeClick={(_e, node) => onNodeClick(node.id)}
|
|
478
|
+
onPaneClick={() => setSelectedNodeId(null)}
|
|
479
|
+
>
|
|
480
|
+
<Background />
|
|
481
|
+
<Controls showInteractive={false} />
|
|
482
|
+
</ReactFlow>
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
{detailNode && (
|
|
486
|
+
<NodeDetail
|
|
487
|
+
node={detailNode}
|
|
488
|
+
resolveUrl={resolveUrl}
|
|
489
|
+
onClose={() => setSelectedNodeId(null)}
|
|
490
|
+
/>
|
|
491
|
+
)}
|
|
492
|
+
</div>
|
|
493
|
+
</aside>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
306
497
|
function countSubtree(trace: ReturnType<typeof deriveInvocations>, rootId: number): number {
|
|
307
498
|
let n = 0;
|
|
308
499
|
const visit = (id: number) => {
|
|
@@ -369,6 +560,9 @@ function NodeDetail({
|
|
|
369
560
|
}) {
|
|
370
561
|
if (!node) return null;
|
|
371
562
|
const i = node.lastInvoke;
|
|
563
|
+
const props = node.properties;
|
|
564
|
+
const hasProps =
|
|
565
|
+
!!props && typeof props === "object" && Object.keys(props as object).length > 0;
|
|
372
566
|
return (
|
|
373
567
|
<aside className="tdbg-graph-detail">
|
|
374
568
|
<DetailHead name={node.name} onClose={onClose} />
|
|
@@ -377,6 +571,12 @@ function NodeDetail({
|
|
|
377
571
|
<span className={`tdbg-badge tdbg-suffix-${node.status}`}>{node.status}</span>
|
|
378
572
|
<span className="tdbg-muted">{node.invokeCount} invocation(s)</span>
|
|
379
573
|
</div>
|
|
574
|
+
{hasProps && (
|
|
575
|
+
<div className="tdbg-graph-detail-body">
|
|
576
|
+
<div className="tdbg-graph-detail-label">Properties</div>
|
|
577
|
+
<PayloadInspector value={props} resolveUrl={resolveUrl} />
|
|
578
|
+
</div>
|
|
579
|
+
)}
|
|
380
580
|
{i ? (
|
|
381
581
|
<div className="tdbg-graph-detail-body">
|
|
382
582
|
<div className="tdbg-graph-detail-sub">
|
|
@@ -420,6 +620,12 @@ function TraceNodeDetail({
|
|
|
420
620
|
<span className="tdbg-muted">{inv.timestamp.slice(11, 23)}</span>
|
|
421
621
|
</div>
|
|
422
622
|
<InOut inv={inv} resolveUrl={resolveUrl} />
|
|
623
|
+
{inv.context !== undefined && (
|
|
624
|
+
<>
|
|
625
|
+
<div className="tdbg-graph-detail-label">Available context</div>
|
|
626
|
+
<PayloadInspector value={inv.context} resolveUrl={resolveUrl} />
|
|
627
|
+
</>
|
|
628
|
+
)}
|
|
423
629
|
</div>
|
|
424
630
|
))}
|
|
425
631
|
</div>
|
|
@@ -333,6 +333,7 @@
|
|
|
333
333
|
display: flex;
|
|
334
334
|
flex: 1;
|
|
335
335
|
min-height: 0;
|
|
336
|
+
position: relative;
|
|
336
337
|
}
|
|
337
338
|
.tdbg-graph-canvas {
|
|
338
339
|
flex: 1;
|
|
@@ -349,6 +350,77 @@
|
|
|
349
350
|
--xy-controls-button-color-hover-default: var(--tdbg-accent);
|
|
350
351
|
--xy-controls-button-border-color-default: var(--tdbg-line);
|
|
351
352
|
background: var(--tdbg-bg);
|
|
353
|
+
/* xyflow ships no `touch-action`, so on touch the browser claims one-finger
|
|
354
|
+
* drags and d3-zoom never pans/zooms. Hand the gestures to the canvas. */
|
|
355
|
+
touch-action: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* ── Drill-down stack ───────────────────────────────────────────────────── */
|
|
359
|
+
/* One backdrop behind the whole stack; clicking it closes every panel. */
|
|
360
|
+
.tdbg-nested-backdrop {
|
|
361
|
+
position: absolute;
|
|
362
|
+
inset: 0;
|
|
363
|
+
z-index: 19;
|
|
364
|
+
background: rgba(0, 0, 0, 0.45);
|
|
365
|
+
}
|
|
366
|
+
/* Each drill level is its own panel, docked right and cascaded left (via inline
|
|
367
|
+
* `left` + `z-index`) so the panels beneath peek out as a navigable stack. */
|
|
368
|
+
.tdbg-nested-pane {
|
|
369
|
+
position: absolute;
|
|
370
|
+
top: 0;
|
|
371
|
+
right: 0;
|
|
372
|
+
bottom: 0;
|
|
373
|
+
/* Cascade offset: each deeper panel starts further right (`--tdbg-depth` set by
|
|
374
|
+
* the component), leaving the panels beneath peeking on the left. */
|
|
375
|
+
left: calc(10% + var(--tdbg-depth, 0) * 46px);
|
|
376
|
+
display: flex;
|
|
377
|
+
flex-direction: column;
|
|
378
|
+
min-width: 0;
|
|
379
|
+
background: var(--tdbg-bg);
|
|
380
|
+
border-left: 1px solid var(--tdbg-line);
|
|
381
|
+
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.3);
|
|
382
|
+
}
|
|
383
|
+
/* A panel beneath the top is a single "go back here" surface. */
|
|
384
|
+
.tdbg-nested-pane-under {
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
}
|
|
387
|
+
.tdbg-nested-pane-under::after {
|
|
388
|
+
content: "";
|
|
389
|
+
position: absolute;
|
|
390
|
+
inset: 0;
|
|
391
|
+
background: rgba(0, 0, 0, 0.2);
|
|
392
|
+
pointer-events: none;
|
|
393
|
+
}
|
|
394
|
+
.tdbg-nested-head {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 8px;
|
|
398
|
+
padding: 6px 10px;
|
|
399
|
+
border-bottom: 1px solid var(--tdbg-line);
|
|
400
|
+
background: var(--tdbg-panel);
|
|
401
|
+
}
|
|
402
|
+
.tdbg-nested-title {
|
|
403
|
+
display: flex;
|
|
404
|
+
align-items: baseline;
|
|
405
|
+
gap: 8px;
|
|
406
|
+
flex: 1;
|
|
407
|
+
min-width: 0;
|
|
408
|
+
overflow: hidden;
|
|
409
|
+
}
|
|
410
|
+
.tdbg-nested-title .tdbg-node-name {
|
|
411
|
+
overflow: hidden;
|
|
412
|
+
text-overflow: ellipsis;
|
|
413
|
+
}
|
|
414
|
+
.tdbg-nested-body {
|
|
415
|
+
display: flex;
|
|
416
|
+
flex: 1;
|
|
417
|
+
min-height: 0;
|
|
418
|
+
}
|
|
419
|
+
.tdbg-nested-canvas {
|
|
420
|
+
flex: 1;
|
|
421
|
+
min-width: 0;
|
|
422
|
+
min-height: 0;
|
|
423
|
+
position: relative;
|
|
352
424
|
}
|
|
353
425
|
|
|
354
426
|
.tdbg-node {
|
|
@@ -429,6 +501,19 @@
|
|
|
429
501
|
overflow: hidden;
|
|
430
502
|
text-overflow: ellipsis;
|
|
431
503
|
}
|
|
504
|
+
.tdbg-node-owner {
|
|
505
|
+
border-left: 3px solid var(--tdbg-accent);
|
|
506
|
+
}
|
|
507
|
+
.tdbg-node-internal {
|
|
508
|
+
display: inline-block;
|
|
509
|
+
margin-top: 3px;
|
|
510
|
+
padding: 0 6px;
|
|
511
|
+
border-radius: 999px;
|
|
512
|
+
background: var(--tdbg-accent-bg, var(--tdbg-panel));
|
|
513
|
+
color: var(--tdbg-accent);
|
|
514
|
+
font-size: 10px;
|
|
515
|
+
font-weight: 600;
|
|
516
|
+
}
|
|
432
517
|
.tdbg-node-pulse-ok {
|
|
433
518
|
animation: tdbg-pulse-ok 0.65s ease-out;
|
|
434
519
|
}
|
|
@@ -491,7 +576,10 @@
|
|
|
491
576
|
.tdbg-graph-detail-body {
|
|
492
577
|
display: flex;
|
|
493
578
|
flex-direction: column;
|
|
494
|
-
|
|
579
|
+
/* Don't let flex shrink the body below its content — otherwise each payload
|
|
580
|
+
* (overflow: auto) collapses into a tiny nested scrollbar. The whole detail
|
|
581
|
+
* aside (overflow: auto) is the single scroll container. */
|
|
582
|
+
flex-shrink: 0;
|
|
495
583
|
}
|
|
496
584
|
.tdbg-graph-detail-sub {
|
|
497
585
|
display: flex;
|
|
@@ -597,3 +685,79 @@
|
|
|
597
685
|
.tdbg-graph-toggle input {
|
|
598
686
|
cursor: pointer;
|
|
599
687
|
}
|
|
688
|
+
|
|
689
|
+
/* ── Mobile (≤640px) ────────────────────────────────────────────────────────
|
|
690
|
+
* The desktop layout leans on fixed-width side rails (220px trace list, 340px
|
|
691
|
+
* detail) and a single-row header. On a phone we wrap the header, stack the graph
|
|
692
|
+
* vertically (trace list → top scroll strip, detail → bottom sheet), and let the
|
|
693
|
+
* drill-down panels go near-full-width. */
|
|
694
|
+
@media (max-width: 640px) {
|
|
695
|
+
.tdbg-tabs {
|
|
696
|
+
flex-wrap: wrap;
|
|
697
|
+
row-gap: 6px;
|
|
698
|
+
}
|
|
699
|
+
.tdbg-tab,
|
|
700
|
+
.tdbg-btn {
|
|
701
|
+
min-height: 34px;
|
|
702
|
+
}
|
|
703
|
+
.tdbg-bar-row {
|
|
704
|
+
flex-wrap: wrap;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/* Graph: stack the rails above/below the canvas instead of beside it. */
|
|
708
|
+
.tdbg-graph {
|
|
709
|
+
flex-direction: column;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/* Trace list → a horizontal scroll strip of invocation chips. */
|
|
713
|
+
.tdbg-trace-list {
|
|
714
|
+
width: 100%;
|
|
715
|
+
flex-direction: row;
|
|
716
|
+
align-items: stretch;
|
|
717
|
+
max-height: 30vh;
|
|
718
|
+
overflow-x: auto;
|
|
719
|
+
overflow-y: hidden;
|
|
720
|
+
border-right: none;
|
|
721
|
+
border-bottom: 1px solid var(--tdbg-line);
|
|
722
|
+
}
|
|
723
|
+
.tdbg-trace-list-head {
|
|
724
|
+
position: static;
|
|
725
|
+
flex: 0 0 auto;
|
|
726
|
+
display: flex;
|
|
727
|
+
align-items: center;
|
|
728
|
+
border-bottom: none;
|
|
729
|
+
border-right: 1px solid var(--tdbg-line);
|
|
730
|
+
}
|
|
731
|
+
.tdbg-trace-row {
|
|
732
|
+
width: auto;
|
|
733
|
+
flex: 0 0 auto;
|
|
734
|
+
border-bottom: none;
|
|
735
|
+
border-right: 1px solid var(--tdbg-line);
|
|
736
|
+
white-space: nowrap;
|
|
737
|
+
}
|
|
738
|
+
.tdbg-trace-row .tdbg-node-name {
|
|
739
|
+
flex: 0 0 auto;
|
|
740
|
+
}
|
|
741
|
+
.tdbg-trace-row-on {
|
|
742
|
+
box-shadow: inset 0 -2px 0 var(--tdbg-accent);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* Detail → a bottom sheet overlaying the canvas, not a 340px side column. */
|
|
746
|
+
.tdbg-graph-detail {
|
|
747
|
+
position: absolute;
|
|
748
|
+
left: 0;
|
|
749
|
+
right: 0;
|
|
750
|
+
bottom: 0;
|
|
751
|
+
width: auto;
|
|
752
|
+
max-height: 55%;
|
|
753
|
+
z-index: 10;
|
|
754
|
+
border-left: none;
|
|
755
|
+
border-top: 1px solid var(--tdbg-line);
|
|
756
|
+
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.3);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/* Drill-down panels go near-full-width with a tight cascade. */
|
|
760
|
+
.tdbg-nested-pane {
|
|
761
|
+
left: calc(var(--tdbg-depth, 0) * 14px);
|
|
762
|
+
}
|
|
763
|
+
}
|
package/src/filter.ts
CHANGED
|
@@ -23,8 +23,12 @@ export function matchesFilter(event: DebugEvent, filter: EventFilter): boolean {
|
|
|
23
23
|
if (suffixes.length > 0 && !suffixes.includes(eventSuffix(event.event))) {
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
|
-
if (filter.kind
|
|
27
|
-
|
|
26
|
+
if (filter.kind) {
|
|
27
|
+
// Lifecycle events carry the kind in the dotted name; dispatch (trace) events
|
|
28
|
+
// carry it in `payload.ref.kind` (the name dropped the kind prefix). Match either.
|
|
29
|
+
const refKind = (event.payload as { ref?: { kind?: unknown } } | undefined)?.ref?.kind;
|
|
30
|
+
const haystack = `${event.event} ${typeof refKind === "string" ? refKind : ""}`.toLowerCase();
|
|
31
|
+
if (!haystack.includes(filter.kind.toLowerCase())) return false;
|
|
28
32
|
}
|
|
29
33
|
if (filter.text) {
|
|
30
34
|
const needle = filter.text.toLowerCase();
|