fix: replace Proxmox cluster handles with configurable side points
The Proxmox node had two always-on cluster-left/cluster-right handles that rendered independently of the new per-side connection-point config — so a configured side stacked on top of the forced handle (e.g. 0 config still showed 1; 2 config left the forced one in the middle). - ProxmoxGroupNode: remove the always-on cluster handles in both container and non-container modes; cluster links now use the normal per-side points - add migrateClusterHandles(nodes, edges): on load, remap any edge still bound to cluster-left/right onto the left/right slot-0 point and set that node's side count to at least 1, so existing links survive (edge 'cluster' type/style kept). Applied on API load, standalone load, LiveView, and YAML import - App: new cluster links are made by choosing the Cluster edge type in the edge modal (drop the cluster-handle auto-default) ha-relevant: yes
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
serializeEdge,
|
||||
deserializeApiNode,
|
||||
deserializeApiEdge,
|
||||
migrateClusterHandles,
|
||||
type ApiNode,
|
||||
type ApiEdge,
|
||||
} from '@/utils/canvasSerializer'
|
||||
@@ -483,3 +484,40 @@ describe('serializeNode — text node roundtrip', () => {
|
||||
expect(restored.data.label).toBe('Hello world')
|
||||
})
|
||||
})
|
||||
|
||||
// ── migrateClusterHandles (#243) ──────────────────────────────────────────────
|
||||
|
||||
describe('migrateClusterHandles', () => {
|
||||
it('remaps cluster-right/left edge handles to right/left slot-0', () => {
|
||||
const nodes = [makeRfNode({ id: 'p1', type: 'proxmox' }), makeRfNode({ id: 'p2', type: 'proxmox' })]
|
||||
const edges = [makeRfEdge({ id: 'c1', source: 'p1', target: 'p2', type: 'cluster', sourceHandle: 'cluster-right', targetHandle: 'cluster-left', data: { type: 'cluster' } })]
|
||||
const out = migrateClusterHandles(nodes, edges)
|
||||
const e = out.edges.find((x) => x.id === 'c1')!
|
||||
expect(e.sourceHandle).toBe('right')
|
||||
expect(e.targetHandle).toBe('left')
|
||||
expect(e.type).toBe('cluster') // style/type preserved
|
||||
})
|
||||
|
||||
it('bumps the connected side to 1 on each proxmox node', () => {
|
||||
const nodes = [makeRfNode({ id: 'p1', type: 'proxmox' }), makeRfNode({ id: 'p2', type: 'proxmox' })]
|
||||
const edges = [makeRfEdge({ id: 'c1', source: 'p1', target: 'p2', sourceHandle: 'cluster-right', targetHandle: 'cluster-left' })]
|
||||
const out = migrateClusterHandles(nodes, edges)
|
||||
expect(out.nodes.find((n) => n.id === 'p1')!.data.right_handles).toBe(1)
|
||||
expect(out.nodes.find((n) => n.id === 'p2')!.data.left_handles).toBe(1)
|
||||
})
|
||||
|
||||
it('does not lower an already-higher side count', () => {
|
||||
const nodes = [makeRfNode({ id: 'p1', type: 'proxmox', data: { label: 'x', type: 'proxmox', status: 'online', services: [], right_handles: 3 } })]
|
||||
const edges = [makeRfEdge({ id: 'c1', source: 'p1', target: 'p2', sourceHandle: 'cluster-right' })]
|
||||
const out = migrateClusterHandles(nodes, edges)
|
||||
expect(out.nodes[0].data.right_handles).toBe(3)
|
||||
})
|
||||
|
||||
it('leaves non-cluster edges and their nodes untouched (identity when nothing to do)', () => {
|
||||
const nodes = [makeRfNode({ id: 'p1' })]
|
||||
const edges = [makeRfEdge({ id: 'e1', sourceHandle: 'bottom', targetHandle: 'top' })]
|
||||
const out = migrateClusterHandles(nodes, edges)
|
||||
expect(out.nodes).toBe(nodes)
|
||||
expect(out.edges[0].sourceHandle).toBe('bottom')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('parseYamlToCanvas', () => {
|
||||
expect(edges[0].data?.type).toBe('fibre')
|
||||
})
|
||||
|
||||
it('cluster edges have cluster-right→cluster-left handles', () => {
|
||||
it('cluster edges are remapped onto right→left connection points (#243)', () => {
|
||||
const yaml = `
|
||||
- nodeType: proxmox
|
||||
label: "PVE1"
|
||||
@@ -115,10 +115,15 @@ describe('parseYamlToCanvas', () => {
|
||||
- nodeType: proxmox
|
||||
label: "PVE2"
|
||||
`
|
||||
const { edges } = parseYamlToCanvas(yaml, empty, emptyEdges)
|
||||
const { nodes, edges } = parseYamlToCanvas(yaml, empty, emptyEdges)
|
||||
expect(edges).toHaveLength(1)
|
||||
expect(edges[0].sourceHandle).toBe('cluster-right')
|
||||
expect(edges[0].targetHandle).toBe('cluster-left')
|
||||
expect(edges[0].sourceHandle).toBe('right')
|
||||
expect(edges[0].targetHandle).toBe('left')
|
||||
// Connected sides get a connection point so the link is anchored.
|
||||
const src = nodes.find((n) => n.id === edges[0].source)!
|
||||
const tgt = nodes.find((n) => n.id === edges[0].target)!
|
||||
expect(src.data.right_handles).toBe(1)
|
||||
expect(tgt.data.left_handles).toBe(1)
|
||||
})
|
||||
|
||||
it('parent relationship sets parentId and creates an edge', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Node, Edge } from '@xyflow/react'
|
||||
import type { NodeData, EdgeData, Waypoint } from '@/types'
|
||||
import { normalizeHandle, clampHandles } from '@/utils/handleUtils'
|
||||
import { normalizeHandle, clampHandles, handleId, handleCountField, type Side } from '@/utils/handleUtils'
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -216,3 +216,52 @@ export function deserializeApiEdge(e: ApiEdge): Edge<EdgeData> {
|
||||
data: e as unknown as EdgeData,
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy Proxmox nodes had two always-on cluster handles ('cluster-left' /
|
||||
// 'cluster-right'). Those are gone — cluster links now use the normal, per-side
|
||||
// connection points. On load we remap any edge still bound to a cluster handle
|
||||
// onto the matching left/right slot-0 handle and give that node's side a
|
||||
// connection point (count → at least 1) so the link survives. The edge's
|
||||
// 'cluster' type/colour is untouched.
|
||||
const CLUSTER_HANDLE_SIDE: Record<string, Side> = {
|
||||
'cluster-left': 'left',
|
||||
'cluster-right': 'right',
|
||||
}
|
||||
|
||||
export function migrateClusterHandles(
|
||||
nodes: Node<NodeData>[],
|
||||
edges: Edge<EdgeData>[],
|
||||
): { nodes: Node<NodeData>[]; edges: Edge<EdgeData>[] } {
|
||||
// nodeId → sides that need at least one connection point after remap.
|
||||
const needed = new Map<string, Set<Side>>()
|
||||
const mark = (id: string, side: Side) => {
|
||||
const set = needed.get(id) ?? new Set<Side>()
|
||||
set.add(side)
|
||||
needed.set(id, set)
|
||||
}
|
||||
|
||||
const migratedEdges = edges.map((e) => {
|
||||
const srcSide = e.sourceHandle ? CLUSTER_HANDLE_SIDE[e.sourceHandle] : undefined
|
||||
const tgtSide = e.targetHandle ? CLUSTER_HANDLE_SIDE[e.targetHandle] : undefined
|
||||
if (!srcSide && !tgtSide) return e
|
||||
const next = { ...e }
|
||||
if (srcSide) { next.sourceHandle = handleId(srcSide, 0); mark(e.source, srcSide) }
|
||||
if (tgtSide) { next.targetHandle = handleId(tgtSide, 0); mark(e.target, tgtSide) }
|
||||
return next
|
||||
})
|
||||
|
||||
if (needed.size === 0) return { nodes, edges: migratedEdges }
|
||||
|
||||
const migratedNodes = nodes.map((n) => {
|
||||
const sides = needed.get(n.id)
|
||||
if (!sides) return n
|
||||
const data: NodeData = { ...n.data }
|
||||
for (const side of sides) {
|
||||
const field = handleCountField(side)
|
||||
data[field] = Math.max((data[field] as number | undefined) ?? 0, 1)
|
||||
}
|
||||
return { ...n, data }
|
||||
})
|
||||
|
||||
return { nodes: migratedNodes, edges: migratedEdges }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { NodeData, EdgeData } from '@/types'
|
||||
import type { YamlNode, YamlNodeConnection } from '@/types/yaml'
|
||||
import { generateUUID } from '@/utils/uuid'
|
||||
import { applyDagreLayout } from '@/utils/layout'
|
||||
import { migrateClusterHandles } from '@/utils/canvasSerializer'
|
||||
|
||||
/**
|
||||
* Parse a YAML string and merge the resulting nodes/edges into the existing canvas.
|
||||
@@ -170,5 +171,9 @@ export function parseYamlToCanvas(
|
||||
const mergedEdges = [...existingEdges, ...newEdges]
|
||||
const laidOut = applyDagreLayout(mergedNodes, mergedEdges)
|
||||
|
||||
return { nodes: laidOut, edges: mergedEdges, imported: newNodes.length }
|
||||
// Cluster links are imported on the legacy 'cluster-left/right' handles;
|
||||
// remap them to the per-side connection points (and give the side a point).
|
||||
const migrated = migrateClusterHandles(laidOut, mergedEdges)
|
||||
|
||||
return { nodes: migrated.nodes, edges: migrated.edges, imported: newNodes.length }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user