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.AgentSummary;
|
||||||
import com.cameleer3.server.app.dto.AppCatalogEntry;
|
import com.cameleer3.server.app.dto.AppCatalogEntry;
|
||||||
import com.cameleer3.server.app.dto.RouteSummary;
|
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.AgentInfo;
|
||||||
import com.cameleer3.server.core.agent.AgentRegistryService;
|
import com.cameleer3.server.core.agent.AgentRegistryService;
|
||||||
import com.cameleer3.server.core.agent.AgentState;
|
import com.cameleer3.server.core.agent.AgentState;
|
||||||
|
import com.cameleer3.server.core.storage.DiagramStore;
|
||||||
import com.cameleer3.server.core.storage.StatsStore;
|
import com.cameleer3.server.core.storage.StatsStore;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
@@ -34,10 +36,14 @@ import java.util.stream.Collectors;
|
|||||||
public class RouteCatalogController {
|
public class RouteCatalogController {
|
||||||
|
|
||||||
private final AgentRegistryService registryService;
|
private final AgentRegistryService registryService;
|
||||||
|
private final DiagramStore diagramStore;
|
||||||
private final JdbcTemplate jdbc;
|
private final JdbcTemplate jdbc;
|
||||||
|
|
||||||
public RouteCatalogController(AgentRegistryService registryService, JdbcTemplate jdbc) {
|
public RouteCatalogController(AgentRegistryService registryService,
|
||||||
|
DiagramStore diagramStore,
|
||||||
|
JdbcTemplate jdbc) {
|
||||||
this.registryService = registryService;
|
this.registryService = registryService;
|
||||||
|
this.diagramStore = diagramStore;
|
||||||
this.jdbc = jdbc;
|
this.jdbc = jdbc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,12 +120,14 @@ public class RouteCatalogController {
|
|||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
Set<String> routeIds = routesByApp.getOrDefault(appId, Set.of());
|
Set<String> routeIds = routesByApp.getOrDefault(appId, Set.of());
|
||||||
|
List<String> agentIds = agents.stream().map(AgentInfo::id).toList();
|
||||||
List<RouteSummary> routeSummaries = routeIds.stream()
|
List<RouteSummary> routeSummaries = routeIds.stream()
|
||||||
.map(routeId -> {
|
.map(routeId -> {
|
||||||
String key = appId + "/" + routeId;
|
String key = appId + "/" + routeId;
|
||||||
long count = routeExchangeCounts.getOrDefault(key, 0L);
|
long count = routeExchangeCounts.getOrDefault(key, 0L);
|
||||||
Instant lastSeen = routeLastSeen.get(key);
|
Instant lastSeen = routeLastSeen.get(key);
|
||||||
return new RouteSummary(routeId, count, lastSeen);
|
String fromUri = resolveFromEndpointUri(routeId, agentIds);
|
||||||
|
return new RouteSummary(routeId, count, lastSeen, fromUri);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -141,6 +149,15 @@ public class RouteCatalogController {
|
|||||||
return ResponseEntity.ok(catalog);
|
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) {
|
private String computeWorstHealth(List<AgentInfo> agents) {
|
||||||
boolean hasDead = false;
|
boolean hasDead = false;
|
||||||
boolean hasStale = false;
|
boolean hasStale = false;
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
getAbsoluteX(wrapperElk, rootNode),
|
getAbsoluteX(wrapperElk, rootNode),
|
||||||
getAbsoluteY(wrapperElk, rootNode),
|
getAbsoluteY(wrapperElk, rootNode),
|
||||||
wrapperElk.getWidth(), wrapperElk.getHeight(),
|
wrapperElk.getWidth(), wrapperElk.getHeight(),
|
||||||
wrapperChildren));
|
wrapperChildren, null));
|
||||||
ctx.compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
|
ctx.compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
|
||||||
}
|
}
|
||||||
// Handler children in order: DO_FINALLY first, then DO_CATCH
|
// 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",
|
rn.getType() != null ? rn.getType().name() : "UNKNOWN",
|
||||||
absX, absY,
|
absX, absY,
|
||||||
elkNode.getWidth(), elkNode.getHeight(),
|
elkNode.getWidth(), elkNode.getHeight(),
|
||||||
children
|
children,
|
||||||
|
rn.getEndpointUri()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ import java.time.Instant;
|
|||||||
public record RouteSummary(
|
public record RouteSummary(
|
||||||
@NotNull String routeId,
|
@NotNull String routeId,
|
||||||
@NotNull long exchangeCount,
|
@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}
|
* For compound nodes (CHOICE, SPLIT, TRY_CATCH, etc.), {@code children}
|
||||||
* contains the nested child nodes rendered inside the parent bounds.
|
* contains the nested child nodes rendered inside the parent bounds.
|
||||||
*
|
*
|
||||||
* @param id node identifier (matches RouteNode.id)
|
* @param id node identifier (matches RouteNode.id)
|
||||||
* @param label display label
|
* @param label display label
|
||||||
* @param type NodeType name (e.g., "ENDPOINT", "PROCESSOR")
|
* @param type NodeType name (e.g., "ENDPOINT", "PROCESSOR")
|
||||||
* @param x horizontal position
|
* @param x horizontal position
|
||||||
* @param y vertical position
|
* @param y vertical position
|
||||||
* @param width node width
|
* @param width node width
|
||||||
* @param height node height
|
* @param height node height
|
||||||
* @param children nested child nodes for compound/swimlane groups
|
* @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(
|
public record PositionedNode(
|
||||||
String id,
|
String id,
|
||||||
@@ -25,6 +26,7 @@ public record PositionedNode(
|
|||||||
double y,
|
double y,
|
||||||
double width,
|
double width,
|
||||||
double height,
|
double height,
|
||||||
List<PositionedNode> children
|
List<PositionedNode> children,
|
||||||
|
String endpointUri
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface DiagramNode {
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
children?: DiagramNode[];
|
children?: DiagramNode[];
|
||||||
|
endpointUri?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiagramEdge {
|
export interface DiagramEdge {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ExecutionDiagramProps {
|
|||||||
executionDetail?: ExecutionDetail;
|
executionDetail?: ExecutionDetail;
|
||||||
direction?: 'LR' | 'TB';
|
direction?: 'LR' | 'TB';
|
||||||
knownRouteIds?: Set<string>;
|
knownRouteIds?: Set<string>;
|
||||||
|
endpointRouteMap?: Map<string, string>;
|
||||||
onNodeAction?: (nodeId: string, action: NodeAction) => void;
|
onNodeAction?: (nodeId: string, action: NodeAction) => void;
|
||||||
nodeConfigs?: Map<string, NodeConfig>;
|
nodeConfigs?: Map<string, NodeConfig>;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -58,6 +59,7 @@ export function ExecutionDiagram({
|
|||||||
executionDetail: externalDetail,
|
executionDetail: externalDetail,
|
||||||
direction = 'LR',
|
direction = 'LR',
|
||||||
knownRouteIds,
|
knownRouteIds,
|
||||||
|
endpointRouteMap,
|
||||||
onNodeAction,
|
onNodeAction,
|
||||||
nodeConfigs,
|
nodeConfigs,
|
||||||
className,
|
className,
|
||||||
@@ -166,6 +168,7 @@ export function ExecutionDiagram({
|
|||||||
onNodeAction={onNodeAction}
|
onNodeAction={onNodeAction}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
knownRouteIds={knownRouteIds}
|
knownRouteIds={knownRouteIds}
|
||||||
|
endpointRouteMap={endpointRouteMap}
|
||||||
executionOverlay={overlay}
|
executionOverlay={overlay}
|
||||||
iterationState={iterationState}
|
iterationState={iterationState}
|
||||||
onIterationChange={setIteration}
|
onIterationChange={setIteration}
|
||||||
|
|||||||
@@ -18,30 +18,6 @@ const PADDING = 40;
|
|||||||
/** Types that support drill-down — double-click navigates to the target route */
|
/** 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']);
|
||||||
|
|
||||||
/** 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({
|
export function ProcessDiagram({
|
||||||
application,
|
application,
|
||||||
routeId,
|
routeId,
|
||||||
@@ -51,6 +27,7 @@ export function ProcessDiagram({
|
|||||||
onNodeAction,
|
onNodeAction,
|
||||||
nodeConfigs,
|
nodeConfigs,
|
||||||
knownRouteIds,
|
knownRouteIds,
|
||||||
|
endpointRouteMap,
|
||||||
className,
|
className,
|
||||||
diagramLayout,
|
diagramLayout,
|
||||||
executionOverlay,
|
executionOverlay,
|
||||||
@@ -190,17 +167,18 @@ export function ProcessDiagram({
|
|||||||
(nodeId: string) => {
|
(nodeId: string) => {
|
||||||
const node = findNodeById(sections, nodeId);
|
const node = findNodeById(sections, nodeId);
|
||||||
if (!node || !DRILLDOWN_TYPES.has(node.type ?? '')) return;
|
if (!node || !DRILLDOWN_TYPES.has(node.type ?? '')) return;
|
||||||
const endpoint = extractTargetEndpoint(node);
|
|
||||||
if (!endpoint) return;
|
// Resolve via endpointUri → endpointRouteMap (exact match, no heuristics)
|
||||||
const resolved = knownRouteIds
|
const uri = node.endpointUri;
|
||||||
? resolveRouteId(endpoint, knownRouteIds)
|
if (!uri) return;
|
||||||
: endpoint;
|
const stripped = uri.split('?')[0];
|
||||||
|
const resolved = endpointRouteMap?.get(stripped);
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
onNodeSelect?.('');
|
onNodeSelect?.('');
|
||||||
setRouteStack(prev => [...prev, resolved]);
|
setRouteStack(prev => [...prev, resolved]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sections, onNodeSelect, knownRouteIds],
|
[sections, onNodeSelect, endpointRouteMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBreadcrumbClick = useCallback(
|
const handleBreadcrumbClick = useCallback(
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface ProcessDiagramProps {
|
|||||||
nodeConfigs?: Map<string, NodeConfig>;
|
nodeConfigs?: Map<string, NodeConfig>;
|
||||||
/** Known route IDs for this application (enables drill-down resolution) */
|
/** Known route IDs for this application (enables drill-down resolution) */
|
||||||
knownRouteIds?: Set<string>;
|
knownRouteIds?: Set<string>;
|
||||||
|
/** Maps from() endpoint URI → routeId for cross-route drill-down */
|
||||||
|
endpointRouteMap?: Map<string, string>;
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Pre-fetched diagram layout (bypasses internal fetch by application/routeId) */
|
/** Pre-fetched diagram layout (bypasses internal fetch by application/routeId) */
|
||||||
diagramLayout?: DiagramLayout;
|
diagramLayout?: DiagramLayout;
|
||||||
|
|||||||
@@ -136,6 +136,21 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
|||||||
return ids;
|
return ids;
|
||||||
}, [catalog]);
|
}, [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)
|
// Build nodeConfigs from tracing store + app config (for TRACE/TAP badges)
|
||||||
const { data: appConfig } = useApplicationConfig(appId);
|
const { data: appConfig } = useApplicationConfig(appId);
|
||||||
const tracedMap = useTracingStore((s) => s.tracedProcessors[appId]);
|
const tracedMap = useTracingStore((s) => s.tracedProcessors[appId]);
|
||||||
@@ -172,6 +187,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
|||||||
executionId={exchangeId}
|
executionId={exchangeId}
|
||||||
executionDetail={detail}
|
executionDetail={detail}
|
||||||
knownRouteIds={knownRouteIds}
|
knownRouteIds={knownRouteIds}
|
||||||
|
endpointRouteMap={endpointRouteMap}
|
||||||
onNodeAction={handleNodeAction}
|
onNodeAction={handleNodeAction}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
/>
|
/>
|
||||||
@@ -187,6 +203,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
|||||||
routeId={routeId}
|
routeId={routeId}
|
||||||
diagramLayout={diagramQuery.data}
|
diagramLayout={diagramQuery.data}
|
||||||
knownRouteIds={knownRouteIds}
|
knownRouteIds={knownRouteIds}
|
||||||
|
endpointRouteMap={endpointRouteMap}
|
||||||
onNodeAction={handleNodeAction}
|
onNodeAction={handleNodeAction}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user