diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java index 8c03b7e2..0843d2fc 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/RouteCatalogController.java @@ -3,9 +3,11 @@ package com.cameleer3.server.app.controller; import com.cameleer3.server.app.dto.AgentSummary; import com.cameleer3.server.app.dto.AppCatalogEntry; import com.cameleer3.server.app.dto.RouteSummary; +import com.cameleer3.common.graph.RouteGraph; import com.cameleer3.server.core.agent.AgentInfo; import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.agent.AgentState; +import com.cameleer3.server.core.storage.DiagramStore; import com.cameleer3.server.core.storage.StatsStore; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -34,10 +36,14 @@ import java.util.stream.Collectors; public class RouteCatalogController { private final AgentRegistryService registryService; + private final DiagramStore diagramStore; private final JdbcTemplate jdbc; - public RouteCatalogController(AgentRegistryService registryService, JdbcTemplate jdbc) { + public RouteCatalogController(AgentRegistryService registryService, + DiagramStore diagramStore, + JdbcTemplate jdbc) { this.registryService = registryService; + this.diagramStore = diagramStore; this.jdbc = jdbc; } @@ -114,12 +120,14 @@ public class RouteCatalogController { // Routes Set routeIds = routesByApp.getOrDefault(appId, Set.of()); + List agentIds = agents.stream().map(AgentInfo::id).toList(); List routeSummaries = routeIds.stream() .map(routeId -> { String key = appId + "/" + routeId; long count = routeExchangeCounts.getOrDefault(key, 0L); Instant lastSeen = routeLastSeen.get(key); - return new RouteSummary(routeId, count, lastSeen); + String fromUri = resolveFromEndpointUri(routeId, agentIds); + return new RouteSummary(routeId, count, lastSeen, fromUri); }) .toList(); @@ -141,6 +149,15 @@ public class RouteCatalogController { return ResponseEntity.ok(catalog); } + /** Resolve the from() endpoint URI for a route by looking up its diagram. */ + private String resolveFromEndpointUri(String routeId, List agentIds) { + return diagramStore.findContentHashForRouteByAgents(routeId, agentIds) + .flatMap(diagramStore::findByContentHash) + .map(RouteGraph::getRoot) + .map(root -> root.getEndpointUri()) + .orElse(null); + } + private String computeWorstHealth(List agents) { boolean hasDead = false; boolean hasStale = false; diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java index cc3f64c2..9934d2dc 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java @@ -666,7 +666,7 @@ public class ElkDiagramRenderer implements DiagramRenderer { getAbsoluteX(wrapperElk, rootNode), getAbsoluteY(wrapperElk, rootNode), wrapperElk.getWidth(), wrapperElk.getHeight(), - wrapperChildren)); + wrapperChildren, null)); ctx.compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE)); } // Handler children in order: DO_FINALLY first, then DO_CATCH @@ -693,7 +693,8 @@ public class ElkDiagramRenderer implements DiagramRenderer { rn.getType() != null ? rn.getType().name() : "UNKNOWN", absX, absY, elkNode.getWidth(), elkNode.getHeight(), - children + children, + rn.getEndpointUri() ); } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/RouteSummary.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/RouteSummary.java index 355322fe..177957cb 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/RouteSummary.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/RouteSummary.java @@ -9,5 +9,7 @@ import java.time.Instant; public record RouteSummary( @NotNull String routeId, @NotNull long exchangeCount, - Instant lastSeen + Instant lastSeen, + @Schema(description = "The from() endpoint URI, e.g. 'direct:processOrder'") + String fromEndpointUri ) {} diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/PositionedNode.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/PositionedNode.java index 7dca6fd0..9f664278 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/PositionedNode.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/PositionedNode.java @@ -8,14 +8,15 @@ import java.util.List; * For compound nodes (CHOICE, SPLIT, TRY_CATCH, etc.), {@code children} * contains the nested child nodes rendered inside the parent bounds. * - * @param id node identifier (matches RouteNode.id) - * @param label display label - * @param type NodeType name (e.g., "ENDPOINT", "PROCESSOR") - * @param x horizontal position - * @param y vertical position - * @param width node width - * @param height node height - * @param children nested child nodes for compound/swimlane groups + * @param id node identifier (matches RouteNode.id) + * @param label display label + * @param type NodeType name (e.g., "ENDPOINT", "PROCESSOR") + * @param x horizontal position + * @param y vertical position + * @param width node width + * @param height node height + * @param children nested child nodes for compound/swimlane groups + * @param endpointUri the Camel endpoint URI (e.g., "direct:processOrder"), null for non-endpoint nodes */ public record PositionedNode( String id, @@ -25,6 +26,7 @@ public record PositionedNode( double y, double width, double height, - List children + List children, + String endpointUri ) { } diff --git a/ui/src/api/queries/diagrams.ts b/ui/src/api/queries/diagrams.ts index 1c805fe1..eb63d8fe 100644 --- a/ui/src/api/queries/diagrams.ts +++ b/ui/src/api/queries/diagrams.ts @@ -10,6 +10,7 @@ export interface DiagramNode { width?: number; height?: number; children?: DiagramNode[]; + endpointUri?: string; } export interface DiagramEdge { diff --git a/ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx b/ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx index 3b6544ef..7a2c355c 100644 --- a/ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx +++ b/ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx @@ -14,6 +14,7 @@ interface ExecutionDiagramProps { executionDetail?: ExecutionDetail; direction?: 'LR' | 'TB'; knownRouteIds?: Set; + endpointRouteMap?: Map; onNodeAction?: (nodeId: string, action: NodeAction) => void; nodeConfigs?: Map; className?: string; @@ -58,6 +59,7 @@ export function ExecutionDiagram({ executionDetail: externalDetail, direction = 'LR', knownRouteIds, + endpointRouteMap, onNodeAction, nodeConfigs, className, @@ -166,6 +168,7 @@ export function ExecutionDiagram({ onNodeAction={onNodeAction} nodeConfigs={nodeConfigs} knownRouteIds={knownRouteIds} + endpointRouteMap={endpointRouteMap} executionOverlay={overlay} iterationState={iterationState} onIterationChange={setIteration} diff --git a/ui/src/components/ProcessDiagram/ProcessDiagram.tsx b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx index a19ea37e..4e621ab7 100644 --- a/ui/src/components/ProcessDiagram/ProcessDiagram.tsx +++ b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx @@ -18,30 +18,6 @@ const PADDING = 40; /** Types that support drill-down — double-click navigates to the target route */ const DRILLDOWN_TYPES = new Set(['DIRECT', 'SEDA']); -/** Extract the target endpoint name from a node's label */ -function extractTargetEndpoint(node: DiagramNodeType): string | null { - // Labels like "to: direct:orderProcessing" or "direct:orderProcessing" - const label = node.label ?? ''; - const match = label.match(/(?:to:\s*)?(?:direct|seda):(\S+)/i); - return match ? match[1] : null; -} - -/** Convert camelCase to kebab-case: "callGetProduct" → "call-get-product" */ -function camelToKebab(s: string): string { - return s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); -} - -/** - * Resolve a direct/seda endpoint name to a routeId. - * Tries: exact match, kebab-case conversion, then gives up. - */ -function resolveRouteId(endpoint: string, knownRouteIds: Set): string | null { - if (knownRouteIds.has(endpoint)) return endpoint; - const kebab = camelToKebab(endpoint); - if (knownRouteIds.has(kebab)) return kebab; - return null; -} - export function ProcessDiagram({ application, routeId, @@ -51,6 +27,7 @@ export function ProcessDiagram({ onNodeAction, nodeConfigs, knownRouteIds, + endpointRouteMap, className, diagramLayout, executionOverlay, @@ -190,17 +167,18 @@ export function ProcessDiagram({ (nodeId: string) => { const node = findNodeById(sections, nodeId); if (!node || !DRILLDOWN_TYPES.has(node.type ?? '')) return; - const endpoint = extractTargetEndpoint(node); - if (!endpoint) return; - const resolved = knownRouteIds - ? resolveRouteId(endpoint, knownRouteIds) - : endpoint; + + // 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); if (resolved) { onNodeSelect?.(''); setRouteStack(prev => [...prev, resolved]); } }, - [sections, onNodeSelect, knownRouteIds], + [sections, onNodeSelect, endpointRouteMap], ); const handleBreadcrumbClick = useCallback( diff --git a/ui/src/components/ProcessDiagram/types.ts b/ui/src/components/ProcessDiagram/types.ts index e7a6e516..ccc0b288 100644 --- a/ui/src/components/ProcessDiagram/types.ts +++ b/ui/src/components/ProcessDiagram/types.ts @@ -26,6 +26,8 @@ export interface ProcessDiagramProps { nodeConfigs?: Map; /** Known route IDs for this application (enables drill-down resolution) */ knownRouteIds?: Set; + /** Maps from() endpoint URI → routeId for cross-route drill-down */ + endpointRouteMap?: Map; className?: string; /** Pre-fetched diagram layout (bypasses internal fetch by application/routeId) */ diagramLayout?: DiagramLayout; diff --git a/ui/src/pages/Exchanges/ExchangesPage.tsx b/ui/src/pages/Exchanges/ExchangesPage.tsx index 23373da2..824688f1 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.tsx +++ b/ui/src/pages/Exchanges/ExchangesPage.tsx @@ -136,6 +136,21 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS return ids; }, [catalog]); + // Build endpoint URI → routeId map for cross-route drill-down + const endpointRouteMap = useMemo(() => { + const map = new Map(); + if (catalog) { + for (const app of catalog as any[]) { + for (const r of app.routes || []) { + if (r.fromEndpointUri) { + map.set(r.fromEndpointUri, r.routeId); + } + } + } + } + return map; + }, [catalog]); + // Build nodeConfigs from tracing store + app config (for TRACE/TAP badges) const { data: appConfig } = useApplicationConfig(appId); const tracedMap = useTracingStore((s) => s.tracedProcessors[appId]); @@ -172,6 +187,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS executionId={exchangeId} executionDetail={detail} knownRouteIds={knownRouteIds} + endpointRouteMap={endpointRouteMap} onNodeAction={handleNodeAction} nodeConfigs={nodeConfigs} /> @@ -187,6 +203,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS routeId={routeId} diagramLayout={diagramQuery.data} knownRouteIds={knownRouteIds} + endpointRouteMap={endpointRouteMap} onNodeAction={handleNodeAction} nodeConfigs={nodeConfigs} />