Construyendo un Editor Visual con React Flow
Un vistazo técnico a cómo construimos nuestro editor drag-and-drop, los desafíos de la customización de nodos y la lógica de conexiones.
Luis Chen
CPO & Co-founder
El Corazón de InfraUX: Nuestro Editor Visual
Si hay una característica que define a InfraUX, es nuestro editor visual. Construir un editor drag-and-drop que sea intuitivo, performante y poderoso ha sido uno de nuestros mayores desafíos técnicos. Esta es la historia de cómo lo logramos.
¿Por Qué React Flow?
Evaluamos múltiples librerías para construir nuestro editor:
Librería | Pros | Contras | Decisión |
---|---|---|---|
D3.js | Máximo control | Requiere construir todo desde cero | ❌ |
Cytoscape.js | Potente para grafos | Orientado a grafos científicos | ❌ |
JointJS | Buenas características | Licencia comercial costosa | ❌ |
React Flow | Moderno, bien mantenido | Curva de aprendizaje | ✅ |
React Flow ganó por su balance entre flexibilidad y facilidad de uso, además de una comunidad activa y excelente documentación.
Arquitectura del Editor
Nuestro editor está compuesto por varias capas:
1// components/Editor/index.tsx
2import ReactFlow, {
3 Background,
4 Controls,
5 MiniMap,
6 ReactFlowProvider
7} from 'reactflow';
8
9export function InfraUXEditor() {
10 return (
11 <ReactFlowProvider>
12 <div className="h-full w-full">
13 <ReactFlow
14 nodes={nodes}
15 edges={edges}
16 onNodesChange={onNodesChange}
17 onEdgesChange={onEdgesChange}
18 onConnect={onConnect}
19 nodeTypes={nodeTypes}
20 edgeTypes={edgeTypes}
21 connectionLineComponent={CustomConnectionLine}
22 onDrop={onDrop}
23 onDragOver={onDragOver}
24 snapToGrid
25 snapGrid={[15, 15]}
26 >
27 <Background variant="dots" gap={12} size={1} />
28 <Controls />
29 <MiniMap />
30 <Panel position="top-left">
31 <Toolbar />
32 </Panel>
33 </ReactFlow>
34 </div>
35 </ReactFlowProvider>
36 );
37}
Customización de Nodos
Cada servicio cloud tiene su propio nodo customizado con propiedades específicas:
1// nodes/EC2Node.tsx
2export function EC2Node({ data, selected }: NodeProps) {
3 const [isEditing, setIsEditing] = useState(false);
4
5 return (
6 <div className={`node-wrapper ${selected ? 'selected' : ''}`}>
7 <Handle type="target" position={Position.Top} />
8
9 <div className="node-header">
10 <EC2Icon className="w-6 h-6" />
11 <span>EC2 Instance</span>
12 </div>
13
14 <div className="node-body">
15 {isEditing ? (
16 <InstanceTypeSelector
17 value={data.instanceType}
18 onChange={(type) => updateNodeData({ instanceType: type })}
19 />
20 ) : (
21 <div onClick={() => setIsEditing(true)}>
22 {data.instanceType || 't3.micro'}
23 </div>
24 )}
25
26 <div className="node-stats">
27 <span>{data.vcpus || 2} vCPUs</span>
28 <span>{data.memory || 1} GB RAM</span>
29 </div>
30 </div>
31
32 <Handle type="source" position={Position.Bottom} />
33 </div>
34 );
35}
36
37// Registro de tipos de nodos
38const nodeTypes = {
39 ec2Instance: EC2Node,
40 rdsDatabase: RDSNode,
41 s3Bucket: S3Node,
42 lambda: LambdaNode,
43 vpc: VPCNode,
44 // ... más de 50 tipos de nodos
45};
Sistema de Drag & Drop
Implementamos un sistema de drag & drop intuitivo desde la paleta de componentes:
1// components/ComponentPalette.tsx
2export function ComponentPalette() {
3 const onDragStart = (event: DragEvent, nodeType: string) => {
4 event.dataTransfer.setData('application/reactflow', nodeType);
5 event.dataTransfer.effectAllowed = 'move';
6 };
7
8 return (
9 <div className="component-palette">
10 {Object.entries(AWS_SERVICES).map(([key, service]) => (
11 <div
12 key={key}
13 className="palette-item"
14 draggable
15 onDragStart={(e) => onDragStart(e, service.nodeType)}
16 >
17 <service.icon className="w-8 h-8" />
18 <span>{service.name}</span>
19 </div>
20 ))}
21 </div>
22 );
23}
Validación de Conexiones en Tiempo Real
No todas las conexiones son válidas. Implementamos reglas complejas para validar conexiones:
1// utils/connectionRules.ts
2export const connectionRules = {
3 ec2Instance: {
4 canConnectTo: ['securityGroup', 'elasticIp', 'loadBalancer', 'vpc'],
5 canReceiveFrom: ['loadBalancer', 'autoScalingGroup'],
6 },
7 rdsDatabase: {
8 canConnectTo: ['securityGroup', 'vpc', 'subnetGroup'],
9 canReceiveFrom: ['ec2Instance', 'lambda'],
10 validation: (source, target) => {
11 // RDS debe estar en la misma VPC que EC2
12 return source.data.vpcId === target.data.vpcId;
13 }
14 },
15 // ... más reglas
16};
17
18// Hook para validar conexiones
19const isValidConnection = useCallback((connection: Connection) => {
20 const sourceNode = nodes.find(n => n.id === connection.source);
21 const targetNode = nodes.find(n => n.id === connection.target);
22
23 if (!sourceNode || !targetNode) return false;
24
25 const rules = connectionRules[sourceNode.type];
26 if (!rules) return false;
27
28 // Verificar si el tipo de target es permitido
29 if (!rules.canConnectTo.includes(targetNode.type)) {
30 showError(`${sourceNode.type} no puede conectarse a ${targetNode.type}`);
31 return false;
32 }
33
34 // Validación adicional si existe
35 if (rules.validation) {
36 return rules.validation(sourceNode, targetNode);
37 }
38
39 return true;
40}, [nodes]);
Optimizaciones de Performance
Con diagramas complejos (100+ nodos), el performance es crítico:
1. Virtualización de Nodos
1// Solo renderizar nodos visibles
2const visibleNodes = useMemo(() => {
3 return nodes.filter(node => {
4 const { x, y } = node.position;
5 return (
6 x > viewport.x - 200 &&
7 x < viewport.x + viewport.width + 200 &&
8 y > viewport.y - 200 &&
9 y < viewport.y + viewport.height + 200
10 );
11 });
12}, [nodes, viewport]);
2. Debouncing de Updates
1// Debounce actualizaciones para evitar re-renders excesivos
2const debouncedUpdateNode = useMemo(
3 () => debounce((nodeId: string, data: any) => {
4 setNodes((nds) =>
5 nds.map((node) =>
6 node.id === nodeId ? { ...node, data: { ...node.data, ...data } } : node
7 )
8 );
9 }, 100),
10 []
11);
3. Memoización Agresiva
1// Memoizar componentes pesados
2const MemoizedNode = memo(({ data, selected }) => {
3 return <CustomNode data={data} selected={selected} />;
4}, (prevProps, nextProps) => {
5 // Solo re-render si cambian propiedades relevantes
6 return (
7 prevProps.selected === nextProps.selected &&
8 prevProps.data.lastModified === nextProps.data.lastModified
9 );
10});
Features Avanzadas
1. Auto-Layout
Implementamos auto-layout usando dagre para organizar automáticamente los nodos:
1import dagre from 'dagre';
2
3export function autoLayout(nodes: Node[], edges: Edge[]) {
4 const dagreGraph = new dagre.graphlib.Graph();
5 dagreGraph.setDefaultEdgeLabel(() => ({}));
6 dagreGraph.setGraph({ rankdir: 'TB', nodesep: 100, ranksep: 100 });
7
8 nodes.forEach((node) => {
9 dagreGraph.setNode(node.id, { width: 180, height: 100 });
10 });
11
12 edges.forEach((edge) => {
13 dagreGraph.setEdge(edge.source, edge.target);
14 });
15
16 dagre.layout(dagreGraph);
17
18 return nodes.map((node) => {
19 const nodeWithPosition = dagreGraph.node(node.id);
20 return {
21 ...node,
22 position: {
23 x: nodeWithPosition.x - 90,
24 y: nodeWithPosition.y - 50,
25 },
26 };
27 });
28}
2. Undo/Redo
Sistema completo de undo/redo para todas las acciones:
1const { undo, redo, canUndo, canRedo } = useUndoRedo({
2 nodes,
3 edges,
4 maxHistorySize: 50,
5});
6
7// Keyboard shortcuts
8useHotkeys('cmd+z', () => canUndo && undo());
9useHotkeys('cmd+shift+z', () => canRedo && redo());
Lecciones Aprendidas
<div class="info-box"> 💡 **La UX es todo:** Invertimos mucho tiempo en hacer el editor intuitivo. Pequeños detalles como el snap-to-grid y el feedback visual hacen una gran diferencia. </div> <div class="warning-box"> ⚠️ **Performance desde el día 1:** Es más fácil optimizar desde el inicio que después. Considera virtualización y memoización desde el principio. </div> <div class="success-box"> ✅ **Feedback visual constante:** Los usuarios necesitan saber qué está pasando siempre. Indicadores de estado, tooltips y animaciones suaves son esenciales. </div>Métricas de Performance
Métrica | Valor | Objetivo |
---|---|---|
Tiempo de renderizado inicial | 120ms | < 200ms |
FPS durante drag | 58 fps | > 30 fps |
Memoria con 100 nodos | 45MB | < 100MB |
Tiempo de auto-layout | 230ms | < 500ms |
El Futuro del Editor
Estamos trabajando en features aún más avanzadas:
- AI-Assisted Design: Sugerencias inteligentes mientras diseñas
- 3D View: Visualización tridimensional de la arquitectura
- Time Travel: Ver cómo evolucionó el diagrama en el tiempo
- Smart Templates: Templates que se adaptan a tus necesidades
Construir este editor ha sido un viaje increíble. Cada día aprendemos algo nuevo de nuestros usuarios y mejoramos la experiencia. El editor visual no es solo una feature de InfraUX, es la esencia de lo que somos: hacer la infraestructura visual, intuitiva y colaborativa.