No description
Find a file
2026-04-07 15:59:55 +00:00
src mejorar funcionalidad de búsqueda 2026-04-07 12:55:02 -03:00
.gitignore first commit 2026-01-27 14:35:59 -03:00
eslint.config.mjs first commit 2026-01-27 14:35:59 -03:00
jsconfig.json first commit 2026-01-27 14:35:59 -03:00
next.config.mjs first commit 2026-01-27 14:35:59 -03:00
package-lock.json instalacion de react-icons y mejora de interfaz 2026-02-24 23:22:45 -03:00
package.json instalacion de react-icons y mejora de interfaz 2026-02-24 23:22:45 -03:00
README.md Cambio el archivo nodes y edges. 2026-02-08 21:25:09 -03:00

Migración en curso desde el proyecto con cliente pesado hacia arquitectura preparada para consumir datos desde una API, separando responsabilidades. De momento los datos están en formato JSON local en src/data.

El objetivo de esta primera iteración es levantar el proyecto pero adaptado a los nuevos requerimientos.

Stack: Next.js 16 - React 19 - Cytoscape.js

  1. Antes, entendiendo lo que hay  
  2. Template inicial + instalación de dependencias    
  3. Creación de DataService - acceso a datos local    
  4. Creación de useGraphData - lógica de estado  
  5. Testeando consumo de datos locales(temporal)  
  6. Migración de componentes

en un mundo ideal >> *dataservice local -- front funcional -- api real -- solo cambiar dataservice


Antes, entendiendo lo que hay...

Primera aproximación a entender los datos:
Los nodos están en data/nodes.js: representan organismos, áreas y sistemas informáticos
Las relaciones están en data/edges.js: representan relaciones funcionales/de intercambio entre nodos.
Los grupos están en data/groups.js: representan vistas lógicas o recortes temáticos del grafo (no relaciones)

Estructura de un nodo

{
  data: {                    
    id: 'mec',
    label: 'Ministerio de Economía', 
    dependency: 'mec-alt',  
    sublabel: 'Economía'    
  },
  classes: 'org'            
}

id: id único del nodo - se usa en edges.sourse/target o groups.nodeIds
label: nombre - lo que se ve en la ui
dependency: dato opcional - id del nodo padre - es estructura
sublabel: texto adicional para los nodos nivel2 para describir al organismo que pertenecen (opcional)
classes: tipo o clase del nodo
- org: organismo
- nivel1 : subsecretaria
- nivel2: dirección/área
- sistema: sistema informático

Estructura de una arista/edge

{
  data: {
    source: 'mec-info',  // id del nodo origen
    target: 'sulh'       // id del nodo destino
  },
  classes: 'manual'      // tipo de conexionn - opcional
}

source y target: id del nodo origen y id del nodo destino
"el sistema/área con id mec-info se vincula con sulh"
classes: puede ser manual o no ser especificada, asumimos que si no es manual es automática * (ej.: un archivo para se pasa manualmente de uno a otro)*

{ data: { 
	source: 'mec-eclab', 
	target: 'sulh' 
  } 
} 

{ data: { 
	source: 'sulh', 
	target: 'siape' 
  } 
} 

{ data: { 
	source: 'sulh', 
	target: 'sigaf' 
  } 
}
  • Economía Política Laboral usa SULH
  • SULH se relaciona con SIAPE
  • SULH hace algo en SIGAF

En este archivo data/edges.js están las relaciones de Interoperabilidad entre sistemas y están definidas manualmente

{ data: { source: 'mec-info', target: 'sulh' } }

y hay otro tipo de relación:

Relaciones de dependencia jerárquica
ANTES, estas relaciones se generaban automáticamente en un archivo en src/data/index.js (del proyecto original)
Entonces, ahí mismo se hacía el procesamiento de aristas: las dependencyEdges se calculaban en tiempo de importación
Se exportaban de manera centralizada los elementos (nodes y edges) y los grupos

src/data/index.js:

