feat: use endpointUri for cross-route drill-down instead of label parsing
Server: - Add endpointUri to PositionedNode (from RouteNode) - Add fromEndpointUri to RouteSummary (catalog API) - Catalog controller resolves endpoint URI from diagram store UI: - Build endpointRouteMap from catalog's fromEndpointUri field - Drill-down uses exact match on node.endpointUri against the map - Remove label parsing heuristics (extractTargetEndpoint, camelToKebab) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<String> routeIds = routesByApp.getOrDefault(appId, Set.of());
|
||||
List<String> agentIds = agents.stream().map(AgentInfo::id).toList();
|
||||
List<RouteSummary> 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<String> agentIds) {
|
||||
return diagramStore.findContentHashForRouteByAgents(routeId, agentIds)
|
||||
.flatMap(diagramStore::findByContentHash)
|
||||
.map(RouteGraph::getRoot)
|
||||
.map(root -> root.getEndpointUri())
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String computeWorstHealth(List<AgentInfo> agents) {
|
||||
boolean hasDead = false;
|
||||
boolean hasStale = false;
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
) {}
|
||||
|
||||
@@ -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<PositionedNode> children
|
||||
List<PositionedNode> children,
|
||||
String endpointUri
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface DiagramNode {
|
||||
width?: number;
|
||||
height?: number;
|
||||
children?: DiagramNode[];
|
||||
endpointUri?: string;
|
||||
}
|
||||
|
||||
export interface DiagramEdge {
|
||||
|
||||
@@ -14,6 +14,7 @@ interface ExecutionDiagramProps {
|
||||
executionDetail?: ExecutionDetail;
|
||||
direction?: 'LR' | 'TB';
|
||||
knownRouteIds?: Set<string>;
|
||||
endpointRouteMap?: Map<string, string>;
|
||||
onNodeAction?: (nodeId: string, action: NodeAction) => void;
|
||||
nodeConfigs?: Map<string, NodeConfig>;
|
||||
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}
|
||||
|
||||
@@ -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>): 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(
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface ProcessDiagramProps {
|
||||
nodeConfigs?: Map<string, NodeConfig>;
|
||||
/** Known route IDs for this application (enables drill-down resolution) */
|
||||
knownRouteIds?: Set<string>;
|
||||
/** Maps from() endpoint URI → routeId for cross-route drill-down */
|
||||
endpointRouteMap?: Map<string, string>;
|
||||
className?: string;
|
||||
/** Pre-fetched diagram layout (bypasses internal fetch by application/routeId) */
|
||||
diagramLayout?: DiagramLayout;
|
||||
|
||||
@@ -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<string, string>();
|
||||
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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user