Skip to content

Commit 28f2ece

Browse files
authored
feat(topology): wired children follow their parent when dragged (#1377) (#1390)
Dragging a hypervisor moves its CTs along; dragging a switch/AP left its wired children behind, pinned by their saved layout positions, so the links stretched across the canvas. Layout seeds now carry a timestamp. A chip (or distnode) whose parent was pinned LATER than itself ignores its own stale seed and re-derives around the moved parent - the group moves together, like hypervisor guests. A child pinned after its parent keeps its manual spot until the parent is dragged again. Old seeds without ts count as oldest, so existing layouts keep working unchanged. Closes #1377
1 parent 091ec34 commit 28f2ece

2 files changed

Lines changed: 41 additions & 11 deletions

File tree

‎app/src/components/topology/model.ts‎

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export interface TopologyInput {
135135
* conectan con la posición real). Los chips sin posición guardada (p. ej.
136136
* dispositivos nuevos) conservan su posición automática.
137137
*/
138-
seedPositions?: Record<string, { x: number; y: number }>
138+
seedPositions?: Record<string, { x: number; y: number; ts?: number }>
139139
}
140140

141141
/** Chip "+N" de un anillo desbordado (posición ya calculada, geometría local).
@@ -981,10 +981,15 @@ export function buildTopologyModel({ routers, devices, wan, wireguard, distribut
981981
// sus chips hijos (abanico/anillos) para que se re-deriven alrededor de la
982982
// posición movida. El resolver de colisiones los trata como fixed, así que
983983
// nada los desplaza después.
984+
// #1377: si el router dueño del distnode se pineó DESPUÉS (ts mayor), el
985+
// pin del distnode es del cluster viejo y se ignora: el distnode sigue a su
986+
// router y el grupo se mueve junto. Un pin posterior del distnode sí manda.
984987
if (seedPositions) {
985988
for (const dv of distNodes) {
986989
const p = seedPositions[dv.id]
987990
if (!p) continue
991+
const owner = seedPositions[dv.node.routerId]
992+
if (owner && (owner.ts ?? 0) > (p.ts ?? 0)) continue
988993
dv.x = p.x
989994
dv.y = p.y
990995
if (anchorPos.has(dv.id)) anchorPos.set(dv.id, { x: p.x, y: p.y })
@@ -1189,10 +1194,17 @@ export function buildTopologyModel({ routers, devices, wan, wireguard, distribut
11891194
if (!hypervisorHosts.has(c.id)) continue
11901195
c.size = 30
11911196
const p = seedPositions[c.id]
1192-
if (p) {
1193-
c.x = p.x
1194-
c.y = p.y
1195-
}
1197+
if (!p) continue
1198+
// #1377: si el hub del host (router/switch, posiblemente vía su
1199+
// distnode) se pineó después que el host, la semilla del host es del
1200+
// cluster viejo: no se aplica y el grid de CTs se re-deriva alrededor
1201+
// de la posición re-derivada del host (el grupo sigue al padre).
1202+
let hubTs = seedPositions[c.hubId]?.ts ?? 0
1203+
const hubDist = distById.get(c.hubId)
1204+
if (hubDist) hubTs = Math.max(hubTs, seedPositions[hubDist.routerId]?.ts ?? 0)
1205+
if (hubTs > (p.ts ?? 0)) continue
1206+
c.x = p.x
1207+
c.y = p.y
11961208
}
11971209
} else {
11981210
for (const c of chips) {
@@ -1234,10 +1246,26 @@ export function buildTopologyModel({ routers, devices, wan, wireguard, distribut
12341246
// bajo el host y el pin de su semilla vieja (#1378).
12351247
if (c.isCt && hypervisorHosts.has(c.hubId)) continue
12361248
const p = seedPositions[c.id]
1237-
if (p) {
1238-
c.x = p.x
1239-
c.y = p.y
1240-
}
1249+
if (!p) continue
1250+
// #1377: si el hub del chip (router/AP/switch/distnode) se pineó
1251+
// DESPUÉS que el chip (ts mayor), la semilla del chip es del cluster
1252+
// viejo y se ignora: el chip se re-deriva alrededor del padre movido
1253+
// y el grupo se mueve junto, igual que los CTs con su hipervisor.
1254+
// Un pin del chip POSTERIOR al del padre sí se respeta (reubicar un
1255+
// hijo a mano sigue funcionando; la próxima vez que se arrastre el
1256+
// padre, el hijo volverá a seguirlo). Semillas antiguas sin ts (=0)
1257+
// se tratan como las más viejas.
1258+
// Cadena: si el hub es un distnode (p. ej. el nodo hipervisor de un
1259+
// host Proxmox), quien puede llevar el pin nuevo es su ROUTER dueño
1260+
// (switch16 -> dist-pve-citadel-01 -> chip citadel-01), así que el ts
1261+
// del cluster del hub es el máximo de ambos niveles.
1262+
const hub = seedPositions[c.hubId]
1263+
let hubTs = hub?.ts ?? 0
1264+
const hubDist = distById.get(c.hubId)
1265+
if (hubDist) hubTs = Math.max(hubTs, seedPositions[hubDist.routerId]?.ts ?? 0)
1266+
if (hubTs > (p.ts ?? 0)) continue
1267+
c.x = p.x
1268+
c.y = p.y
12411269
}
12421270
}
12431271

‎app/src/pages/Topology.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default function Topology() {
8989
// (no se reordena); solo se re-edita al entrar de nuevo o al "Restablecer".
9090
const LAYOUT_KEY = 'netpulse.topology.layout'
9191
const [editMode, setEditMode] = useState(false)
92-
const [layout, setLayout] = useState<Record<string, { x: number; y: number }>>(() => {
92+
const [layout, setLayout] = useState<Record<string, { x: number; y: number; ts?: number }>>(() => {
9393
try {
9494
return JSON.parse(localStorage.getItem(LAYOUT_KEY) || '{}')
9595
} catch {
@@ -114,8 +114,10 @@ export default function Topology() {
114114

115115
// Durante el arrastre, onMoveNode actualiza la posición del nodo en `layout`
116116
// (vía semilla → el modelo recalcula y los hijos se re-derivan alrededor).
117+
// #1377: la semilla lleva ts (epoch ms) para que el modelo sepa si el pin de
118+
// un hijo es anterior al de su padre (entonces lo sigue) o posterior (manda).
117119
const handleMoveNode = (id: string, x: number, y: number) => {
118-
setLayout((prev) => (prev[id] && prev[id]!.x === x && prev[id]!.y === y ? prev : { ...prev, [id]: { x, y } }))
120+
setLayout((prev) => (prev[id] && prev[id]!.x === x && prev[id]!.y === y ? prev : { ...prev, [id]: { x, y, ts: Date.now() } }))
119121
}
120122

121123
const handleSaveLayout = () => {

0 commit comments

Comments
 (0)