feat: resolved URI display and drill-down for TO/TO_DYNAMIC nodes
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

- Show resolved endpoint URI as teal italic line on diagram nodes
  when execution overlay is active
- Enable drill-down for TO and TO_DYNAMIC nodes (not just DIRECT/SEDA)
- Use runtime resolvedEndpointUri from execution overlay for drill-down
  when static endpointUri doesn't match
- Increase node height from 50px to 56px to accommodate the third line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-29 16:30:11 +02:00
parent 32cde5363f
commit 090c51c809
5 changed files with 47 additions and 17 deletions

View File

@@ -53,7 +53,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
}
private static final int PADDING = 20;
private static final int NODE_HEIGHT = 50;
private static final int NODE_HEIGHT = 56;
private static final int NODE_WIDTH = 220;
private static final int COMPOUND_TOP_PADDING = 30;
private static final int COMPOUND_SIDE_PADDING = 10;

View File

@@ -10,6 +10,8 @@ export interface NodeExecutionState {
subRouteFailed?: boolean;
/** True if trace data is available for this processor */
hasTraceData?: boolean;
/** Runtime-resolved endpoint URI (for TO_DYNAMIC, etc.) */
resolvedEndpointUri?: string;
}
export interface IterationInfo {

View File

@@ -60,6 +60,7 @@ function buildOverlay(
durationMs: proc.durationMs ?? 0,
subRouteFailed: subRouteFailed || undefined,
hasTraceData: !!proc.hasTraceData,
resolvedEndpointUri: proc.resolvedEndpointUri || undefined,
});
// Recurse into children

View File

@@ -42,6 +42,7 @@ export function DiagramNode({
// Extract label parts: type name and detail
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
const detail = node.label || '';
const resolvedUri = executionState?.resolvedEndpointUri;
// Overlay state derivation
const isCompleted = executionState?.status === 'COMPLETED';
@@ -125,15 +126,33 @@ export function DiagramNode({
)}
</g>
{/* Type name + detail (clipped to available width) */}
{/* Type name + detail + resolved URI (clipped to available width) */}
<g clipPath={`url(#clip-${node.id})`}>
<text x={TEXT_LEFT} y={h / 2 - 1} fill={labelColor} fontSize={11} fontWeight={600}>
{typeName}
</text>
{detail && detail !== typeName && (
<text x={TEXT_LEFT} y={h / 2 + 12} fill={isFailed ? '#C0392B' : '#5C5347'} fontSize={10}>
{detail}
</text>
{resolvedUri ? (
<>
<text x={TEXT_LEFT} y={TOP_BAR_HEIGHT + 12} fill={labelColor} fontSize={11} fontWeight={600}>
{typeName}
</text>
{detail && detail !== typeName && (
<text x={TEXT_LEFT} y={TOP_BAR_HEIGHT + 24} fill={isFailed ? '#C0392B' : '#5C5347'} fontSize={10}>
{detail}
</text>
)}
<text x={TEXT_LEFT} y={h - 5} fill="#1A7F8E" fontSize={9} fontStyle="italic">
{resolvedUri.split('?')[0]}
</text>
</>
) : (
<>
<text x={TEXT_LEFT} y={h / 2 - 1} fill={labelColor} fontSize={11} fontWeight={600}>
{typeName}
</text>
{detail && detail !== typeName && (
<text x={TEXT_LEFT} y={h / 2 + 12} fill={isFailed ? '#C0392B' : '#5C5347'} fontSize={10}>
{detail}
</text>
)}
</>
)}
</g>

View File

@@ -16,7 +16,7 @@ import styles from './ProcessDiagram.module.css';
const PADDING = 40;
/** Types that support drill-down — double-click navigates to the target route */
const DRILLDOWN_TYPES = new Set(['DIRECT', 'SEDA']);
const DRILLDOWN_TYPES = new Set(['DIRECT', 'SEDA', 'TO', 'TO_DYNAMIC']);
export function ProcessDiagram({
application,
@@ -168,17 +168,26 @@ export function ProcessDiagram({
const node = findNodeById(sections, nodeId);
if (!node || !DRILLDOWN_TYPES.has(node.type ?? '')) return;
// Resolve via endpointUri → endpointRouteMap (exact match, no heuristics)
const uri = node.endpointUri;
if (!uri) return;
const stripped = uri.split('?')[0];
const resolved = endpointRouteMap?.get(stripped);
// Try static endpointUri first, then runtime-resolved URI from execution overlay
const staticUri = node.endpointUri;
const runtimeUri = executionOverlay?.get(nodeId)?.resolvedEndpointUri;
function normalize(uri: string): string {
return uri.split('?')[0];
}
function tryResolve(uri: string | undefined): string | undefined {
if (!uri) return undefined;
return endpointRouteMap?.get(normalize(uri));
}
const resolved = tryResolve(staticUri) ?? tryResolve(runtimeUri);
if (resolved) {
onNodeSelect?.('');
setRouteStack(prev => [...prev, resolved]);
}
},
[sections, onNodeSelect, endpointRouteMap],
[sections, onNodeSelect, endpointRouteMap, executionOverlay],
);
const handleBreadcrumbClick = useCallback(
@@ -201,7 +210,6 @@ export function ProcessDiagram({
(e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
if (routeStack.length > 1) {
// Go back one level
setRouteStack(prev => prev.slice(0, -1));
} else {
onNodeSelect?.('');