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.EIP_SPLIT, NodeType.TRY_CATCH,
|
||||||
NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY,
|
NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY,
|
||||||
NodeType.EIP_LOOP, NodeType.EIP_MULTICAST,
|
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() {
|
public ElkDiagramRenderer() {
|
||||||
|
|||||||
@@ -21,10 +21,16 @@ interface ErrorSectionProps {
|
|||||||
onNodeLeave: () => void;
|
onNodeLeave: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VARIANT_COLORS: Record<string, string> = {
|
||||||
|
error: '#C0392B',
|
||||||
|
completion: '#1A7F8E',
|
||||||
|
};
|
||||||
|
|
||||||
export function ErrorSection({
|
export function ErrorSection({
|
||||||
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs,
|
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs,
|
||||||
onNodeClick, onNodeEnter, onNodeLeave,
|
onNodeClick, onNodeEnter, onNodeLeave,
|
||||||
}: ErrorSectionProps) {
|
}: ErrorSectionProps) {
|
||||||
|
const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error;
|
||||||
const boxHeight = useMemo(() => {
|
const boxHeight = useMemo(() => {
|
||||||
let maxY = 0;
|
let maxY = 0;
|
||||||
for (const n of section.nodes) {
|
for (const n of section.nodes) {
|
||||||
@@ -44,7 +50,7 @@ export function ErrorSection({
|
|||||||
return (
|
return (
|
||||||
<g transform={`translate(0, ${section.offsetY})`}>
|
<g transform={`translate(0, ${section.offsetY})`}>
|
||||||
{/* Section label */}
|
{/* 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}
|
{section.label}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
@@ -54,7 +60,7 @@ export function ErrorSection({
|
|||||||
y1={0}
|
y1={0}
|
||||||
x2={totalWidth}
|
x2={totalWidth}
|
||||||
y2={0}
|
y2={0}
|
||||||
stroke="#C0392B"
|
stroke={color}
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
strokeDasharray="6 3"
|
strokeDasharray="6 3"
|
||||||
opacity={0.5}
|
opacity={0.5}
|
||||||
@@ -66,7 +72,7 @@ export function ErrorSection({
|
|||||||
y={4}
|
y={4}
|
||||||
width={totalWidth}
|
width={totalWidth}
|
||||||
height={boxHeight}
|
height={boxHeight}
|
||||||
fill="#C0392B"
|
fill={color}
|
||||||
opacity={0.03}
|
opacity={0.03}
|
||||||
rx={4}
|
rx={4}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ const TYPE_MAP: Record<string, string> = {
|
|||||||
DO_CATCH: ERROR_COLOR,
|
DO_CATCH: ERROR_COLOR,
|
||||||
DO_FINALLY: ERROR_COLOR,
|
DO_FINALLY: ERROR_COLOR,
|
||||||
|
|
||||||
|
ON_COMPLETION: '#1A7F8E', // --running (teal, lifecycle handler)
|
||||||
|
|
||||||
EIP_WIRE_TAP: CROSS_ROUTE_COLOR,
|
EIP_WIRE_TAP: CROSS_ROUTE_COLOR,
|
||||||
EIP_ENRICH: CROSS_ROUTE_COLOR,
|
EIP_ENRICH: CROSS_ROUTE_COLOR,
|
||||||
EIP_POLL_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',
|
'DO_TRY', 'DO_CATCH', 'DO_FINALLY',
|
||||||
'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE',
|
'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE',
|
||||||
'ON_EXCEPTION', 'ERROR_HANDLER',
|
'ON_EXCEPTION', 'ERROR_HANDLER',
|
||||||
|
'ON_COMPLETION',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ERROR_COMPOUND_TYPES = new Set([
|
const ERROR_COMPOUND_TYPES = new Set([
|
||||||
'ON_EXCEPTION', 'ERROR_HANDLER',
|
'ON_EXCEPTION', 'ERROR_HANDLER',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const COMPLETION_COMPOUND_TYPES = new Set([
|
||||||
|
'ON_COMPLETION',
|
||||||
|
]);
|
||||||
|
|
||||||
export function colorForType(type: string | undefined): string {
|
export function colorForType(type: string | undefined): string {
|
||||||
if (!type) return DEFAULT_COLOR;
|
if (!type) return DEFAULT_COLOR;
|
||||||
return TYPE_MAP[type] ?? 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);
|
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 */
|
/** Icon character for a node type */
|
||||||
export function iconForType(type: string | undefined): string {
|
export function iconForType(type: string | undefined): string {
|
||||||
if (!type) return '\u2699'; // gear
|
if (!type) return '\u2699'; // gear
|
||||||
@@ -87,6 +98,7 @@ export function iconForType(type: string | undefined): string {
|
|||||||
if (t === 'ENDPOINT') return '\u25B6'; // play
|
if (t === 'ENDPOINT') return '\u25B6'; // play
|
||||||
if (t === 'TO' || t === 'TO_DYNAMIC' || t === 'DIRECT' || t === 'SEDA') return '\u25A0'; // square
|
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.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 === '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_SPLIT' || t === 'EIP_MULTICAST') return '\u2442'; // fork
|
||||||
if (t === 'EIP_LOOP') return '\u21BA'; // loop arrow
|
if (t === 'EIP_LOOP') return '\u21BA'; // loop arrow
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface DiagramSection {
|
|||||||
nodes: DiagramNode[];
|
nodes: DiagramNode[];
|
||||||
edges: DiagramEdge[];
|
edges: DiagramEdge[];
|
||||||
offsetY: number;
|
offsetY: number;
|
||||||
variant?: 'error';
|
variant?: 'error' | 'completion';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProcessDiagramProps {
|
export interface ProcessDiagramProps {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
||||||
import type { DiagramNode, DiagramEdge } from '../../api/queries/diagrams';
|
import type { DiagramNode, DiagramEdge } from '../../api/queries/diagrams';
|
||||||
import type { DiagramSection } from './types';
|
import type { DiagramSection } from './types';
|
||||||
import { isErrorCompoundType } from './node-colors';
|
import { isErrorCompoundType, isCompletionCompoundType } from './node-colors';
|
||||||
|
|
||||||
const SECTION_GAP = 40;
|
const SECTION_GAP = 40;
|
||||||
|
|
||||||
@@ -20,12 +20,18 @@ export function useDiagramData(
|
|||||||
|
|
||||||
const allEdges = layout.edges ?? [];
|
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 mainNodes: DiagramNode[] = [];
|
||||||
|
const completionSections: { label: string; nodes: DiagramNode[] }[] = [];
|
||||||
const errorSections: { label: string; nodes: DiagramNode[] }[] = [];
|
const errorSections: { label: string; nodes: DiagramNode[] }[] = [];
|
||||||
|
|
||||||
for (const node of layout.nodes) {
|
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({
|
errorSections.push({
|
||||||
label: node.label || 'Error Handler',
|
label: node.label || 'Error Handler',
|
||||||
nodes: node.children,
|
nodes: node.children,
|
||||||
@@ -58,39 +64,41 @@ export function useDiagramData(
|
|||||||
let currentY = mainBounds.maxY + SECTION_GAP;
|
let currentY = mainBounds.maxY + SECTION_GAP;
|
||||||
let maxWidth = mainBounds.maxX;
|
let maxWidth = mainBounds.maxX;
|
||||||
|
|
||||||
for (const es of errorSections) {
|
const addHandlerSections = (
|
||||||
const errorBounds = computeBounds(es.nodes);
|
handlers: { label: string; nodes: DiagramNode[] }[],
|
||||||
const offX = errorBounds.minX;
|
variant: 'completion' | 'error',
|
||||||
const offY = errorBounds.minY;
|
) => {
|
||||||
|
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
|
// Completion handlers first (above error handlers)
|
||||||
const shiftedNodes = shiftNodes(es.nodes, offX, offY);
|
addHandlerSections(completionSections, 'completion');
|
||||||
|
// Then error handlers
|
||||||
const errorNodeIds = new Set<string>();
|
addHandlerSections(errorSections, 'error');
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalWidth = Math.max(layout.width ?? 0, mainBounds.maxX, maxWidth);
|
const totalWidth = Math.max(layout.width ?? 0, mainBounds.maxX, maxWidth);
|
||||||
const totalHeight = currentY;
|
const totalHeight = currentY;
|
||||||
|
|||||||
Reference in New Issue
Block a user