import nodes from './nodes';
import systemEdges from './edges';
import groups from './groups';

const dependencyEdges = nodes
  .filter(node => node.data.dependency)
  .map(node => ({
    data: {
      source: node.data.id,
      target: node.data.dependency
    }
}))

export default { 
  groups,
  elements: { 
    nodes, 
    edges: systemEdges.concat(dependencyEdges)
  }
};
  • lógica que se ejecuta al importar
  • no hay control sobre cuando se procesan

AHORA se generan automáticamente ahora desde el DataService de la misma manera, a partir de los nodos que tienen el atributo dependency:

src/services/DataService.js

    this.dependencyEdges = nodes
      .filter(node => node.data.dependency) //solo nodos con padre
      .map(node => ({
        data: {
          source: node.data.id, //nodo hijo
          target: node.data.dependency //nodo padre
        }
      }));

    /* combinar aristas de sistema (interoperabilidad) con aristas de dependencia, esto se hace una sola vez, no cada vez que pedimos edges */
    this.allEdges = systemEdges.concat(this.dependencyEdges);

Ejemplo:
mec-info ---> mec-alt (Informática depende de Subsecretaría Administrativa)
mec-alt ---> mec (Subsecretaría depende del Ministerio)
Estas representan la estructura organizacional (org ---> nivel1 ---> nivel2)

AHORA: index.js se fue

Entonces, resumiendo:

  • La relación jerárquica se hace con dependency
  • La relación de interoperabilidad se hace con edges
org (mec)
  |──> nivel1 (mec-alt)
        |──> nivel2 (mec-info) ──────> sistema (sulh)
                                           [edge]    

1. Template inicial + instalación de dependencias

Inicialmente tengo un un template emulando la estructura original.

Resumen de lo primero que hice: modifique los archivos .js -> pasan a .jsx , agregando App Router de next.js para adaptar la estructura a Next y sumando services que tiene un dataservice para consumir los datos localmente y mas cosas (solo estructura) para el futuro con el backend + hook para manejar los datos del grafo.

-	src/
+	├── app/ # app router de next.js ppal   
+	│   ├── api/ # endpoints imaginarios    
+	│   │   ├── nodes/
+	│   │   │   └── route.js # api para nodos
+	│   │   ├── edges/
+	│   │   │   └── route.js # api para aristas y conexiones
+	│   │   └── groups/
+	│   │       └── route.js # api para grupos
+	│   ├── layout.jsx # layout ppal de la app
-	│   ├── page.jsx # home
-	│   └── globals.css # estilos globales
-	│   
-	├── components/ # componentes react
-	│   ├── App.jsx
-	│   ├── Graph.jsx
-   │   └── Sidebar.jsx
-	│
+	├── services/ # manejo de datos (inicialmente local)
+	│   └── DataService.js    
-	│
+	├── lib/ # utilidades y configuraciones  
+	│   ├── api-client.js # cliente http centralizado p llamar a la api
+	│   └── constants.js # const compartidas (tipos de nodos, endpoints... )
-	│
-	├── data/ # datos estaticos - mocks            
-	│   ├── nodes.js
-	│   ├── edges.js
-	│   ├── groups.js
-	│   └── index.js #exportacion centralizada de los datos
-	│ 
+	├── hooks/ #custom react hooks (solo1)
+	│   └── useGraphData.js # con hook para manejar datos del grafo
-	│
-	├── utils/ #funciones utilitarias
-	│    └── cyutils.js
-	│
-	├── styles.css
-	├── styles.js
-	├── index.css

Dependencias

Está sujeto a modificaciones a medida que el proyecto vaya creciendo

npm ls
├── @emnapi/runtime@1.8.1 extraneous
├── babel-plugin-react-compiler@1.0.0
├── cytoscape-cola@2.5.1
├── cytoscape-cose-bilkent@4.1.0
├── cytoscape-fcose@2.2.0
├── cytoscape@3.33.1
├── eslint-config-next@16.1.5
├── eslint@9.39.2
├── next@16.1.5
├── react-dom@19.2.3
└── react@19.2.3

