feat(zigbee): import to pending section with edge persistence
Coordinator auto-approves to a canvas Node; routers/end devices land in pending_devices keyed by IEEE. Discovered parent->child edges are stored in pending_device_links so that approving a pending device later auto-creates the Edge once both endpoints exist as canvas Nodes. - new POST /api/v1/zigbee/import-pending (default mode in modal) - new pending_device_links table; ieee_address on nodes + pending_devices - pending_devices.ip migrated to nullable (table rebuild on existing DBs) - approve / bulk-approve return auto-created edges; sidebar pushes them into the canvas store with bottom -> top-t handles - ZigbeeImportModal: radio toggle pending vs canvas; reset on close - PendingDeviceModal: zigbee badge, IEEE/LQI/vendor/model rows, services hidden for zigbee - Sidebar pending row: ZIG source badge, LQI badge, friendly_name fallback - SearchBar: null-safe IP, also searches friendly_name and ieee_address - Tooltip trigger uses asChild to avoid nested-button hydration error
This commit is contained in:
@@ -12,7 +12,7 @@ interface Service {
|
||||
|
||||
export interface PendingDevice {
|
||||
id: string
|
||||
ip: string
|
||||
ip: string | null
|
||||
mac: string | null
|
||||
hostname: string | null
|
||||
os: string | null
|
||||
@@ -20,6 +20,12 @@ export interface PendingDevice {
|
||||
suggested_type: string | null
|
||||
status: string
|
||||
discovery_source: string | null
|
||||
ieee_address?: string | null
|
||||
friendly_name?: string | null
|
||||
device_subtype?: string | null
|
||||
model?: string | null
|
||||
vendor?: string | null
|
||||
lqi?: number | null
|
||||
discovered_at: string
|
||||
}
|
||||
|
||||
@@ -77,6 +83,8 @@ export function PendingDeviceModal({ device, onClose, onApprove, onHide, onIgnor
|
||||
if (!device) return null
|
||||
|
||||
const TypeIcon = TYPE_ICONS[device.suggested_type ?? 'generic'] ?? Circle
|
||||
const isZigbee = device.discovery_source === 'zigbee'
|
||||
const titleLabel = device.friendly_name ?? device.hostname ?? device.ip ?? device.ieee_address ?? 'Pending device'
|
||||
|
||||
const handleApprove = () => { onApprove(device) }
|
||||
const handleHide = () => { onHide(device); onClose() }
|
||||
@@ -88,17 +96,30 @@ export function PendingDeviceModal({ device, onClose, onApprove, onHide, onIgnor
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<TypeIcon size={15} className="text-[#00d4ff] shrink-0" />
|
||||
{device.hostname ?? device.ip}
|
||||
{titleLabel}
|
||||
{isZigbee && (
|
||||
<span className="ml-1 text-[9px] font-mono uppercase px-1 py-0.5 rounded bg-[#00d4ff]/15 text-[#00d4ff] border border-[#00d4ff]/30">
|
||||
Zigbee
|
||||
</span>
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 mt-1">
|
||||
{/* Device info */}
|
||||
<div className="flex flex-col gap-1.5 p-3 rounded-md bg-[#21262d] border border-[#30363d]">
|
||||
<InfoRow label="IP" value={device.ip} />
|
||||
{device.ip && <InfoRow label="IP" value={device.ip} />}
|
||||
{device.hostname && <InfoRow label="Hostname" value={device.hostname} />}
|
||||
{device.mac && <InfoRow label="MAC" value={device.mac} />}
|
||||
{device.os && <InfoRow label="OS" value={device.os} />}
|
||||
{device.ieee_address && <InfoRow label="IEEE" value={device.ieee_address} />}
|
||||
{device.friendly_name && device.friendly_name !== device.hostname && (
|
||||
<InfoRow label="Name" value={device.friendly_name} />
|
||||
)}
|
||||
{device.vendor && <InfoRow label="Vendor" value={device.vendor} />}
|
||||
{device.model && <InfoRow label="Model" value={device.model} />}
|
||||
{device.device_subtype && <InfoRow label="Role" value={device.device_subtype} />}
|
||||
{device.lqi != null && <InfoRow label="LQI" value={String(device.lqi)} />}
|
||||
{device.suggested_type && (
|
||||
<InfoRow label="Type" value={device.suggested_type} />
|
||||
)}
|
||||
@@ -108,8 +129,8 @@ export function PendingDeviceModal({ device, onClose, onApprove, onHide, onIgnor
|
||||
<InfoRow label="Discovered" value={new Date(device.discovered_at.endsWith('Z') ? device.discovered_at : device.discovered_at + 'Z').toLocaleString()} />
|
||||
</div>
|
||||
|
||||
{/* Services */}
|
||||
<div>
|
||||
{/* Services (skipped for Zigbee devices — they don't have IP services) */}
|
||||
{!isZigbee && <div>
|
||||
<p className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
||||
Services found ({device.services.length})
|
||||
</p>
|
||||
@@ -138,7 +159,7 @@ export function PendingDeviceModal({ device, onClose, onApprove, onHide, onIgnor
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-1">
|
||||
|
||||
Reference in New Issue
Block a user