From 8b97f578c6eacb95be642a3f7a0d176cc41b403f Mon Sep 17 00:00:00 2001 From: Pouzor Date: Sat, 16 May 2026 17:41:58 +0200 Subject: [PATCH] feat(node-modal): sanitize parent_id on type change and exclude descendants - App.tsx: filter descendants of edited node from parentCandidates to prevent picking a child as parent (would create a cycle). - NodeModal.tsx: clear parent_id when switching to a type with no valid parents, and revalidate parent_id on submit against the type's allowlist. - Tests: cover getValidParentTypes for lxc/vm/docker_container and types that disallow parents. ha-relevant: yes --- frontend/src/App.tsx | 19 ++++++++++++++- frontend/src/components/modals/NodeModal.tsx | 16 ++++++++++++- .../utils/__tests__/virtualEdgeParent.test.ts | 23 ++++++++++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eaee103..073b7b9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -561,7 +561,24 @@ export default function App() { onSubmit={handleUpdateNode} initial={editNode?.data} title="Edit Node" - parentCandidates={nodes.map((n) => ({ id: n.id, label: n.data.label ?? n.id, type: n.data.type }))} + parentCandidates={(() => { + const descendants = new Set() + if (editNodeId) { + const queue = [editNodeId] + while (queue.length) { + const id = queue.shift()! + for (const n of nodes) { + if (n.data.parent_id === id && !descendants.has(n.id)) { + descendants.add(n.id) + queue.push(n.id) + } + } + } + } + return nodes + .filter((n) => !descendants.has(n.id)) + .map((n) => ({ id: n.id, label: n.data.label ?? n.id, type: n.data.type })) + })()} currentNodeId={editNodeId ?? undefined} /> diff --git a/frontend/src/components/modals/NodeModal.tsx b/frontend/src/components/modals/NodeModal.tsx index 1b91cf3..d5b851f 100644 --- a/frontend/src/components/modals/NodeModal.tsx +++ b/frontend/src/components/modals/NodeModal.tsx @@ -95,8 +95,17 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node' setLabelError(false) const selectedType = (form.type ?? 'generic') as NodeType const canUseContainerMode = CONTAINER_MODE_TYPES.includes(selectedType) + const validParentTypes = getValidParentTypes(selectedType) + let safeParentId = form.parent_id + if (validParentTypes.length === 0) { + safeParentId = undefined + } else if (safeParentId) { + const parent = parentCandidates.find((n) => n.id === safeParentId) + if (!parent || !validParentTypes.includes(parent.type)) safeParentId = undefined + } onSubmit({ ...form, + parent_id: safeParentId, container_mode: canUseContainerMode ? !!form.container_mode : false, }) onClose() @@ -116,7 +125,12 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'