83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
|
|
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||
|
|
import type { NodeConfig } from './types';
|
||
|
|
import { colorForType } from './node-colors';
|
||
|
|
import { DiagramNode } from './DiagramNode';
|
||
|
|
|
||
|
|
const HEADER_HEIGHT = 22;
|
||
|
|
const CORNER_RADIUS = 4;
|
||
|
|
|
||
|
|
interface CompoundNodeProps {
|
||
|
|
node: DiagramNodeType;
|
||
|
|
selectedNodeId?: string;
|
||
|
|
hoveredNodeId: string | null;
|
||
|
|
nodeConfigs?: Map<string, NodeConfig>;
|
||
|
|
onNodeClick: (nodeId: string) => void;
|
||
|
|
onNodeEnter: (nodeId: string) => void;
|
||
|
|
onNodeLeave: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function CompoundNode({
|
||
|
|
node, selectedNodeId, hoveredNodeId, nodeConfigs,
|
||
|
|
onNodeClick, onNodeEnter, onNodeLeave,
|
||
|
|
}: CompoundNodeProps) {
|
||
|
|
const x = node.x ?? 0;
|
||
|
|
const y = node.y ?? 0;
|
||
|
|
const w = node.width ?? 200;
|
||
|
|
const h = node.height ?? 100;
|
||
|
|
const color = colorForType(node.type);
|
||
|
|
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
|
||
|
|
const label = node.label ? `${typeName}: ${node.label}` : typeName;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
|
||
|
|
{/* Container body */}
|
||
|
|
<rect
|
||
|
|
x={0}
|
||
|
|
y={0}
|
||
|
|
width={w}
|
||
|
|
height={h}
|
||
|
|
rx={CORNER_RADIUS}
|
||
|
|
fill="white"
|
||
|
|
stroke={color}
|
||
|
|
strokeWidth={1.5}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Colored header bar */}
|
||
|
|
<rect x={0} y={0} width={w} height={HEADER_HEIGHT} rx={CORNER_RADIUS} fill={color} />
|
||
|
|
<rect x={CORNER_RADIUS} y={CORNER_RADIUS} width={w - CORNER_RADIUS * 2} height={HEADER_HEIGHT - CORNER_RADIUS} fill={color} />
|
||
|
|
|
||
|
|
{/* Header label */}
|
||
|
|
<text
|
||
|
|
x={w / 2}
|
||
|
|
y={HEADER_HEIGHT / 2 + 4}
|
||
|
|
fill="white"
|
||
|
|
fontSize={10}
|
||
|
|
fontWeight={600}
|
||
|
|
textAnchor="middle"
|
||
|
|
>
|
||
|
|
{label}
|
||
|
|
</text>
|
||
|
|
|
||
|
|
{/* Children nodes (positioned relative to compound) */}
|
||
|
|
{node.children?.map(child => (
|
||
|
|
<DiagramNode
|
||
|
|
key={child.id}
|
||
|
|
node={{
|
||
|
|
...child,
|
||
|
|
// Children have absolute coordinates from the backend,
|
||
|
|
// but since we're inside the compound's translate, subtract parent offset
|
||
|
|
x: (child.x ?? 0) - x,
|
||
|
|
y: (child.y ?? 0) - y,
|
||
|
|
}}
|
||
|
|
isHovered={hoveredNodeId === child.id}
|
||
|
|
isSelected={selectedNodeId === child.id}
|
||
|
|
config={child.id ? nodeConfigs?.get(child.id) : undefined}
|
||
|
|
onClick={() => child.id && onNodeClick(child.id)}
|
||
|
|
onMouseEnter={() => child.id && onNodeEnter(child.id)}
|
||
|
|
onMouseLeave={onNodeLeave}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</g>
|
||
|
|
);
|
||
|
|
}
|