From b200ccc8a1fab7a8079cb59b554d90afb0fedb82 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Fri, 14 Aug 2026 02:09:21 +0200 Subject: [PATCH] fix(canvas): import YAML hardware specs as properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/utils/__tests__/importYaml.test.ts | 35 +++++++++++++++++-- frontend/src/utils/importYaml.ts | 26 ++++++++++++-- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/__tests__/importYaml.test.ts b/frontend/src/utils/__tests__/importYaml.test.ts index dfc481e..970b341 100644 --- a/frontend/src/utils/__tests__/importYaml.test.ts +++ b/frontend/src/utils/__tests__/importYaml.test.ts @@ -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() }) diff --git a/frontend/src/utils/importYaml.ts b/frontend/src/utils/importYaml.ts index 653ba2c..f220cc4 100644 --- a/frontend/src/utils/importYaml.ts +++ b/frontend/src/utils/importYaml.ts @@ -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