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