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>
2026-03-27 13:55:29 +01:00
|
|
|
import { useCallback, useEffect } from 'react';
|
|
|
|
|
import type { ProcessDiagramProps } from './types';
|
|
|
|
|
import { useDiagramData } from './useDiagramData';
|
|
|
|
|
import { useZoomPan } from './useZoomPan';
|
|
|
|
|
import { useToolbarHover, NodeToolbar } from './NodeToolbar';
|
|
|
|
|
import { DiagramNode } from './DiagramNode';
|
|
|
|
|
import { DiagramEdge } from './DiagramEdge';
|
|
|
|
|
import { CompoundNode } from './CompoundNode';
|
|
|
|
|
import { ErrorSection } from './ErrorSection';
|
|
|
|
|
import { ZoomControls } from './ZoomControls';
|
|
|
|
|
import { isCompoundType } from './node-colors';
|
|
|
|
|
import styles from './ProcessDiagram.module.css';
|
|
|
|
|
|
|
|
|
|
const PADDING = 40;
|
|
|
|
|
|
|
|
|
|
export function ProcessDiagram({
|
|
|
|
|
application,
|
|
|
|
|
routeId,
|
|
|
|
|
direction = 'LR',
|
|
|
|
|
selectedNodeId,
|
|
|
|
|
onNodeSelect,
|
|
|
|
|
onNodeAction,
|
|
|
|
|
nodeConfigs,
|
|
|
|
|
className,
|
|
|
|
|
}: ProcessDiagramProps) {
|
|
|
|
|
const { sections, totalWidth, totalHeight, isLoading, error } = useDiagramData(
|
|
|
|
|
application, routeId, direction,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const zoom = useZoomPan();
|
|
|
|
|
const toolbar = useToolbarHover();
|
|
|
|
|
|
|
|
|
|
const contentWidth = totalWidth + PADDING * 2;
|
|
|
|
|
const contentHeight = totalHeight + PADDING * 2;
|
|
|
|
|
|
2026-03-27 16:33:24 +01:00
|
|
|
// Reset to 100% at top-left on first data load
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (totalWidth > 0 && totalHeight > 0) {
|
2026-03-27 16:33:24 +01:00
|
|
|
zoom.resetView();
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
}
|
|
|
|
|
}, [totalWidth, totalHeight]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
const handleNodeClick = useCallback(
|
|
|
|
|
(nodeId: string) => { onNodeSelect?.(nodeId); },
|
|
|
|
|
[onNodeSelect],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleNodeAction = useCallback(
|
|
|
|
|
(nodeId: string, action: import('./types').NodeAction) => {
|
|
|
|
|
if (action === 'inspect') onNodeSelect?.(nodeId);
|
|
|
|
|
onNodeAction?.(nodeId, action);
|
|
|
|
|
},
|
|
|
|
|
[onNodeSelect, onNodeAction],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = useCallback(
|
|
|
|
|
(e: React.KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
onNodeSelect?.('');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
zoom.onKeyDown(e, contentWidth, contentHeight);
|
|
|
|
|
},
|
|
|
|
|
[onNodeSelect, zoom, contentWidth, contentHeight],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className={`${styles.container} ${className ?? ''}`}>
|
|
|
|
|
<div className={styles.loading}>Loading diagram...</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className={`${styles.container} ${className ?? ''}`}>
|
|
|
|
|
<div className={styles.error}>Failed to load diagram</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sections.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className={`${styles.container} ${className ?? ''}`}>
|
|
|
|
|
<div className={styles.loading}>No diagram data available</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mainSection = sections[0];
|
|
|
|
|
const errorSections = sections.slice(1);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={zoom.containerRef}
|
|
|
|
|
className={`${styles.container} ${className ?? ''}`}
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
className={styles.svg}
|
|
|
|
|
onWheel={zoom.onWheel}
|
|
|
|
|
onPointerDown={zoom.onPointerDown}
|
|
|
|
|
onPointerMove={zoom.onPointerMove}
|
|
|
|
|
onPointerUp={zoom.onPointerUp}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onClick={() => onNodeSelect?.('')}
|
|
|
|
|
>
|
|
|
|
|
<defs>
|
|
|
|
|
<marker
|
|
|
|
|
id="arrowhead"
|
|
|
|
|
markerWidth="8"
|
|
|
|
|
markerHeight="6"
|
|
|
|
|
refX="7"
|
|
|
|
|
refY="3"
|
|
|
|
|
orient="auto"
|
|
|
|
|
>
|
|
|
|
|
<polygon points="0 0, 8 3, 0 6" fill="#9CA3AF" />
|
|
|
|
|
</marker>
|
|
|
|
|
</defs>
|
|
|
|
|
|
2026-03-27 16:33:24 +01:00
|
|
|
<g style={{ transform: zoom.transform, transformOrigin: '0 0' }}>
|
|
|
|
|
{/* Main section top-level edges (not inside compounds) */}
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
<g className="edges">
|
2026-03-27 16:33:24 +01:00
|
|
|
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => (
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
|
|
|
|
|
))}
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
{/* Main section nodes */}
|
|
|
|
|
<g className="nodes">
|
|
|
|
|
{mainSection.nodes.map(node => {
|
|
|
|
|
if (isCompoundType(node.type) && node.children && node.children.length > 0) {
|
|
|
|
|
return (
|
|
|
|
|
<CompoundNode
|
|
|
|
|
key={node.id}
|
|
|
|
|
node={node}
|
2026-03-27 16:33:24 +01:00
|
|
|
edges={mainSection.edges}
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
selectedNodeId={selectedNodeId}
|
|
|
|
|
hoveredNodeId={toolbar.hoveredNodeId}
|
|
|
|
|
nodeConfigs={nodeConfigs}
|
|
|
|
|
onNodeClick={handleNodeClick}
|
|
|
|
|
onNodeEnter={toolbar.onNodeEnter}
|
|
|
|
|
onNodeLeave={toolbar.onNodeLeave}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<DiagramNode
|
|
|
|
|
key={node.id}
|
|
|
|
|
node={node}
|
|
|
|
|
isHovered={toolbar.hoveredNodeId === node.id}
|
|
|
|
|
isSelected={selectedNodeId === node.id}
|
|
|
|
|
config={node.id ? nodeConfigs?.get(node.id) : undefined}
|
|
|
|
|
onClick={() => node.id && handleNodeClick(node.id)}
|
|
|
|
|
onMouseEnter={() => node.id && toolbar.onNodeEnter(node.id)}
|
|
|
|
|
onMouseLeave={toolbar.onNodeLeave}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</g>
|
|
|
|
|
|
2026-03-27 16:33:24 +01:00
|
|
|
{/* Toolbar rendered as HTML overlay below */}
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
|
|
|
|
|
{/* Error handler sections */}
|
|
|
|
|
{errorSections.map((section, i) => (
|
|
|
|
|
<ErrorSection
|
|
|
|
|
key={`error-${i}`}
|
|
|
|
|
section={section}
|
|
|
|
|
totalWidth={totalWidth}
|
|
|
|
|
selectedNodeId={selectedNodeId}
|
|
|
|
|
hoveredNodeId={toolbar.hoveredNodeId}
|
|
|
|
|
nodeConfigs={nodeConfigs}
|
|
|
|
|
onNodeClick={handleNodeClick}
|
|
|
|
|
onNodeEnter={toolbar.onNodeEnter}
|
|
|
|
|
onNodeLeave={toolbar.onNodeLeave}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</g>
|
|
|
|
|
</svg>
|
|
|
|
|
|
2026-03-27 16:33:24 +01:00
|
|
|
{/* Node toolbar — HTML overlay, fixed size regardless of zoom */}
|
|
|
|
|
{toolbar.hoveredNodeId && onNodeAction && (() => {
|
|
|
|
|
const hNode = findNodeById(sections, toolbar.hoveredNodeId!);
|
|
|
|
|
if (!hNode) return null;
|
|
|
|
|
// Convert SVG coordinates to screen-space using zoom transform
|
|
|
|
|
const nodeCenter = (hNode.x ?? 0) + (hNode.width ?? 160) / 2;
|
|
|
|
|
const nodeTop = hNode.y ?? 0;
|
|
|
|
|
const screenX = nodeCenter * zoom.state.scale + zoom.state.translateX;
|
|
|
|
|
const screenY = nodeTop * zoom.state.scale + zoom.state.translateY;
|
|
|
|
|
return (
|
|
|
|
|
<NodeToolbar
|
|
|
|
|
nodeId={toolbar.hoveredNodeId!}
|
|
|
|
|
screenX={screenX}
|
|
|
|
|
screenY={screenY}
|
|
|
|
|
onAction={handleNodeAction}
|
|
|
|
|
onMouseEnter={toolbar.onToolbarEnter}
|
|
|
|
|
onMouseLeave={toolbar.onToolbarLeave}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
|
|
|
|
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
<ZoomControls
|
|
|
|
|
onZoomIn={zoom.zoomIn}
|
|
|
|
|
onZoomOut={zoom.zoomOut}
|
|
|
|
|
onFitToView={() => zoom.fitToView(contentWidth, contentHeight)}
|
|
|
|
|
scale={zoom.state.scale}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findNodeById(
|
|
|
|
|
sections: import('./types').DiagramSection[],
|
|
|
|
|
nodeId: string,
|
|
|
|
|
): import('../../api/queries/diagrams').DiagramNode | undefined {
|
|
|
|
|
for (const section of sections) {
|
|
|
|
|
for (const node of section.nodes) {
|
|
|
|
|
if (node.id === nodeId) return node;
|
|
|
|
|
if (node.children) {
|
2026-03-27 16:33:24 +01:00
|
|
|
const found = findInChildren(node.children, nodeId);
|
|
|
|
|
if (found) return found;
|
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>
2026-03-27 13:55:29 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-03-27 16:33:24 +01:00
|
|
|
|
|
|
|
|
function findInChildren(
|
|
|
|
|
nodes: import('../../api/queries/diagrams').DiagramNode[],
|
|
|
|
|
nodeId: string,
|
|
|
|
|
): import('../../api/queries/diagrams').DiagramNode | undefined {
|
|
|
|
|
for (const n of nodes) {
|
|
|
|
|
if (n.id === nodeId) return n;
|
|
|
|
|
if (n.children) {
|
|
|
|
|
const found = findInChildren(n.children, nodeId);
|
|
|
|
|
if (found) return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Returns true if the edge connects two top-level nodes (not inside any compound). */
|
|
|
|
|
function topLevelEdge(
|
|
|
|
|
edge: import('../../api/queries/diagrams').DiagramEdge,
|
|
|
|
|
nodes: import('../../api/queries/diagrams').DiagramNode[],
|
|
|
|
|
): boolean {
|
|
|
|
|
// Collect all IDs that are children of compound nodes (at any depth)
|
|
|
|
|
const compoundChildIds = new Set<string>();
|
|
|
|
|
for (const n of nodes) {
|
|
|
|
|
if (n.children && n.children.length > 0) {
|
|
|
|
|
collectDescendantIds(n.children, compoundChildIds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return !compoundChildIds.has(edge.sourceId) && !compoundChildIds.has(edge.targetId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectDescendantIds(
|
|
|
|
|
nodes: import('../../api/queries/diagrams').DiagramNode[],
|
|
|
|
|
set: Set<string>,
|
|
|
|
|
) {
|
|
|
|
|
for (const n of nodes) {
|
|
|
|
|
if (n.id) set.add(n.id);
|
|
|
|
|
if (n.children) collectDescendantIds(n.children, set);
|
|
|
|
|
}
|
|
|
|
|
}
|