Files
cameleer-server/ui/src/components/ProcessDiagram/useDiagramData.ts
hsiegeln 990d607d4b
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 49s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
fix: normalize main flow section to (0,0) origin in frontend
The root cause of the Y-offset: ELK places main flow nodes at
arbitrary positions (e.g., y=679) within its root graph, and the
frontend rendered them at those raw positions. Handler sections were
already normalized via shiftNodes, but the main section was not.

Now useDiagramData.ts applies the same normalization to the main
section: computes bounding box, shifts nodes and edges so the section
starts at (0,0). This fixes the Y-offset regardless of what ELK
produces internally.

Removed the backend normalizePositions (was ineffective because handler
nodes at y=12 dominated the global minimum, preventing meaningful shift
of main flow nodes at y=679).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:35 +01:00

170 lines
5.6 KiB
TypeScript

import { useMemo } from 'react';
import { useDiagramByRoute } from '../../api/queries/diagrams';
import type { DiagramNode, DiagramEdge, DiagramLayout } from '../../api/queries/diagrams';
import type { DiagramSection } from './types';
import { isErrorCompoundType, isCompletionCompoundType } from './node-colors';
const SECTION_GAP = 80;
export function useDiagramData(
application: string,
routeId: string,
direction: 'LR' | 'TB' = 'LR',
preloadedLayout?: DiagramLayout,
) {
// When a preloaded layout is provided, disable the internal fetch
const fetchApp = preloadedLayout ? undefined : application;
const fetchRoute = preloadedLayout ? undefined : routeId;
const { data: fetchedLayout, isLoading, error } = useDiagramByRoute(fetchApp, fetchRoute, direction);
const layout = preloadedLayout ?? fetchedLayout;
const result = useMemo(() => {
if (!layout?.nodes) {
return { sections: [], totalWidth: 0, totalHeight: 0 };
}
const allEdges = layout.edges ?? [];
// 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 (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,
});
} else {
mainNodes.push(node);
}
}
// Collect node IDs for edge filtering
const mainNodeIds = new Set<string>();
collectNodeIds(mainNodes, mainNodeIds);
const mainEdges = allEdges.filter(
e => mainNodeIds.has(e.sourceId) && mainNodeIds.has(e.targetId),
);
// Normalize main section to start at (0, 0) — ELK can place nodes
// at arbitrary positions within its root graph
const mainBounds = computeBounds(mainNodes);
const mainOffX = mainBounds.minX;
const mainOffY = mainBounds.minY;
const shiftedMainNodes = shiftNodes(mainNodes, mainOffX, mainOffY);
const shiftedMainEdges = mainEdges.map(e => ({
...e,
points: e.points.map(p => [p[0] - mainOffX, p[1] - mainOffY]),
}));
const mainWidth = mainBounds.maxX - mainBounds.minX;
const mainHeight = mainBounds.maxY - mainBounds.minY;
const sections: DiagramSection[] = [
{
label: 'Main Route',
nodes: shiftedMainNodes,
edges: shiftedMainEdges,
offsetY: 0,
},
];
let currentY = mainHeight + SECTION_GAP;
let maxWidth = mainWidth;
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;
}
};
// Completion handlers first (above error handlers)
addHandlerSections(completionSections, 'completion');
// Then error handlers
addHandlerSections(errorSections, 'error');
const totalWidth = Math.max(layout.width ?? 0, mainWidth, maxWidth);
const totalHeight = currentY;
return { sections, totalWidth, totalHeight };
}, [layout]);
return {
...result,
isLoading: preloadedLayout ? false : isLoading,
error: preloadedLayout ? null : error,
};
}
/** Shift all node coordinates by subtracting an offset, recursively. */
function shiftNodes(nodes: DiagramNode[], offX: number, offY: number): DiagramNode[] {
return nodes.map(n => ({
...n,
x: (n.x ?? 0) - offX,
y: (n.y ?? 0) - offY,
children: n.children ? shiftNodes(n.children, offX, offY) : undefined,
}));
}
function collectNodeIds(nodes: DiagramNode[], set: Set<string>) {
for (const n of nodes) {
if (n.id) set.add(n.id);
if (n.children) collectNodeIds(n.children, set);
}
}
function computeBounds(nodes: DiagramNode[]): {
minX: number; minY: number; maxX: number; maxY: number;
} {
let minX = Infinity, minY = Infinity, maxX = 0, maxY = 0;
for (const n of nodes) {
const x = n.x ?? 0;
const y = n.y ?? 0;
const w = n.width ?? 80;
const h = n.height ?? 40;
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x + w > maxX) maxX = x + w;
if (y + h > maxY) maxY = y + h;
if (n.children) {
const childBounds = computeBounds(n.children);
if (childBounds.maxX > maxX) maxX = childBounds.maxX;
if (childBounds.maxY > maxY) maxY = childBounds.maxY;
}
}
return { minX: minX === Infinity ? 0 : minX, minY: minY === Infinity ? 0 : minY, maxX, maxY };
}