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) <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -21,10 +21,16 @@ interface ErrorSectionProps {
|
||||
onNodeLeave: () => void;
|
||||
}
|
||||
|
||||
const VARIANT_COLORS: Record<string, string> = {
|
||||
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 (
|
||||
<g transform={`translate(0, ${section.offsetY})`}>
|
||||
{/* Section label */}
|
||||
<text x={8} y={-6} fill="#C0392B" fontSize={11} fontWeight={600}>
|
||||
<text x={8} y={-6} fill={color} fontSize={11} fontWeight={600}>
|
||||
{section.label}
|
||||
</text>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -50,6 +50,8 @@ const TYPE_MAP: Record<string, string> = {
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface DiagramSection {
|
||||
nodes: DiagramNode[];
|
||||
edges: DiagramEdge[];
|
||||
offsetY: number;
|
||||
variant?: 'error';
|
||||
variant?: 'error' | 'completion';
|
||||
}
|
||||
|
||||
export interface ProcessDiagramProps {
|
||||
|
||||
@@ -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<string>();
|
||||
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<string>();
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user