Add route diagram page with execution overlay and group-aware APIs
Backend: Add group filtering to agent list, search, stats, and timeseries
endpoints. Add diagram lookup by group+routeId. Resolve application group
to agent IDs server-side for ClickHouse IN-clause queries.
Frontend: New route detail page at /apps/{group}/routes/{routeId} with
three tabs (Diagram, Performance, Processor Tree). SVG diagram rendering
with panzoom, execution overlay (glow effects, duration/sequence badges,
flow particles, minimap), and processor detail panel. uPlot charts for
performance tab replacing old SVG sparklines. Ctrl+Click from
ExecutionExplorer navigates to route diagram with overlay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
130
ui/src/hooks/useExecutionOverlay.ts
Normal file
130
ui/src/hooks/useExecutionOverlay.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import type { ExecutionDetail, ProcessorNode } from '../api/types';
|
||||
|
||||
export interface IterationData {
|
||||
count: number;
|
||||
current: number;
|
||||
}
|
||||
|
||||
export interface OverlayState {
|
||||
isActive: boolean;
|
||||
toggle: () => void;
|
||||
executedNodes: Set<string>;
|
||||
executedEdges: Set<string>;
|
||||
durations: Map<string, number>;
|
||||
sequences: Map<string, number>;
|
||||
iterationData: Map<string, IterationData>;
|
||||
selectedNodeId: string | null;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
setIteration: (nodeId: string, iteration: number) => void;
|
||||
}
|
||||
|
||||
/** Walk the processor tree and collect execution data keyed by diagramNodeId */
|
||||
function collectProcessorData(
|
||||
processors: ProcessorNode[],
|
||||
executedNodes: Set<string>,
|
||||
durations: Map<string, number>,
|
||||
sequences: Map<string, number>,
|
||||
counter: { seq: number },
|
||||
) {
|
||||
for (const proc of processors) {
|
||||
const nodeId = proc.diagramNodeId;
|
||||
if (nodeId) {
|
||||
executedNodes.add(nodeId);
|
||||
durations.set(nodeId, proc.durationMs ?? 0);
|
||||
sequences.set(nodeId, ++counter.seq);
|
||||
}
|
||||
if (proc.children && proc.children.length > 0) {
|
||||
collectProcessorData(proc.children, executedNodes, durations, sequences, counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Determine which edges are executed (both source and target are executed) */
|
||||
function computeExecutedEdges(
|
||||
executedNodes: Set<string>,
|
||||
edges: Array<{ sourceId?: string; targetId?: string }>,
|
||||
): Set<string> {
|
||||
const result = new Set<string>();
|
||||
for (const edge of edges) {
|
||||
if (edge.sourceId && edge.targetId
|
||||
&& executedNodes.has(edge.sourceId) && executedNodes.has(edge.targetId)) {
|
||||
result.add(`${edge.sourceId}->${edge.targetId}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function useExecutionOverlay(
|
||||
execution: ExecutionDetail | null | undefined,
|
||||
edges: Array<{ sourceId?: string; targetId?: string }> = [],
|
||||
): OverlayState {
|
||||
const [isActive, setIsActive] = useState(!!execution);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
const [iterations, setIterations] = useState<Map<string, number>>(new Map());
|
||||
|
||||
// Activate overlay when an execution is loaded
|
||||
useEffect(() => {
|
||||
if (execution) setIsActive(true);
|
||||
}, [execution]);
|
||||
|
||||
const { executedNodes, durations, sequences, iterationData } = useMemo(() => {
|
||||
const en = new Set<string>();
|
||||
const dur = new Map<string, number>();
|
||||
const seq = new Map<string, number>();
|
||||
const iter = new Map<string, IterationData>();
|
||||
|
||||
if (!execution?.processors) {
|
||||
return { executedNodes: en, durations: dur, sequences: seq, iterationData: iter };
|
||||
}
|
||||
|
||||
collectProcessorData(execution.processors, en, dur, seq, { seq: 0 });
|
||||
|
||||
return { executedNodes: en, durations: dur, sequences: seq, iterationData: iter };
|
||||
}, [execution]);
|
||||
|
||||
const executedEdges = useMemo(
|
||||
() => computeExecutedEdges(executedNodes, edges),
|
||||
[executedNodes, edges],
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => setIsActive((v) => !v), []);
|
||||
const selectNode = useCallback((nodeId: string | null) => setSelectedNodeId(nodeId), []);
|
||||
const setIteration = useCallback((nodeId: string, iteration: number) => {
|
||||
setIterations((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(nodeId, iteration);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcut: E to toggle overlay
|
||||
useEffect(() => {
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === 'e' || e.key === 'E') {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') return;
|
||||
e.preventDefault();
|
||||
setIsActive((v) => !v);
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isActive,
|
||||
toggle,
|
||||
executedNodes,
|
||||
executedEdges,
|
||||
durations,
|
||||
sequences,
|
||||
iterationData: new Map([...iterationData].map(([k, v]) => {
|
||||
const current = iterations.get(k) ?? v.current;
|
||||
return [k, { ...v, current }];
|
||||
})),
|
||||
selectedNodeId,
|
||||
selectNode,
|
||||
setIteration,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user