fix(canvas): import YAML hardware specs as properties

A node draws its `properties`; `cpu_model` / `cpu_count` / `ram_gb` /
`disk_gb` are inventory data that nothing renders — BaseNode reads them
only when `properties` is absent, which the API never allows (it returns
`device.properties or []`, and the model defaults to a list).

The YAML import filled those fields and set `show_hardware`, so an
imported node did show its specs — until the first save, when the
serializer sent `properties: []`, the fallback died and the specs
silently vanished.

It now mints the same four properties the 3.x migration and the Proxmox
import already create (CPU Model, CPU Cores, RAM, Disk), visible, so the
specs survive the save. The structured fields stay written: the YAML
export reads them back.

`show_hardware` is no longer set — it drives nothing.

The two assertions that covered the old behaviour move with it: the
scalar-field test drops `show_hardware`, and "sets show_hardware only
when hardware fields present" becomes three tests over the minted
properties.

ha-relevant: yes
This commit is contained in:
Pouzor
2026-08-14 02:09:21 +02:00
parent cb07c576db
commit b200ccc8a1
2 changed files with 56 additions and 5 deletions
@@ -65,12 +65,43 @@ describe('parseYamlToCanvas', () => {
expect(d.cpu_count).toBe(16)
expect(d.ram_gb).toBe(64)
expect(d.disk_gb).toBe(2000)
expect(d.show_hardware).toBe(true)
})
it('sets show_hardware only when hardware fields present', () => {
// A node draws its `properties`; the hardware fields are inventory data. An
// import that set `show_hardware` alone showed the specs until the first
// save, then lost them — so the specs arrive as properties instead.
it('mints the hardware specs as visible properties', () => {
const yaml = `
- nodeType: proxmox
label: "PVE1"
cpuModel: "Intel Xeon"
cpuCore: 16
ram: 64
disk: 2000
`
const { nodes } = parseYamlToCanvas(yaml, empty, emptyEdges)
expect(nodes[0].data.properties).toEqual([
{ key: 'CPU Model', value: 'Intel Xeon', icon: 'Cpu', visible: true },
{ key: 'CPU Cores', value: '16', icon: 'Cpu', visible: true },
{ key: 'RAM', value: '64 GB', icon: 'MemoryStick', visible: true },
{ key: 'Disk', value: '2000 GB', icon: 'HardDrive', visible: true },
])
// The structured fields survive for the export round trip.
expect(nodes[0].data.cpu_count).toBe(16)
})
it('mints only the specs the YAML carries', () => {
const yaml = `- nodeType: server\n label: "RamOnly"\n ram: 8\n`
const { nodes } = parseYamlToCanvas(yaml, empty, emptyEdges)
expect(nodes[0].data.properties).toEqual([
{ key: 'RAM', value: '8 GB', icon: 'MemoryStick', visible: true },
])
})
it('leaves a node with no hardware without properties', () => {
const yaml = `- nodeType: server\n label: "NoHW"\n`
const { nodes } = parseYamlToCanvas(yaml, empty, emptyEdges)
expect(nodes[0].data.properties).toBeUndefined()
expect(nodes[0].data.show_hardware).toBeUndefined()
})
+23 -3
View File
@@ -1,11 +1,29 @@
import yaml from 'js-yaml'
import type { Node, Edge } from '@xyflow/react'
import type { NodeData, EdgeData } from '@/types'
import type { NodeData, EdgeData, NodeProperty } from '@/types'
import type { YamlNode, YamlNodeConnection } from '@/types/yaml'
import { generateUUID } from '@/utils/uuid'
import { applyDagreLayout } from '@/utils/layout'
import { migrateClusterHandles } from '@/utils/canvasSerializer'
/**
* Hardware specs as canvas properties.
*
* A node draws its `properties`, never the `cpu_*` / `ram_gb` / `disk_gb`
* fields — those are inventory data, kept for the YAML round trip and the
* Proxmox import. An import that only filled them produced specs that showed
* until the first save and then silently vanished, so it mints the same four
* properties the 3.x migration and the Proxmox import already create.
*/
function hardwareProperties(yn: YamlNode): NodeProperty[] {
const props: NodeProperty[] = []
if (yn.cpuModel) props.push({ key: 'CPU Model', value: String(yn.cpuModel), icon: 'Cpu', visible: true })
if (yn.cpuCore) props.push({ key: 'CPU Cores', value: String(yn.cpuCore), icon: 'Cpu', visible: true })
if (yn.ram) props.push({ key: 'RAM', value: `${yn.ram} GB`, icon: 'MemoryStick', visible: true })
if (yn.disk) props.push({ key: 'Disk', value: `${yn.disk} GB`, icon: 'HardDrive', visible: true })
return props
}
/**
* Parse a YAML string and merge the resulting nodes/edges into the existing canvas.
* - Nodes with the same label as an existing node are skipped (no duplicates).
@@ -55,7 +73,7 @@ export function parseYamlToCanvas(
const id = generateUUID()
labelToId.set(yn.label, id)
const hasHardware = !!(yn.cpuModel || yn.cpuCore || yn.ram || yn.disk)
const hardware = hardwareProperties(yn)
const data: NodeData = {
label: yn.label,
@@ -72,7 +90,9 @@ export function parseYamlToCanvas(
...(yn.cpuCore ? { cpu_count: yn.cpuCore } : {}),
...(yn.ram ? { ram_gb: yn.ram } : {}),
...(yn.disk ? { disk_gb: yn.disk } : {}),
...(hasHardware ? { show_hardware: true } : {}),
// Kept as structured fields for the export round trip; drawn through the
// properties above.
...(hardware.length > 0 ? { properties: hardware } : {}),
// Restore custom connection-point counts so the edges below attach to the
// slots they were exported on instead of collapsing onto slot 0.
// A count of 0 is meaningful (side with no connection point), so test for