2. Creación de DataService (local)

Creación de src/services/DataService.js que retorna datos desde src/data (el acceso a datos sigue siendo local)

Centraliza todo el acceso a los datos de la app, cuando se pase a una API real solo cambia la implementación interna de los métodos sin tocar el resto de la app (con suerte funciona)

ahora: lógica centralizada para tener los nodos conectados

class DataService {

  constructor() {  
    this.dependencyEdges = nodes
      .filter(node => node.data.dependency) //solo nodos con padre
      .map(node => ({
        data: {
          source: node.data.id, //nodo hijo
          target: node.data.dependency //nodo padre
        }
      }));
      
    this.allEdges = systemEdges.concat(this.dependencyEdges);
    this.nodes = nodes;
    this.groups = groups;
  } 
  .. 

A simple vista el código esta raro.
Primero que nada porque hay funciones async siendo que, estamos consumiendo los datos localmente y eso se resuelve inmediatamente. Esto tiene una razón y es que la idea sigue siendo solo realizar pequeñas modificaciones en el código al integrar la API real.

  async getAllNodes() {
    return Promise.resolve(this.nodes);
  }

Entonces, ya estan definidas de manera tal que al llamarlas tengamos que hacer await cada vez que las llamamos.

const algo = await dataService.searchNodes(searchText);
		    |-----|

Todo esto en mis sueños sirve para que la migración a la api sea mas amena, es decir, no tengo que buscar quien lo llama y de donde porque ya esta preparadito.


3. Creación del hook - useGraphData

Creación de src/hooks/useGraphData.js
Centraliza toda la lógica para obtener y manipular los datos:
Por qué esto? porque todo lo relacionado a los datos del grafo esta en un solo lugar, la lógica de datos está separada de la interfaz -> ANTES, por ejemplo, el filtrado se re-implementaba en cada componente

  1. cuando arranca el componente, carga los datos iniciales automáticamente (ejecuta useEffect -> loadInitialData())
  2. el usuario selecciona nodos, filtra por grupos y demás
  3. el hook actualiza estados y maneja llamadas a la api
  4. el componente en la interfaz se re-renderiza con nuevos datos

COSITAS:

useState - Manejo de Estado

= permite agregar un estado a componentes funcionales

const [valor, setValor] = useState(valorInicial);

-> elements: almacena nodos y aristas del grafo

const [elements, setElements] = useState({ nodes: [], edges: [] });

-> groups: lista de grupos disponibles

const [groups, setGroups] = useState([]);

-> selection: nodos que están seleccionados

const [selection, setSelection] = useState({ selectedNodes: [] });

-> loading: indica si hay una operación en curso

const [loading, setLoading] = useState(true);

-> error: mensajes de error

const [error, setError] = useState(null);

useEffect - efectos

=maneja efectos secundarios en componentes funcionales - operaciones que afectan algo fuera del componente

useEffect(() => {
  // codigo que se ejecuta
  return () => {
  };
}, [dependencias]); //array de dependencias

useCallBack

como y xq lo uso acá?

const selectNode = useCallback(async (nodeId) => {
  // funcion
}, []); //array de dependencias

porque los callbacks se pasan a componentes hijos del grafo que se re-renderizan mucho y cytoscape es pesadito

