fix(canvas): keep a parent ahead of its children after a subnet import
The subnet import only guaranteed the zone preceded the nodes it pulled in. An arrival that is itself a parent — a Proxmox host with nested VMs, whose children stay put because they already have a parent — could end up listed after those children, which React Flow renders detached with a parent-not-found error. Nothing re-sorts on load, so the broken order survived a save. Reorder the whole array instead: every node now follows its parent, which covers both directions at once. An already-valid list comes back untouched and a parent cycle terminates rather than recursing. ha-relevant: yes
This commit is contained in:
committed by
Pouzor - Rémy Jardient
parent
d9451f35dc
commit
2bf9f1bca8
@@ -253,6 +253,40 @@ describe('canvasStore — importZoneSubnet', () => {
|
||||
expect(ids.indexOf('z1')).toBeLessThan(ids.indexOf('n2'))
|
||||
})
|
||||
|
||||
it('keeps an arrival that is itself a parent ahead of the children it leaves behind', () => {
|
||||
// A Proxmox host matches the subnet; its VM does not move, because it
|
||||
// already has a parent. The host must still be listed before the VM.
|
||||
useCanvasStore.setState({
|
||||
nodes: [
|
||||
device('proxmox', '192.168.1.10'),
|
||||
{ ...device('vm1', '10.0.0.1'), parentId: 'proxmox' },
|
||||
zone('z1'),
|
||||
],
|
||||
})
|
||||
|
||||
expect(useCanvasStore.getState().importZoneSubnet('z1', '192.168.1.0/24')).toBe(1)
|
||||
|
||||
const nodes = useCanvasStore.getState().nodes
|
||||
const ids = nodes.map((n) => n.id)
|
||||
expect(nodes.find((n) => n.id === 'proxmox')!.parentId).toBe('z1')
|
||||
expect(nodes.find((n) => n.id === 'vm1')!.parentId).toBe('proxmox')
|
||||
expect(ids.indexOf('z1')).toBeLessThan(ids.indexOf('proxmox'))
|
||||
expect(ids.indexOf('proxmox')).toBeLessThan(ids.indexOf('vm1'))
|
||||
})
|
||||
|
||||
it('leaves an already-valid order untouched', () => {
|
||||
useCanvasStore.setState({
|
||||
nodes: [zone('z1'), device('a', '10.0.0.1'), device('b', '10.0.0.2'), device('hit', '192.168.1.10')],
|
||||
})
|
||||
useCanvasStore.getState().importZoneSubnet('z1', '192.168.1.0/24')
|
||||
|
||||
// Only the matched node moves in the array; the untouched ones keep their
|
||||
// relative order.
|
||||
const ids = useCanvasStore.getState().nodes.map((n) => n.id)
|
||||
expect(ids.indexOf('a')).toBeLessThan(ids.indexOf('b'))
|
||||
expect(ids[0]).toBe('z1')
|
||||
})
|
||||
|
||||
it('is additive: a second subnet keeps the first import inside', () => {
|
||||
useCanvasStore.setState({
|
||||
nodes: [zone('z1'), device('n1', '192.168.1.10'), device('n2', '10.0.0.5')],
|
||||
|
||||
@@ -30,6 +30,32 @@ type Clipboard = { nodes: Node<NodeData>[]; edges: Edge<EdgeData>[] }
|
||||
/** Resolve a node's effective parent id from either the RF field or domain data. */
|
||||
const parentIdOf = (n: Node<NodeData>): string | undefined => n.parentId ?? n.data.parent_id ?? undefined
|
||||
|
||||
/**
|
||||
* Reorder so every node follows its parent, which is what React Flow needs to
|
||||
* resolve nesting — a child listed first renders detached and logs a
|
||||
* parent-not-found error. Order is otherwise preserved: a list that is already
|
||||
* valid comes back untouched. A parent cycle terminates instead of recursing.
|
||||
*/
|
||||
function orderParentsFirst(nodes: Node<NodeData>[]): Node<NodeData>[] {
|
||||
const byId = new Map(nodes.map((n) => [n.id, n]))
|
||||
const emitted = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
const out: Node<NodeData>[] = []
|
||||
|
||||
const visit = (n: Node<NodeData>) => {
|
||||
if (emitted.has(n.id) || visiting.has(n.id)) return
|
||||
visiting.add(n.id)
|
||||
const parent = n.parentId ? byId.get(n.parentId) : undefined
|
||||
if (parent) visit(parent)
|
||||
visiting.delete(n.id)
|
||||
emitted.add(n.id)
|
||||
out.push(n)
|
||||
}
|
||||
|
||||
for (const n of nodes) visit(n)
|
||||
return out
|
||||
}
|
||||
|
||||
// Zone subnet import: the grid the arrivals are packed on, in zone-relative
|
||||
// pixels. A cell is one node box plus the gap that follows it.
|
||||
const DEFAULT_ZONE_WIDTH = 360
|
||||
@@ -1069,13 +1095,13 @@ export const useCanvasStore = create<CanvasState>((rawSet, get) => {
|
||||
}
|
||||
})
|
||||
|
||||
// React Flow requires a parent to precede its children in the array.
|
||||
const others = updated.filter((n) => !moved.has(n.id))
|
||||
const children = updated.filter((n) => moved.has(n.id))
|
||||
const zoneIdx = others.findIndex((n) => n.id === zoneId)
|
||||
|
||||
return {
|
||||
nodes: [...others.slice(0, zoneIdx + 1), ...children, ...others.slice(zoneIdx + 1)],
|
||||
// React Flow requires a parent to precede its children in the array.
|
||||
// Reordering the whole list covers both directions at once: the zone
|
||||
// ahead of its new children, and an arrival that is itself a parent
|
||||
// (a Proxmox host with nested VMs, whose children stay put because
|
||||
// they already have a parent) ahead of the children it left behind.
|
||||
nodes: orderParentsFirst(updated),
|
||||
hasUnsavedChanges: true,
|
||||
past: [...s.past.slice(-49), { nodes: s.nodes, edges: s.edges }],
|
||||
future: [],
|
||||
|
||||
Reference in New Issue
Block a user