From f6220a9f89034cad5e367ad353399436f0c654e9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:45:10 +0100 Subject: [PATCH] feat: support ON_COMPLETION handler sections in diagram Add ON_COMPLETION to backend COMPOUND_TYPES and frontend rendering. Completion handlers render as teal-tinted sections between the main flow and error handlers, structurally parallel to onException. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/diagram/ElkDiagramRenderer.java | 3 +- .../ProcessDiagram/ErrorSection.tsx | 12 ++- .../components/ProcessDiagram/node-colors.ts | 12 +++ ui/src/components/ProcessDiagram/types.ts | 2 +- .../ProcessDiagram/useDiagramData.ts | 78 ++++++++++--------- 5 files changed, 67 insertions(+), 40 deletions(-) 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 3f5de05e..25234df2 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 @@ -102,7 +102,8 @@ public class ElkDiagramRenderer implements DiagramRenderer { NodeType.EIP_SPLIT, NodeType.TRY_CATCH, NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY, NodeType.EIP_LOOP, NodeType.EIP_MULTICAST, - NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER + NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, + NodeType.ON_COMPLETION ); public ElkDiagramRenderer() { diff --git a/ui/src/components/ProcessDiagram/ErrorSection.tsx b/ui/src/components/ProcessDiagram/ErrorSection.tsx index fe3bfdf1..b36618aa 100644 --- a/ui/src/components/ProcessDiagram/ErrorSection.tsx +++ b/ui/src/components/ProcessDiagram/ErrorSection.tsx @@ -21,10 +21,16 @@ interface ErrorSectionProps { onNodeLeave: () => void; } +const VARIANT_COLORS: Record = { + error: '#C0392B', + completion: '#1A7F8E', +}; + export function ErrorSection({ section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, onNodeClick, onNodeEnter, onNodeLeave, }: ErrorSectionProps) { + const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error; const boxHeight = useMemo(() => { let maxY = 0; for (const n of section.nodes) { @@ -44,7 +50,7 @@ export function ErrorSection({ return ( {/* Section label */} - + {section.label} @@ -54,7 +60,7 @@ export function ErrorSection({ y1={0} x2={totalWidth} y2={0} - stroke="#C0392B" + stroke={color} strokeWidth={1} strokeDasharray="6 3" opacity={0.5} @@ -66,7 +72,7 @@ export function ErrorSection({ y={4} width={totalWidth} height={boxHeight} - fill="#C0392B" + fill={color} opacity={0.03} rx={4} /> diff --git a/ui/src/components/ProcessDiagram/node-colors.ts b/ui/src/components/ProcessDiagram/node-colors.ts index bebbd028..d62d0c47 100644 --- a/ui/src/components/ProcessDiagram/node-colors.ts +++ b/ui/src/components/ProcessDiagram/node-colors.ts @@ -50,6 +50,8 @@ const TYPE_MAP: Record = { DO_CATCH: ERROR_COLOR, DO_FINALLY: ERROR_COLOR, + ON_COMPLETION: '#1A7F8E', // --running (teal, lifecycle handler) + EIP_WIRE_TAP: CROSS_ROUTE_COLOR, EIP_ENRICH: CROSS_ROUTE_COLOR, EIP_POLL_ENRICH: CROSS_ROUTE_COLOR, @@ -61,12 +63,17 @@ const COMPOUND_TYPES = new Set([ 'DO_TRY', 'DO_CATCH', 'DO_FINALLY', 'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE', 'ON_EXCEPTION', 'ERROR_HANDLER', + 'ON_COMPLETION', ]); const ERROR_COMPOUND_TYPES = new Set([ 'ON_EXCEPTION', 'ERROR_HANDLER', ]); +const COMPLETION_COMPOUND_TYPES = new Set([ + 'ON_COMPLETION', +]); + export function colorForType(type: string | undefined): string { if (!type) return DEFAULT_COLOR; return TYPE_MAP[type] ?? DEFAULT_COLOR; @@ -80,6 +87,10 @@ export function isErrorCompoundType(type: string | undefined): boolean { return !!type && ERROR_COMPOUND_TYPES.has(type); } +export function isCompletionCompoundType(type: string | undefined): boolean { + return !!type && COMPLETION_COMPOUND_TYPES.has(type); +} + /** Icon character for a node type */ export function iconForType(type: string | undefined): string { if (!type) return '\u2699'; // gear @@ -87,6 +98,7 @@ export function iconForType(type: string | undefined): string { if (t === 'ENDPOINT') return '\u25B6'; // play if (t === 'TO' || t === 'TO_DYNAMIC' || t === 'DIRECT' || t === 'SEDA') return '\u25A0'; // square if (t.startsWith('EIP_CHOICE') || t === 'EIP_WHEN' || t === 'EIP_OTHERWISE') return '\u25C6'; // diamond + if (t === 'ON_COMPLETION') return '\u2714'; // checkmark if (t === 'ON_EXCEPTION' || t === 'ERROR_HANDLER' || t.startsWith('TRY') || t.startsWith('DO_')) return '\u26A0'; // warning if (t === 'EIP_SPLIT' || t === 'EIP_MULTICAST') return '\u2442'; // fork if (t === 'EIP_LOOP') return '\u21BA'; // loop arrow diff --git a/ui/src/components/ProcessDiagram/types.ts b/ui/src/components/ProcessDiagram/types.ts index 5ab1e32f..dc4ac79a 100644 --- a/ui/src/components/ProcessDiagram/types.ts +++ b/ui/src/components/ProcessDiagram/types.ts @@ -12,7 +12,7 @@ export interface DiagramSection { nodes: DiagramNode[]; edges: DiagramEdge[]; offsetY: number; - variant?: 'error'; + variant?: 'error' | 'completion'; } export interface ProcessDiagramProps { diff --git a/ui/src/components/ProcessDiagram/useDiagramData.ts b/ui/src/components/ProcessDiagram/useDiagramData.ts index 0abe2721..674a801f 100644 --- a/ui/src/components/ProcessDiagram/useDiagramData.ts +++ b/ui/src/components/ProcessDiagram/useDiagramData.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import type { DiagramNode, DiagramEdge } from '../../api/queries/diagrams'; import type { DiagramSection } from './types'; -import { isErrorCompoundType } from './node-colors'; +import { isErrorCompoundType, isCompletionCompoundType } from './node-colors'; const SECTION_GAP = 40; @@ -20,12 +20,18 @@ export function useDiagramData( const allEdges = layout.edges ?? []; - // Separate main nodes from error handler compound sections + // Separate main nodes from completion and error handler compound sections const mainNodes: DiagramNode[] = []; + const completionSections: { label: string; nodes: DiagramNode[] }[] = []; const errorSections: { label: string; nodes: DiagramNode[] }[] = []; for (const node of layout.nodes) { - if (isErrorCompoundType(node.type) && node.children && node.children.length > 0) { + if (isCompletionCompoundType(node.type) && node.children && node.children.length > 0) { + completionSections.push({ + label: node.label || 'onCompletion', + nodes: node.children, + }); + } else if (isErrorCompoundType(node.type) && node.children && node.children.length > 0) { errorSections.push({ label: node.label || 'Error Handler', nodes: node.children, @@ -58,39 +64,41 @@ export function useDiagramData( let currentY = mainBounds.maxY + SECTION_GAP; let maxWidth = mainBounds.maxX; - for (const es of errorSections) { - const errorBounds = computeBounds(es.nodes); - const offX = errorBounds.minX; - const offY = errorBounds.minY; + const addHandlerSections = ( + handlers: { label: string; nodes: DiagramNode[] }[], + variant: 'completion' | 'error', + ) => { + for (const hs of handlers) { + const bounds = computeBounds(hs.nodes); + const offX = bounds.minX; + const offY = bounds.minY; + const shiftedNodes = shiftNodes(hs.nodes, offX, offY); + const nodeIds = new Set(); + collectNodeIds(hs.nodes, nodeIds); + const edges = allEdges + .filter(e => nodeIds.has(e.sourceId) && nodeIds.has(e.targetId)) + .map(e => ({ + ...e, + points: e.points.map(p => [p[0] - offX, p[1] - offY]), + })); + const sectionHeight = bounds.maxY - bounds.minY; + const sectionWidth = bounds.maxX - bounds.minX; + sections.push({ + label: hs.label, + nodes: shiftedNodes, + edges, + offsetY: currentY, + variant, + }); + currentY += sectionHeight + SECTION_GAP; + if (sectionWidth > maxWidth) maxWidth = sectionWidth; + } + }; - // Normalize node coordinates relative to the section's own origin - const shiftedNodes = shiftNodes(es.nodes, offX, offY); - - const errorNodeIds = new Set(); - collectNodeIds(es.nodes, errorNodeIds); - - // Shift edge points too - const errorEdges = allEdges - .filter(e => errorNodeIds.has(e.sourceId) && errorNodeIds.has(e.targetId)) - .map(e => ({ - ...e, - points: e.points.map(p => [p[0] - offX, p[1] - offY]), - })); - - const sectionHeight = errorBounds.maxY - errorBounds.minY; - const sectionWidth = errorBounds.maxX - errorBounds.minX; - - sections.push({ - label: es.label, - nodes: shiftedNodes, - edges: errorEdges, - offsetY: currentY, - variant: 'error', - }); - - currentY += sectionHeight + SECTION_GAP; - if (sectionWidth > maxWidth) maxWidth = sectionWidth; - } + // Completion handlers first (above error handlers) + addHandlerSections(completionSections, 'completion'); + // Then error handlers + addHandlerSections(errorSections, 'error'); const totalWidth = Math.max(layout.width ?? 0, mainBounds.maxX, maxWidth); const totalHeight = currentY;