  • memoriza la función entre renders
  • solo crea la función si cambian las dependencias
  • evita renders innecesarios en componentes hijos
// SIN useCallback - SE CREA UNA FUNCION NUEVA EN CADA RENDER 
const selectNode = (nodeId) => { 
	const connectedNodes = await dataService.getConnectedNodes(nodeId); 
	setSelection({ selectedNodes: connectedNodes }); };
	 
// problem 
// render 1 - selectNode: fA (ref memoria 1)
// render 2 - selectNode: fB (ref memoria 2)
// eender 3 - selectNode: fC (ref memoria 3)

Cada vez que un componente se rerenderiza, js crea una funcion nueva aunque el código sea igual, la referencia en memoria cambia >> react compara referencias >> los componentes hijos que reciben esta funcion se rerenderizan innesesariamente.

tenemos muchos nodos -> cada nodo recibe callbacks (onClick y esas) sin useCallback cada render crea funciones nuevas, todos los nodos se re-renderizan cytoscape es pesado y re-renderizar todo el grafo puede afectar un montón a la perfo

-> selectNode - callback para seleccionar un nodo

const selectNode = useCallback(async (nodeId) => {
  try {
    const connectedNodes = await dataService.getConnectedNodes(nodeId);
    setSelection({ selectedNodes: connectedNodes });
  } catch (err) {
    console.error('Error seleccionando nodo:', err);
  }
}, []); //array vacio porque no tiene dependencias, nunk se vuelve a crear
  • Es un callback porque se va a pasar a componentes hijos
  • async/await para manejar promesas

-> selectNodes - callback para multiples nodos: solo actualiza el estado, no necesita async

const selectNodes = useCallback((nodeIds) => {
  setSelection({ selectedNodes: nodeIds });
}, []);

-> clearSelection - callback para limpiar: sin parametros, limpia el estado de seleccionn

const clearSelection = useCallback(() => {
  setSelection({ selectedNodes: [] });
}, []);

-> showAllElements - con 1 dependencia

const showAllElements = useCallback(async () => {
  // ... codigo
}, [clearSelection]); //DEPENDE de clearSelection
  • Si clearSelection cambia, se recrea esta funcion pero como clearSelection también usa useCallback([]), nunca cambia...

Primeros tests en el front

Creo que la idea es migrar desp a una cosa mas piola como jest, pero por ahora sirven los visuales para chequear el dataservice y el hook - basic

Validación de datos locales (xahora)

1. src/test/dataservice/page.jsx - Data Service Test
  1. npm run dev
  2. entrar a http://localhost:3000/test/dataservice

Lo que se ve es una prueba de todas las funciones declaradas en dataservice y su resultado visual en el front al hacer click en la lista de resultados.

2. src/test/hook/page.jsx - useGraphData Test
  1. npm run dev
  2. entrar a http://localhost:3000/test/hook

Acá aparece useMemo() de React:
Evita calculos costosos con cada render:

const nodosVisibles = useMemo(() => {
    if (nodosPersonalizados) return nodosPersonalizados;
    
    return elements.nodes  // filtrar + mapear TODA la lista
        .filter(n => n?.data?.id)
        .map(n => n.data.id);
}, [elements.nodes, nodosPersonalizados]);

Sin useMemo -> cada vez que el componente se renderiza por cualquier cambio de estado, se recorre y procesa todo el array de nodos otra vez.

importante por la rerenderizacion que dije antes de cytoscape (por búsqueda, selección, lo que sea) - useMemo memoriza el resultado y solo recalcula cuando cambian las dependencias

haciendo este me encontre con este error "Encountered two children with the same key 'cgp'..."

2. src/test/hook/data-validation.jsx - data validation

Entonces, hay otro test de validación de datos con dos tipos de mensajes:

  • ERROR: rompe la funcionalidad y tiene que corregirse
  • ADVERTENCIA: no es lo ideal pero no jode

En este punto encontré problemas en los datos: ids duplicados, conexiones apuntando a nodos inexistentes y grupos con referencias que tampoco existían

Cambios que hice: arreglo errores y agrego nodos de prueba.

en: src/data/edges.js

Elimino duplicado:

  { data: { source: 'dgcye-fin', target: 'esc-liq-alc' } },

Agrego:

