fix: improve auto-layout peer alignment and edge direction
- Exclude peer-to-peer edges from Dagre to keep same-type nodes at same rank - Snap peer group Y to average, re-space X preserving user's original order - Reverse upward edges (sourceHandle=top) in Dagre so child nodes always render below parent - Add layout.test.ts covering peer alignment, X ordering, and upward edge cases
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { applyDagreLayout } from '../layout'
|
||||
import type { Node, Edge } from '@xyflow/react'
|
||||
import type { NodeData, EdgeData } from '@/types'
|
||||
|
||||
function makeNode(id: string, type: string, parentId?: string): Node<NodeData> {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position: { x: 0, y: 0 },
|
||||
data: { type, label: id } as unknown as NodeData,
|
||||
...(parentId ? { parentId } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function makeEdge(source: string, target: string, sourceHandle?: string): Edge<EdgeData> {
|
||||
return { id: `${source}-${target}`, source, target, sourceHandle, data: {} as EdgeData }
|
||||
}
|
||||
|
||||
describe('applyDagreLayout', () => {
|
||||
it('places two proxmox nodes connected to each other at the same Y', () => {
|
||||
const nodes = [
|
||||
makeNode('router', 'router'),
|
||||
makeNode('pve1', 'proxmox'),
|
||||
makeNode('pve2', 'proxmox'),
|
||||
]
|
||||
const edges = [
|
||||
makeEdge('router', 'pve1'),
|
||||
makeEdge('router', 'pve2'),
|
||||
makeEdge('pve1', 'pve2'),
|
||||
]
|
||||
|
||||
const result = applyDagreLayout(nodes, edges)
|
||||
const pve1 = result.find((n) => n.id === 'pve1')!
|
||||
const pve2 = result.find((n) => n.id === 'pve2')!
|
||||
|
||||
expect(pve1.position.y).toBe(pve2.position.y)
|
||||
})
|
||||
|
||||
it('orders peer nodes left-to-right by chain: endpoint first, middle last', () => {
|
||||
// pve-left -- pve-center -- pve-right (chain)
|
||||
// router connects to all three
|
||||
const nodes = [
|
||||
makeNode('router', 'router'),
|
||||
makeNode('pve-left', 'proxmox'),
|
||||
makeNode('pve-center', 'proxmox'),
|
||||
makeNode('pve-right', 'proxmox'),
|
||||
]
|
||||
const edges = [
|
||||
makeEdge('router', 'pve-left'),
|
||||
makeEdge('router', 'pve-center'),
|
||||
makeEdge('router', 'pve-right'),
|
||||
makeEdge('pve-left', 'pve-center'),
|
||||
makeEdge('pve-center', 'pve-right'),
|
||||
]
|
||||
|
||||
const result = applyDagreLayout(nodes, edges)
|
||||
const left = result.find((n) => n.id === 'pve-left')!
|
||||
const center = result.find((n) => n.id === 'pve-center')!
|
||||
const right = result.find((n) => n.id === 'pve-right')!
|
||||
|
||||
// All at same Y
|
||||
expect(left.position.y).toBe(center.position.y)
|
||||
expect(center.position.y).toBe(right.position.y)
|
||||
|
||||
// X order: endpoint (left or right) < center (middle has 2 peer connections)
|
||||
// The BFS starts from an endpoint, so we just verify the middle is not at the extremes
|
||||
const xs = [left.position.x, center.position.x, right.position.x].sort((a, b) => a - b)
|
||||
expect(center.position.x).toBe(xs[1]) // pve-center must be in the middle
|
||||
})
|
||||
|
||||
it('keeps child nodes (parentId set) in place', () => {
|
||||
const nodes = [
|
||||
makeNode('router', 'router'),
|
||||
makeNode('pve1', 'proxmox'),
|
||||
makeNode('vm1', 'vm', 'pve1'),
|
||||
]
|
||||
const edges = [makeEdge('router', 'pve1')]
|
||||
|
||||
const result = applyDagreLayout(nodes, edges)
|
||||
const vm1 = result.find((n) => n.id === 'vm1')!
|
||||
expect(vm1.position).toEqual({ x: 0, y: 0 })
|
||||
})
|
||||
|
||||
it('places a node below its parent when the edge exits from the top handle (upward edge)', () => {
|
||||
// Frigate connects UP to Proxmox via its top handle (source=Frigate, sourceHandle='top')
|
||||
// Dagre must place Frigate BELOW Proxmox, not above.
|
||||
const nodes = [
|
||||
makeNode('router', 'router'),
|
||||
makeNode('proxmox', 'proxmox'),
|
||||
makeNode('frigate', 'server'),
|
||||
]
|
||||
const edges = [
|
||||
makeEdge('router', 'proxmox'),
|
||||
makeEdge('frigate', 'proxmox', 'top'), // upward edge: frigate → proxmox via top handle
|
||||
]
|
||||
|
||||
const result = applyDagreLayout(nodes, edges)
|
||||
const proxmox = result.find((n) => n.id === 'proxmox')!
|
||||
const frigate = result.find((n) => n.id === 'frigate')!
|
||||
|
||||
expect(frigate.position.y).toBeGreaterThan(proxmox.position.y)
|
||||
})
|
||||
|
||||
it('places two switch nodes connected to each other at the same Y', () => {
|
||||
const nodes = [
|
||||
makeNode('router', 'router'),
|
||||
makeNode('sw1', 'switch'),
|
||||
makeNode('sw2', 'switch'),
|
||||
]
|
||||
const edges = [
|
||||
makeEdge('router', 'sw1'),
|
||||
makeEdge('router', 'sw2'),
|
||||
makeEdge('sw1', 'sw2'),
|
||||
]
|
||||
|
||||
const result = applyDagreLayout(nodes, edges)
|
||||
const sw1 = result.find((n) => n.id === 'sw1')!
|
||||
const sw2 = result.find((n) => n.id === 'sw2')!
|
||||
expect(sw1.position.y).toBe(sw2.position.y)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user