New interactive route diagram component with SVG rendering using server-computed ELK layout coordinates. TIBCO BW5-inspired top-bar card node style with zoom/pan, hover toolbars, config badges, and error handler sections below the main flow. Backend: add direction query parameter (LR/TB) to diagram render endpoints, defaulting to left-to-right layout. Frontend: 14-file ProcessDiagram component in ui/src/components/ with DiagramNode, CompoundNode, DiagramEdge, ConfigBadge, NodeToolbar, ErrorSection, ZoomControls, and supporting hooks. Dev test page at /dev/diagram for validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
Interactive Process Diagram — Design Spec
Sub-project: 1 of 3 (Component → Execution Overlay → Page Integration) Scope: Interactive SVG diagram component with zoom/pan, node interactions, config badges, and a configurable layout direction. Does NOT include execution overlay or page replacement — those are sub-projects 2 and 3.
Problem
The current RouteFlow component renders Camel routes as a flat vertical list of nodes. It cannot show compound structures (choice branches, split fan-out, try-catch nesting), does not support zoom/pan, and has no interactive controls beyond click-to-select. Routes with 10+ processors become hard to follow, and the relationship between processors is not visually clear.
Goal
Build an interactive process diagram component styled after MuleSoft / TIBCO BusinessWorks 5, rendering Camel routes as left-to-right flow diagrams using server-computed ELK layout coordinates. The component supports zoom/pan, node hover toolbars for tracing/tap configuration, config badge indicators, and a collapsible detail side-panel.
Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Rendering | SVG + custom React | Full control over styling, no heavy deps. Server owns layout. |
| Node style | Top-Bar Cards | TIBCO BW5-inspired white cards with colored top accent bar. Professional, clean. |
| Flow direction | Left-to-right (default) | Matches MuleSoft/BW5 conventions. Query param for flexibility. |
| Component location | ui/src/components/ProcessDiagram/ |
Tightly coupled to Cameleer data model, no design-system abstraction needed. |
| Interactions | Hover floating toolbar + click-to-select | Discoverable, no right-click dependency. |
| Error handlers | Below main flow | Clear visual separation, labeled divider. |
| Selection behavior | Side panel with config info; execution data only with overlay | Keeps base diagram focused on topology. |
1. Backend: Layout Direction Parameter
Change
Add optional direction query parameter to diagram render endpoints.
Files
cameleer3-server-app/.../diagram/ElkDiagramRenderer.java— accept direction param, map to ELKDirection.RIGHT(LR) orDirection.DOWN(TB)cameleer3-server-core/.../diagram/DiagramRenderer.java— update interface to accept directioncameleer3-server-app/.../controller/DiagramRenderController.java— add@RequestParam(defaultValue = "LR") String directionto render endpointsui/src/api/queries/diagrams.ts— passdirectionquery param to API calls; also updateDiagramLayoutedge type to match backendPositionedEdgeserialization:{ sourceId, targetId, label?, points: number[][] }(currently defines{ from?, to? }which is missingpointsandlabel)
Behavior
GET /diagrams/{contentHash}/render?direction=LR→ left-to-right layout (default)GET /diagrams/{contentHash}/render?direction=TB→ top-to-bottom layoutGET /diagrams?application=X&routeId=Y&direction=LR→ same for by-route endpoint
Compound Node Direction
The direction parameter applies to the root layout only. Compound nodes (CHOICE, SPLIT, TRY_CATCH, etc.) keep their internal layout direction as top-to-bottom regardless of the root direction. This matches how MuleSoft/BW5 render branching patterns: the main flow goes left-to-right, but branches within a choice or split fan out vertically inside their container.
2. Frontend: ProcessDiagram Component
File Structure
ui/src/components/ProcessDiagram/
├── ProcessDiagram.tsx # Root: SVG container, zoom/pan, section layout
├── ProcessDiagram.module.css # Styles using design system tokens
├── DiagramNode.tsx # Individual node: top-bar card rendering
├── DiagramEdge.tsx # Edge: cubic Bezier path with arrowhead
├── CompoundNode.tsx # Container for compound types (choice, split)
├── NodeToolbar.tsx # Floating action toolbar on hover
├── ConfigBadge.tsx # Indicator badges (TRACE, TAP) on nodes
├── ErrorSection.tsx # Visual separator + error handler flow section
├── ZoomControls.tsx # HTML overlay: zoom in/out/fit buttons
├── useZoomPan.ts # Hook: viewBox transform, wheel zoom, drag pan
├── useDiagramData.ts # Hook: fetch + separate layout into sections
├── node-colors.ts # NodeType → design system color token mapping
├── types.ts # Shared TypeScript interfaces
└── index.ts # Public exports
Props API
interface ProcessDiagramProps {
application: string;
routeId: string;
direction?: 'LR' | 'TB'; // default 'LR'
selectedNodeId?: string; // controlled selection
onNodeSelect?: (nodeId: string) => void;
onNodeAction?: (nodeId: string, action: NodeAction) => void;
nodeConfigs?: Map<string, NodeConfig>; // active taps/tracing per processor
className?: string;
}
type NodeAction = 'inspect' | 'toggle-trace' | 'configure-tap' | 'copy-id';
interface NodeConfig {
traceEnabled?: boolean;
tapExpression?: string;
}
// ExecutionOverlay types will be added in sub-project 2 when needed.
// No forward-declared types here to avoid drift.
SVG Structure
<div class="process-diagram">
<svg viewBox="..."> // zoom = viewBox transform
<defs> // arrowhead markers, filters
<marker id="arrow">...</marker>
</defs>
<g class="diagram-content"> // pan offset transform
<!-- Main Route section -->
<g class="section section--main">
<g class="edges"> // rendered first (behind nodes)
<path d="M ... C ..." /> // cubic bezier from ELK waypoints
</g>
<g class="nodes">
<g transform="translate(x,y)"> // ELK-computed position
<!-- DiagramNode: top-bar card -->
<!-- ConfigBadge: top-right corner pills -->
<!-- NodeToolbar: foreignObject on hover -->
</g>
<g class="compound"> // CompoundNode: dashed border container
<g transform="translate(...)"> <!-- children inside -->
</g>
</g>
</g>
<!-- Error Handler section(s) -->
<g class="section section--error"
transform="translate(0, mainHeight + gap)">
<text>onException: java.lang.Exception</text>
<line ... /> // divider
<g class="edges">...</g>
<g class="nodes">...</g>
</g>
</g>
</svg>
<div class="zoom-controls">...</div> // HTML overlay, bottom-right
</div>
3. Node Visual States
Base States
| State | Visual |
|---|---|
| Normal | White card, --border (#E4DFD8), colored top bar per type |
| Hovered | Warm tint background (--bg-hover / #F5F0EA), stronger border, floating toolbar appears above |
| Selected | Amber selection ring (2.5px solid --amber), side panel opens |
Config Badges
Small colored pill badges positioned at the top-right corner of the node card, always visible:
- TRACE — teal (
--running) pill, shown when tracing is enabled - TAP — purple (
--purple) pill, shown when a tap expression is configured
Execution Overlay States (sub-project 2 — node must support these props)
| State | Visual |
|---|---|
| Executed (OK) | Green left border or subtle green tint |
| Failed (caused error handler) | Red border (2px --error), red marker icon |
| Not executed | Dimmed (reduced opacity) |
| Has trace data | Small "data available" indicator icon |
| No trace data | No indicator (or grayed-out data icon) |
Node Type Colors
| Category | Token | Hex | Types |
|---|---|---|---|
| Endpoints | --running |
#1A7F8E teal | ENDPOINT |
| Processors | --amber |
#C6820E | PROCESSOR, BEAN, LOG, SET_HEADER, SET_BODY, TRANSFORM, MARSHAL, UNMARSHAL |
| Targets | --success |
#3D7C47 green | TO, TO_DYNAMIC, DIRECT, SEDA |
| EIP Patterns | --purple |
#7C3AED | EIP_CHOICE, EIP_WHEN, EIP_OTHERWISE, EIP_SPLIT, EIP_MULTICAST, EIP_LOOP, EIP_AGGREGATE, EIP_FILTER, etc. |
| Error Handling | --error |
#C0392B red | ERROR_HANDLER, ON_EXCEPTION, TRY_CATCH, DO_TRY, DO_CATCH, DO_FINALLY |
| Cross-Route | (hardcoded) | #06B6D4 cyan | EIP_WIRE_TAP, EIP_ENRICH, EIP_POLL_ENRICH |
Note: This frontend color mapping intentionally differs from the backend ElkDiagramRenderer SVG colors (which use blue for endpoints, green for processors). The frontend uses design system tokens for consistency with the rest of the UI. The backend SVG renderer is not changed.
Compound Node Rendering
Compound types (CHOICE, SPLIT, TRY_CATCH, LOOP, etc.) render as:
- Full-width colored header bar with white text label (type name)
- White body area with subtle border matching the type color
- Children rendered inside at their ELK-relative positions
- Children have their own hover/select/badge behavior
4. Interactions
Hover Floating Toolbar
On mouse enter over a node, a dark floating toolbar appears above the node (centered). Uses <foreignObject> for HTML accessibility.
| Icon | Action | Callback |
|---|---|---|
| Search | Inspect | onNodeAction(id, 'inspect') — selects node, opens side panel |
| T | Toggle Trace | onNodeAction(id, 'toggle-trace') — enables/disables tracing |
| Pencil | Configure Tap | onNodeAction(id, 'configure-tap') — opens tap config |
| ... | More | onNodeAction(id, 'copy-id') — copies processor ID |
Toolbar hides on mouse leave after a short delay (150ms) to prevent flicker when moving between node and toolbar.
Click-to-Select
Click on a node → calls onNodeSelect(nodeId). Parent controls selectedNodeId prop. Selected node shows amber ring.
Zoom & Pan
useZoomPan hook manages:
- Mouse wheel → zoom centered on cursor
- Click+drag on background → pan
- Pinch gesture → zoom (trackpad/touch)
- State:
{ scale, translateX, translateY } - Applied to SVG
viewBoxattribute
ZoomControls component:
- Three buttons:
+(zoom in),−(zoom out), fit-to-view icon - Positioned as HTML overlay at bottom-right of diagram container
- Fit-to-view calculates viewBox to show entire diagram with 40px padding
Zoom limits: 25% to 400%.
Keyboard Navigation
Required:
| Key | Action |
|---|---|
| Escape | Deselect / close panel |
| +/- | Zoom in/out |
| 0 | Fit to view |
Stretch (implement if time permits):
| Key | Action |
|---|---|
| Arrow keys | Move selection between connected nodes |
| Tab | Cycle through nodes in flow order |
| Enter | Open detail panel for selected node |
5. Error Handler Sections
Error handler compounds (ON_EXCEPTION, ERROR_HANDLER) render as separate sections below the main flow:
- Divider: Horizontal line with label text (e.g., "onException: java.lang.Exception")
- Gap: 40px vertical gap between main section and error section
- Layout: Error section gets its own ELK-computed layout (compound node children already have relative coordinates)
- Styling: Same node rendering as main section, but the section background has a subtle red tint
- Multiple handlers: Each ON_EXCEPTION becomes its own section, stacked vertically
The useDiagramData hook separates top-level compound error nodes from regular nodes, computing the Y offset for each error section based on accumulated heights.
6. Data Flow
useDiagramByRoute(app, routeId)
→ contentHash
→ useDiagramLayout(contentHash, direction)
→ DiagramLayout { nodes[], edges[], width, height }
useDiagramData hook:
1. Separate nodes into mainNodes[] and errorSections[]
(reuses logic from buildFlowSegments: error-handler compounds with children → error sections)
2. Filter edges: mainEdges (between main nodes), errorEdges (within each error section)
3. Compute total SVG dimensions: max(mainWidth, errorWidths) × (mainHeight + gap + errorHeights)
4. Return { mainNodes, mainEdges, errorSections, totalWidth, totalHeight }
The existing diagram-mapping.ts buildFlowSegments function handles the separation logic. The new useDiagramData hook adapts this for SVG coordinate-based rendering instead of RouteFlow's FlowSegment format.
7. Side Panel (Detail Panel)
When a node is selected, a collapsible side panel slides in from the right of the diagram container.
Base mode (no execution overlay):
- Processor ID
- Processor type
- Endpoint URI (if applicable)
- Active configuration: tracing status, tap expression
- Node metadata from the diagram
With execution overlay (sub-project 2):
- Execution status + duration
- Input/output body (if trace data captured)
- Input/output headers
- Error message + stack trace (if failed)
- Loop iteration selector (if inside a loop)
For sub-project 1, the side panel shows config info only. The component accepts an onNodeSelect callback — the parent page controls what appears in the panel.
The side panel is NOT part of the ProcessDiagram component itself. It is rendered by the parent page and controlled via the selectedNodeId / onNodeSelect props. This keeps the diagram component focused on visualization.
Dev test page (/dev/diagram): In sub-project 1, the test page renders the ProcessDiagram with a simple stub side panel that shows the selected node's ID, type, label, and any nodeConfigs entry. This validates the selection interaction without needing full page integration.
8. Non-Goals (Sub-project 2 & 3)
These are explicitly out of scope for sub-project 1:
- Execution overlay rendering — animated flow, per-node status/duration, dimming non-executed nodes
- Loop/split iteration stepping — "debugger" UI with iteration tabs
- Page integration — replacing RouteFlow on RouteDetail, ExchangeDetail, Dashboard
- Minimap — small overview for large diagrams (stretch goal, not v1)
- Drag to rearrange — nodes are server-positioned, not user-movable
Verification
- Backend:
mvn clean verify -DskipITspasses after direction param addition - Frontend types:
npx tsc -p tsconfig.app.json --noEmitpasses - Manual test: Create a temporary test page or Storybook-like route (
/dev/diagram) that renders the ProcessDiagram component with a known route - Zoom/pan: Mouse wheel zooms, drag pans, fit-to-view works
- Node interaction: Hover shows toolbar, click selects with amber ring
- Config badges: Pass mock
nodeConfigsand verify TRACE/TAP pills render - Error sections: Route with ON_EXCEPTION renders error handler below main flow
- Compound nodes: Route with CHOICE renders children inside dashed container
- Keyboard (required): Escape deselects, +/- zooms, 0 fits to view
- Direction:
?direction=TBrenders top-to-bottom layout