Files
homelable/frontend/src/utils/exportYaml.ts
T

137 lines
5.3 KiB
TypeScript

import type { Node, Edge } from '@xyflow/react'
import type { NodeData, EdgeData, EdgeType } from '@/types'
import type { YamlNode, YamlNodeConnection } from '@/types/yaml'
import yaml from 'js-yaml'
/** Build a map of node id → label for edge resolution */
function buildIdToLabel(nodes: Node<NodeData>[]): Map<string, string> {
const m = new Map<string, string>()
for (const n of nodes) m.set(n.id, n.data.label)
return m
}
function makeConnection(targetLabel: string, edgeType: EdgeType, edgeLabel: string | undefined): YamlNodeConnection {
return {
label: targetLabel,
linkType: edgeType,
linkLabel: edgeLabel ?? '',
}
}
/**
* Serialize React Flow canvas state to a YAML string.
* Each node becomes one entry; edges are embedded as parent/clusterR/clusterL sub-objects.
* Edge deduplication: each edge is written on exactly one side (source as clusterR, target as clusterL)
* unless the edge type is 'virtual' or there is a parentId relationship, in which case
* it becomes the 'parent' field of the child node.
*/
export function exportCanvasToYaml(nodes: Node<NodeData>[], edges: Edge<EdgeData>[]): string {
const idToLabel = buildIdToLabel(nodes)
// Build per-node edge maps (id → connections)
// We use a Set to track already-serialized edge ids (deduplication).
const serializedEdges = new Set<string>()
// Index edges by source and target for quick lookup
const edgesBySource = new Map<string, Edge<EdgeData>[]>()
const edgesByTarget = new Map<string, Edge<EdgeData>[]>()
for (const e of edges) {
if (!edgesBySource.has(e.source)) edgesBySource.set(e.source, [])
edgesBySource.get(e.source)!.push(e)
if (!edgesByTarget.has(e.target)) edgesByTarget.set(e.target, [])
edgesByTarget.get(e.target)!.push(e)
}
const yamlNodes: YamlNode[] = []
for (const node of nodes) {
const d = node.data
// Skip groupRect nodes — they are canvas decoration only
if (d.type === 'groupRect') continue
const entry: YamlNode = {
nodeType: d.type,
label: d.label,
}
if (d.custom_icon) entry.nodeIcon = d.custom_icon
if (d.hostname) entry.hostname = d.hostname
if (d.ip) entry.ipAddress = d.ip
if (d.check_method && d.check_method !== 'none') entry.checkMethod = d.check_method
if (d.check_target) entry.checkTarget = d.check_target
if (d.notes) entry.notes = d.notes
// Hardware specs — omit zero values
if (d.cpu_model) entry.cpuModel = d.cpu_model
if (d.cpu_count && d.cpu_count > 0) entry.cpuCore = d.cpu_count
if (d.ram_gb && d.ram_gb > 0) entry.ram = d.ram_gb
if (d.disk_gb && d.disk_gb > 0) entry.disk = d.disk_gb
// Parent relationship: if this node has a parentId in React Flow,
// encode it as a 'parent' connection using any virtual edge between them.
if (node.parentId) {
const parentLabel = idToLabel.get(node.parentId) ?? node.parentId
// Find an edge between parent and this node (either direction)
const parentEdges = [
...(edgesBySource.get(node.parentId) ?? []).filter((e) => e.target === node.id),
...(edgesByTarget.get(node.parentId) ?? []).filter((e) => e.source === node.id),
]
const pEdge = parentEdges[0]
const linkType: EdgeType = (pEdge?.data?.type as EdgeType) ?? 'virtual'
const linkLabel = pEdge?.data?.label ?? ''
entry.parent = { label: parentLabel, linkType, linkLabel: linkLabel as string }
if (pEdge) serializedEdges.add(pEdge.id)
}
// Outgoing edges (this node is the source):
// - cluster type → clusterR (Proxmox cluster link, directional)
// - everything else → links array (supports multiple connections)
const outgoingEdges = (edgesBySource.get(node.id) ?? []).filter(
(e) => !serializedEdges.has(e.id) && e.target !== node.parentId,
)
for (const e of outgoingEdges) {
const targetLabel = idToLabel.get(e.target)
if (!targetLabel) continue
const edgeType: EdgeType = (e.data?.type as EdgeType) ?? 'ethernet'
const edgeLabel = e.data?.label as string | undefined
const conn = makeConnection(targetLabel, edgeType, edgeLabel)
if (edgeType === 'cluster') {
if (!entry.clusterR) entry.clusterR = conn
} else {
entry.links = [...(entry.links ?? []), conn]
}
serializedEdges.add(e.id)
}
// Incoming cluster edges not yet serialized → clusterL
const incomingClusterEdges = (edgesByTarget.get(node.id) ?? []).filter(
(e) => !serializedEdges.has(e.id) && (e.data?.type as EdgeType) === 'cluster',
)
for (const e of incomingClusterEdges) {
const sourceLabel = idToLabel.get(e.source)
if (!sourceLabel) continue
const edgeLabel = e.data?.label as string | undefined
if (!entry.clusterL) entry.clusterL = makeConnection(sourceLabel, 'cluster', edgeLabel)
serializedEdges.add(e.id)
}
yamlNodes.push(entry)
}
return yaml.dump(yamlNodes, { lineWidth: -1, noRefs: true })
}
/** Trigger a browser file download with the given YAML content */
export function downloadYaml(content: string, filename = 'homelable-export.yaml'): void {
const blob = new Blob([content], { type: 'text/yaml;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}