  // conn economia
  { data: { source: 'mec-info', target: 'test-econ-1' } },
  { data: { source: 'test-econ-1', target: 'sigaf' } },
  { data: { source: 'test-econ-2', target: 'sulh' } },
  // conn escuelas
  { data: { source: 'dgcye-rrhh', target: 'test-esc-1' } },
  { data: { source: 'dgcye-fin', target: 'test-esc-2' } },
  { data: { source: 'test-esc-2', target: 'test-esc-3' } },
  // conn inv publica
  { data: { source: 'bapin', target: 'test-inv-1' } },
  { data: { source: 'test-inv-1', target: 'tablero' } },
en: src/groups/nodes.js

Agrego:

  // economia
  { data: { id: 'test-econ-1', label: 'TEST Sistema Economía 1' }, classes: 'accion' },
  { data: { id: 'test-econ-2', label: 'TEST Sistema Economía 2' }, classes: 'accion' },

  // escuelas
  { data: { id: 'test-esc-1', label: 'TEST Sistema Escuelas 1' }, classes: 'accion' },
  { data: { id: 'test-esc-2', label: 'TEST Sistema Escuelas 2' }, classes: 'accion' },
  { data: { id: 'test-esc-3', label: 'TEST Liquidación Prueba' }, classes: 'accion' },
  
  // inversión publica
  { data: { id: 'test-inv-1', label: 'TEST Proyecto Monitor' }, classes: 'accion' },
en src/data/groups.js

Agrego:

GRUPO DE PRUEBA - EXTRA
  {
    title: 'TEST-Economía',
    icono: "recursos.png",
    nodeIds: ['mec', 'mec-info', 'test-econ-1', 'test-econ-2', 'sigaf', 'sulh']
  }

Elimino duplicado:

Ecosistema salarial -> tenía duplicado 'cgp'

4. Migración de componentes

1. App.js ---> App.jsx

Se movió la lógica del filtrado de elementos a useGraphData

Antes en src/components/App.js:

const filterElements = (allElements, selection) => {
  const nodes = allElements.nodes.filter(({ data: { id }}) => selection.selectedNodes.includes(id))
  const edges = allElements.edges.filter(({ data: { source, target } }) =>
    selection.selectedNodes.includes(source) && selection.selectedNodes.includes(target))
  return ({ nodes, edges })
}

El resto se mantiene igual a la espera de cambiar el tipo de grafo.

1. Sidebar.js ---> Sidebar.jsx

Cambios al momento de filtrar elementos en la búsqueda:

Antes en src/components/App.js:

  // Filtrar nodos dinámicamente al cambiar el texto del filtro
  useEffect(() => {
    const lowerCaseFilter = filterText.toLowerCase();
    
    if (!filterText.trim()) {
      // Si no hay texto en el filtro, no mostrar ningún nodo
      setFilteredNodes([]);
      return;
    }
    
    const filtered = allNodes.filter((node) => {
      return node.data.label.toLowerCase().includes(lowerCaseFilter)
    });
    
    setFilteredNodes(filtered);
  }, [filterText, allNodes]);

Ahora:

useEffect(() => {

    const nuevaBusqueda = async () => {

      if (!filterText.trim()) {
        setNodesFiltrados([]);
        return;
      }
      
      const res = await searchNodes(filterText);
      
      setNodesFiltrados(res);
    };

    nuevaBusqueda();
  }, [filterText, searchNodes]);

1. Graph.js ---> Graph.jsx

Sigue exactamente igual.


Proyecto original

Descripción

Sistema de nodos de relaciona sistemas informáticos entre si y con las áreas que lo administran.

Arquitectura

App creada con CRA Create React App documentation. Librería: Cytoscape Cliente pesado. Está deployado con gitlab pages: https://mapa-sistemas-181a7a.gitlab.io Aún no trabaja con una base de datos, la información está formato json.

Datos

Los nodos están en data/nodes.js Las relaciones están en data/edges.js

Hay dos tipos de nodos, los que representan sistemas informáticos y los que representan áreas. Los nodos de sistemas se relacionan entre si si interoperan de alguna manera. Puede ser manual (con un archivo para se pasa manuelamente de uno a otro), o a través de apis. En este caso, es manual. Cada nodo sistema, se relaciona con todas aquellas áreas que

Cada Nodo tiene principalmente, un label y un class, que dependerá del tipo de nodo.

Hay dos tipos de nodos, los que representan un área, y los que representan un sistema. Dentro de los que representan un área los class son:

