feat: add interactive ProcessDiagram SVG component (sub-project 1/3)
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>
This commit is contained in:
115
ui/src/components/ProcessDiagram/useDiagramData.ts
Normal file
115
ui/src/components/ProcessDiagram/useDiagramData.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
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';
|
||||
|
||||
const SECTION_GAP = 40;
|
||||
|
||||
export function useDiagramData(
|
||||
application: string,
|
||||
routeId: string,
|
||||
direction: 'LR' | 'TB' = 'LR',
|
||||
) {
|
||||
const { data: layout, isLoading, error } = useDiagramByRoute(application, routeId, direction);
|
||||
|
||||
const result = useMemo(() => {
|
||||
if (!layout?.nodes) {
|
||||
return { sections: [], totalWidth: 0, totalHeight: 0 };
|
||||
}
|
||||
|
||||
const allEdges = layout.edges ?? [];
|
||||
|
||||
// Separate main nodes from error handler compound sections
|
||||
const mainNodes: DiagramNode[] = [];
|
||||
const errorSections: { label: string; nodes: DiagramNode[] }[] = [];
|
||||
|
||||
for (const node of layout.nodes) {
|
||||
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),
|
||||
);
|
||||
|
||||
// Compute main section bounding box
|
||||
const mainBounds = computeBounds(mainNodes);
|
||||
|
||||
const sections: DiagramSection[] = [
|
||||
{
|
||||
label: 'Main Route',
|
||||
nodes: mainNodes,
|
||||
edges: mainEdges,
|
||||
offsetY: 0,
|
||||
},
|
||||
];
|
||||
|
||||
let currentY = mainBounds.maxY + SECTION_GAP;
|
||||
|
||||
for (const es of errorSections) {
|
||||
const errorNodeIds = new Set<string>();
|
||||
collectNodeIds(es.nodes, errorNodeIds);
|
||||
const errorEdges = allEdges.filter(
|
||||
e => errorNodeIds.has(e.sourceId) && errorNodeIds.has(e.targetId),
|
||||
);
|
||||
|
||||
sections.push({
|
||||
label: es.label,
|
||||
nodes: es.nodes,
|
||||
edges: errorEdges,
|
||||
offsetY: currentY,
|
||||
variant: 'error',
|
||||
});
|
||||
|
||||
const errorBounds = computeBounds(es.nodes);
|
||||
currentY += (errorBounds.maxY - errorBounds.minY) + SECTION_GAP;
|
||||
}
|
||||
|
||||
const totalWidth = layout.width ?? mainBounds.maxX;
|
||||
const totalHeight = currentY;
|
||||
|
||||
return { sections, totalWidth, totalHeight };
|
||||
}, [layout]);
|
||||
|
||||
return { ...result, isLoading, error };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user