feat(docker): allow LXC as parent for docker_container
Previously docker_container could only attach to a docker_host via virtual edge. Extend the rule so an LXC node can also act as parent, enabling docker_container nesting under LXC without requiring an intermediate docker_host. Extracts the virtual-edge parent rule into a pure helper (resolveVirtualEdgeParent) with unit tests covering all type pairs. ha-relevant: yes
This commit is contained in:
+9
-11
@@ -4,6 +4,7 @@ import { type Node } from '@xyflow/react'
|
||||
import { applyDagreLayout } from '@/utils/layout'
|
||||
import { serializeNode, serializeEdge, deserializeApiNode, deserializeApiEdge, type ApiNode, type ApiEdge } from '@/utils/canvasSerializer'
|
||||
import { generateUUID } from '@/utils/uuid'
|
||||
import { resolveVirtualEdgeParent } from '@/utils/virtualEdgeParent'
|
||||
import { generateMarkdownTable } from '@/utils/exportMarkdown'
|
||||
import { ExportModal } from '@/components/modals/ExportModal'
|
||||
import { exportCanvasToYaml, downloadYaml } from '@/utils/exportYaml'
|
||||
@@ -37,7 +38,6 @@ import type { ZigbeeNode, ZigbeeEdge } from '@/components/zigbee/types'
|
||||
|
||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||
const STANDALONE_STORAGE_KEY = 'homelable_canvas'
|
||||
const CONTAINER_MODE_TYPES = new Set<NodeData['type']>(['proxmox', 'vm', 'lxc', 'docker_host'])
|
||||
|
||||
export default function App() {
|
||||
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, copySelectedNodes, pasteNodes } = useCanvasStore()
|
||||
@@ -450,16 +450,14 @@ export default function App() {
|
||||
if (edgeData.type === 'virtual') {
|
||||
const src = nodes.find((n) => n.id === pendingConnection.source)
|
||||
const tgt = nodes.find((n) => n.id === pendingConnection.target)
|
||||
const srcType = src?.data.type as NodeData['type']
|
||||
const tgtType = tgt?.data.type as NodeData['type']
|
||||
if ((srcType === 'lxc' || srcType === 'vm') && CONTAINER_MODE_TYPES.has(tgtType)) {
|
||||
updateNode(pendingConnection.source, { parent_id: pendingConnection.target })
|
||||
} else if (CONTAINER_MODE_TYPES.has(srcType) && (tgtType === 'lxc' || tgtType === 'vm')) {
|
||||
updateNode(pendingConnection.target, { parent_id: pendingConnection.source })
|
||||
} else if (srcType === 'docker_container' && tgtType === 'docker_host') {
|
||||
updateNode(pendingConnection.source, { parent_id: pendingConnection.target })
|
||||
} else if (tgtType === 'docker_container' && srcType === 'docker_host') {
|
||||
updateNode(pendingConnection.target, { parent_id: pendingConnection.source })
|
||||
if (src && tgt) {
|
||||
const assignment = resolveVirtualEdgeParent(
|
||||
{ id: src.id, type: src.data.type as NodeData['type'] },
|
||||
{ id: tgt.id, type: tgt.data.type as NodeData['type'] },
|
||||
)
|
||||
if (assignment) {
|
||||
updateNode(assignment.childId, { parent_id: assignment.parentId })
|
||||
}
|
||||
}
|
||||
}
|
||||
setPendingConnection(null)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { resolveVirtualEdgeParent } from '../virtualEdgeParent'
|
||||
|
||||
describe('resolveVirtualEdgeParent', () => {
|
||||
it('nests lxc under proxmox (container-mode parent)', () => {
|
||||
const res = resolveVirtualEdgeParent(
|
||||
{ id: 'lxc1', type: 'lxc' },
|
||||
{ id: 'px1', type: 'proxmox' },
|
||||
)
|
||||
expect(res).toEqual({ childId: 'lxc1', parentId: 'px1' })
|
||||
})
|
||||
|
||||
it('nests vm under proxmox regardless of edge direction', () => {
|
||||
const res = resolveVirtualEdgeParent(
|
||||
{ id: 'px1', type: 'proxmox' },
|
||||
{ id: 'vm1', type: 'vm' },
|
||||
)
|
||||
expect(res).toEqual({ childId: 'vm1', parentId: 'px1' })
|
||||
})
|
||||
|
||||
it('nests docker_container under docker_host', () => {
|
||||
const res = resolveVirtualEdgeParent(
|
||||
{ id: 'dc1', type: 'docker_container' },
|
||||
{ id: 'dh1', type: 'docker_host' },
|
||||
)
|
||||
expect(res).toEqual({ childId: 'dc1', parentId: 'dh1' })
|
||||
})
|
||||
|
||||
it('nests docker_container under lxc (reverse direction)', () => {
|
||||
const res = resolveVirtualEdgeParent(
|
||||
{ id: 'lxc1', type: 'lxc' },
|
||||
{ id: 'dc1', type: 'docker_container' },
|
||||
)
|
||||
expect(res).toEqual({ childId: 'dc1', parentId: 'lxc1' })
|
||||
})
|
||||
|
||||
it('nests docker_container under lxc (forward direction)', () => {
|
||||
const res = resolveVirtualEdgeParent(
|
||||
{ id: 'dc1', type: 'docker_container' },
|
||||
{ id: 'lxc1', type: 'lxc' },
|
||||
)
|
||||
expect(res).toEqual({ childId: 'dc1', parentId: 'lxc1' })
|
||||
})
|
||||
|
||||
it('returns null when docker_container links to non-host/lxc target', () => {
|
||||
const res = resolveVirtualEdgeParent(
|
||||
{ id: 'dc1', type: 'docker_container' },
|
||||
{ id: 'srv1', type: 'server' },
|
||||
)
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for unrelated type pairs', () => {
|
||||
const res = resolveVirtualEdgeParent(
|
||||
{ id: 'srv1', type: 'server' },
|
||||
{ id: 'rt1', type: 'router' },
|
||||
)
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { NodeData } from '@/types'
|
||||
|
||||
export type NodeType = NodeData['type']
|
||||
|
||||
const CONTAINER_MODE_TYPES = new Set<NodeType>(['proxmox', 'vm', 'lxc', 'docker_host'])
|
||||
|
||||
export interface VirtualEdgeEndpoint {
|
||||
id: string
|
||||
type: NodeType
|
||||
}
|
||||
|
||||
export interface ParentAssignment {
|
||||
childId: string
|
||||
parentId: string
|
||||
}
|
||||
|
||||
export function resolveVirtualEdgeParent(
|
||||
source: VirtualEdgeEndpoint,
|
||||
target: VirtualEdgeEndpoint,
|
||||
): ParentAssignment | null {
|
||||
const { type: srcType, id: srcId } = source
|
||||
const { type: tgtType, id: tgtId } = target
|
||||
|
||||
if ((srcType === 'lxc' || srcType === 'vm') && CONTAINER_MODE_TYPES.has(tgtType)) {
|
||||
return { childId: srcId, parentId: tgtId }
|
||||
}
|
||||
if (CONTAINER_MODE_TYPES.has(srcType) && (tgtType === 'lxc' || tgtType === 'vm')) {
|
||||
return { childId: tgtId, parentId: srcId }
|
||||
}
|
||||
if (srcType === 'docker_container' && (tgtType === 'docker_host' || tgtType === 'lxc')) {
|
||||
return { childId: srcId, parentId: tgtId }
|
||||
}
|
||||
if (tgtType === 'docker_container' && (srcType === 'docker_host' || srcType === 'lxc')) {
|
||||
return { childId: tgtId, parentId: srcId }
|
||||
}
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user