  • org (si es un organismo)
  • nivel 1 (dependencias directa de org, por ejemplo, subsecretarias)
  • nivel 2 (dependencias directas del nivel 1, por ejemplo, direcciones provinciales) y si es una sistema el class es:
  • sistema

También cada nodo de tipo área tiene el parent si corresponde. Y aquellos que son de nivel 2 también tienen un sublabel con un texto que representa el organismo al que pertenecen.

Cada Arista tiene los ides de los nodos que conecta: source y target.

Estructuras de datos

Los datos se importan desde la carpeta data en App.js. Estos datos provienen de index.js que proporciona los siguientes datos:

  • groups
  • elements: { nodes, edges }

groups es una estructura que contiene 3 datos:

  • title: título del grupo
  • icono: un ícono para ese grupo (para la vista)
  • nodeIds: una lista con los id's de los nodos que conforman ese grupo

y elements es una estructura que contiene:

  • nodes: todos los nodos
  • edjes: todas las aristas

Luego en App se utilizan las siguientes estructuras:

  • selection: {selectedNodes: [nodeId]} - conjunto de nodos
  • elements: es una lista con nodes y edges. Pueden estar todos los nodos y edges o sólo un grupo.

Sidebar

Atributos: data, selection, setSelectiond, mostrarSoloGrupo, toggleMostrarSoloGrupo Funcionalidades: Por una lado el de filtro, por otro el de grupos predefinidos y por último, la función de filtrar nodos.

  • Filtro: El filtro permite el input de texto, con ese texto recorrerá filteredNodes, eleccionará aquellos cuyo label contengan ese texto y mostrará una lista con esos labels (filteredNOdes es una lista de todos los nodos ({id, label})). Esos labels a su vez son botones, que al ser clickeados, con el id que mapea dicho label, se generará el objeto selection: {[nodeId]}.
    Con nodeId se calculará la lista de nodos de se realcionan con él y se guardarán en selection.
    Con setSelection se actualizará el grafo.

  • Grupos Predefinido: El grupo predefinido tiene configurados un grupo de ids. Al clickear sobre un de estos botones, también genera el objeto selection, con la lista de ids.

Las aristas se calcularan a partir de los nodos pertenecientes al grupo.

Luego Sidebar tiene varios filtros automáticos que aplican sobre el gráfico:

  • showOrg: Muestra u oculta los organismos. El objetivo es simplificar el gráfico a las áreas consumidoras o productoras directamente.
  • mostrarSoloGrupo: Si se selecciona esta opción, el gráfico muestra sólamente el conjunto de nodos seleccionados. De la otra manera, este conjuntos queda resaltado en el grafo pero con todos los demás nodos por detrás.

Graph

Aatributos: elements, selection, setSelection, showOrg
Su principal tarea es gestionar cytoscape.
Al clickear en un nodo, se ejectua el setSelection.


Organizacion del proyecto:

src/
|
├── components/      	# componentes
│   ├── App.js
│   ├── Graph.js
│   └── Sidebar.js
│
├── data/              # datos json
│   ├── nodes.js
│   ├── edges.js
│   ├── groups.js
│   └── index.js
└── utils/           	# utilidades
    └── cyutils.js

Al hacer npm ls en la consola:

mapa-sistemas@0.1.0 C:\Users\Pc\Desktop\aaaa\grafos
├── @testing-library/jest-dom@5.17.0
├── @testing-library/react@13.4.0
├── @testing-library/user-event@13.5.0
├── cytoscape-cola@2.5.1
├── cytoscape-cose-bilkent@4.1.0
├── cytoscape-fcose@2.2.0
├── cytoscape@3.33.1
├── react-dom@18.3.1
├── react-scripts@5.0.1
├── react@18.3.1
└── web-vitals@2.1.4