import { useState, useMemo, useCallback } from 'react' import { Search, Plus, ChevronDown, ChevronRight, X, LayoutGrid, GripVertical, Globe } from 'lucide-react' import { getDeviceRenderConfig, CATEGORY_LABELS, CATEGORY_ORDER } from '../nodes/deviceRegistry' import type { DeviceTypeResponse, DeviceTypeCreate } from '@/types' import { deviceTypesApi } from '@/api' interface DeviceToolbarProps { deviceTypes: DeviceTypeResponse[] onDeviceTypesChange: () => void } export function DeviceToolbar({ deviceTypes, onDeviceTypesChange }: DeviceToolbarProps) { const [search, setSearch] = useState('') const [collapsedCategories, setCollapsedCategories] = useState>(new Set()) const [showAddForm, setShowAddForm] = useState(false) const [newType, setNewType] = useState({ slug: '', label: '', category: 'network' }) const [addError, setAddError] = useState(null) const [addLoading, setAddLoading] = useState(false) const filteredByCategory = useMemo(() => { const lower = search.toLowerCase() const filtered = search ? deviceTypes.filter(dt => dt.label.toLowerCase().includes(lower) || dt.slug.toLowerCase().includes(lower)) : deviceTypes const grouped: Record = {} for (const dt of filtered) { if (!grouped[dt.category]) grouped[dt.category] = [] grouped[dt.category].push(dt) } return grouped }, [deviceTypes, search]) const toggleCategory = useCallback((cat: string) => { setCollapsedCategories(prev => { const next = new Set(prev) if (next.has(cat)) next.delete(cat) else next.add(cat) return next }) }, []) const handleDragStart = useCallback((e: React.DragEvent, deviceType: DeviceTypeResponse) => { e.dataTransfer.setData('application/reactflow-device', JSON.stringify({ slug: deviceType.slug, label: deviceType.label, category: deviceType.category, })) e.dataTransfer.effectAllowed = 'move' }, []) const handleAddType = useCallback(async () => { if (!newType.slug || !newType.label) { setAddError('Slug and label are required') return } setAddLoading(true) setAddError(null) try { await deviceTypesApi.create(newType) setNewType({ slug: '', label: '', category: 'network' }) setShowAddForm(false) onDeviceTypesChange() } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Failed to create device type' setAddError(msg) } finally { setAddLoading(false) } }, [newType, onDeviceTypesChange]) return (
setSearch(e.target.value)} className="w-full rounded-md border border-default bg-input pl-8 pr-2 py-1.5 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" />
{CATEGORY_ORDER.map(cat => { const items = filteredByCategory[cat] || [] const isCloud = cat === 'cloud' const ispMatchesSearch = !search || 'isp'.includes(search.toLowerCase()) || 'internet service provider'.includes(search.toLowerCase()) const showIsp = isCloud && ispMatchesSearch if (!items.length && !showIsp) return null const collapsed = collapsedCategories.has(cat) const totalCount = items.length + (showIsp ? 1 : 0) return (
{!collapsed && (
{items.map(dt => { const { icon: Icon, color } = getDeviceRenderConfig(dt.slug, dt.category) return (
handleDragStart(e, dt)} className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing active:scale-[0.98] transition-transform" > {dt.label}
) })} {showIsp && (
{ e.dataTransfer.setData('application/reactflow-device', JSON.stringify({ slug: 'isp', label: 'ISP', category: 'cloud', })) e.dataTransfer.effectAllowed = 'move' }} className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing active:scale-[0.98] transition-transform" > ISP
)}
)}
) })}
{/* Grouping section */}
Grouping
{[ { slug: 'subnet', label: 'Subnet', color: '#60a5fa' }, { slug: 'vlan', label: 'VLAN', color: '#a78bfa' }, { slug: 'site', label: 'Site', color: '#34d399' }, { slug: 'dmz', label: 'DMZ', color: '#f87171' }, ].map(item => (
{ e.dataTransfer.setData('application/reactflow-group', JSON.stringify({ slug: item.slug, label: item.label })) e.dataTransfer.effectAllowed = 'move' }} className="flex cursor-grab items-center gap-2 rounded px-2 py-1.5 text-xs text-primary hover:bg-elevated active:cursor-grabbing active:scale-[0.98] transition-transform" > {item.label}
))}
{!showAddForm ? ( ) : (
New Type
setNewType(prev => ({ ...prev, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') }))} className="rounded border border-default bg-input px-2 py-1 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" /> setNewType(prev => ({ ...prev, label: e.target.value }))} className="rounded border border-default bg-input px-2 py-1 text-xs text-primary placeholder:text-muted-foreground focus:border-accent focus:outline-none" /> {addError &&

{addError}

}
)}
) }