feat(network): add GroupNode component with resize, inline label, and group type colors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
86
frontend/src/components/network/nodes/GroupNode.tsx
Normal file
86
frontend/src/components/network/nodes/GroupNode.tsx
Normal file
@@ -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<string, string> = {
|
||||||
|
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<HTMLInputElement>(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 (
|
||||||
|
<>
|
||||||
|
<NodeResizer
|
||||||
|
isVisible={selected}
|
||||||
|
minWidth={120}
|
||||||
|
minHeight={80}
|
||||||
|
lineStyle={{ border: `1px solid ${color}` }}
|
||||||
|
handleStyle={{ width: 8, height: 8, borderRadius: 2, background: color }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-full h-full rounded-lg relative"
|
||||||
|
style={{
|
||||||
|
border: `1.5px dashed ${color}`,
|
||||||
|
background: `${color}0d`,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 -translate-y-full pb-0.5 pl-1">
|
||||||
|
{editing ? (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={labelValue}
|
||||||
|
onChange={e => 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 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-medium cursor-text select-none"
|
||||||
|
style={{ color }}
|
||||||
|
onDoubleClick={() => setEditing(true)}
|
||||||
|
>
|
||||||
|
{labelValue || groupData.groupType}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupNodeComponent.displayName = 'GroupNode'
|
||||||
|
|
||||||
|
export const GroupNode = memo(GroupNodeComponent)
|
||||||
|
export default GroupNode
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeviceNode } from './DeviceNode'
|
import { DeviceNode } from './DeviceNode'
|
||||||
import { GroupNode } from '../ui/labeled-group-node'
|
import { GroupNode } from './GroupNode'
|
||||||
|
|
||||||
export const nodeTypes = {
|
export const nodeTypes = {
|
||||||
device: DeviceNode,
|
device: DeviceNode,
|
||||||
|
|||||||
@@ -123,6 +123,12 @@ export interface DiagramImportResponse {
|
|||||||
warnings: string[]
|
warnings: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GroupNodeData {
|
||||||
|
label: string
|
||||||
|
groupType: 'subnet' | 'vlan' | 'site' | 'dmz' | 'custom'
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export interface DiagramExportResponse {
|
export interface DiagramExportResponse {
|
||||||
schemaVersion: number
|
schemaVersion: number
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Reference in New Issue
Block a user