From a4512dcf90a115429665f8886008db1aba4802ad Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 13 Apr 2026 20:13:03 +0000 Subject: [PATCH] feat(network): add GroupNode component with resize, inline label, and group type colors Co-Authored-By: Claude Sonnet 4.6 --- .../components/network/nodes/GroupNode.tsx | 86 +++++++++++++++++++ .../src/components/network/nodes/nodeTypes.ts | 2 +- frontend/src/types/network-diagram.ts | 6 ++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/network/nodes/GroupNode.tsx diff --git a/frontend/src/components/network/nodes/GroupNode.tsx b/frontend/src/components/network/nodes/GroupNode.tsx new file mode 100644 index 00000000..69f04be2 --- /dev/null +++ b/frontend/src/components/network/nodes/GroupNode.tsx @@ -0,0 +1,86 @@ +import { memo, useState, useRef, useEffect } from 'react' +import { NodeProps, NodeResizer, useReactFlow } from '@xyflow/react' +import type { GroupNodeData } from '@/types/network-diagram' + +const GROUP_COLORS: Record = { + subnet: '#60a5fa', + vlan: '#a78bfa', + site: '#34d399', + dmz: '#f87171', + custom: '#94a3b8', +} + +const GroupNodeComponent = ({ data, selected, id }: NodeProps) => { + const groupData = data as GroupNodeData + const color = GROUP_COLORS[groupData.groupType] ?? GROUP_COLORS.custom + const [editing, setEditing] = useState(false) + const [labelValue, setLabelValue] = useState(groupData.label ?? '') + const inputRef = useRef(null) + const { updateNodeData } = useReactFlow() + + useEffect(() => { + if (editing) inputRef.current?.focus() + }, [editing]) + + // Sync if external data.label changes + useEffect(() => { + if (!editing) setLabelValue(groupData.label ?? '') + }, [groupData.label, editing]) + + const handleLabelCommit = () => { + setEditing(false) + if (labelValue !== groupData.label) { + updateNodeData(id, { ...groupData, label: labelValue }) + } + } + + return ( + <> + +
+
+ {editing ? ( + setLabelValue(e.target.value)} + onBlur={handleLabelCommit} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === 'Escape') handleLabelCommit() + e.stopPropagation() + }} + className="text-[11px] font-medium bg-transparent border-none outline-none text-primary min-w-[40px] max-w-[200px]" + style={{ color }} + /> + ) : ( + setEditing(true)} + > + {labelValue || groupData.groupType} + + )} +
+
+ + ) +} + +GroupNodeComponent.displayName = 'GroupNode' + +export const GroupNode = memo(GroupNodeComponent) +export default GroupNode diff --git a/frontend/src/components/network/nodes/nodeTypes.ts b/frontend/src/components/network/nodes/nodeTypes.ts index eb0a4462..586b3af6 100644 --- a/frontend/src/components/network/nodes/nodeTypes.ts +++ b/frontend/src/components/network/nodes/nodeTypes.ts @@ -1,5 +1,5 @@ import { DeviceNode } from './DeviceNode' -import { GroupNode } from '../ui/labeled-group-node' +import { GroupNode } from './GroupNode' export const nodeTypes = { device: DeviceNode, diff --git a/frontend/src/types/network-diagram.ts b/frontend/src/types/network-diagram.ts index 878984b7..55962640 100644 --- a/frontend/src/types/network-diagram.ts +++ b/frontend/src/types/network-diagram.ts @@ -123,6 +123,12 @@ export interface DiagramImportResponse { warnings: string[] } +export interface GroupNodeData { + label: string + groupType: 'subnet' | 'vlan' | 'site' | 'dmz' | 'custom' + [key: string]: unknown +} + export interface DiagramExportResponse { schemaVersion: number name: string