/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useMemo } from 'react'; import { Device, DeviceType } from '../types'; import { Server, Search, Plus, Trash, Edit2, MapPin, Gauge, BookOpen, Save, X, Info, ExternalLink } from 'lucide-react'; // Built-in device class presets shown in the dropdown. const DEVICE_CLASS_PRESETS = ['Switch', 'Firewall', 'Access-Point', 'Controller']; interface DeviceInventoryProps { devices: Device[]; checkmkEnabled: boolean; checkmkBaseUrl: string; onAddDevice: (device: Omit) => void; onUpdateDevice: (device: Device) => void; onDeleteDevice: (id: string) => void; } export default function DeviceInventory({ devices, checkmkEnabled, checkmkBaseUrl, onAddDevice, onUpdateDevice, onDeleteDevice, }: DeviceInventoryProps) { // Filters & State const [searchTerm, setSearchTerm] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [selectedDeviceId, setSelectedDeviceId] = useState(devices[0]?.id || null); // Always derived from prop so edits reflect immediately in the detail panel const selectedDevice = useMemo( () => devices.find(d => d.id === selectedDeviceId) ?? null, [devices, selectedDeviceId] ); // Create / Edit modal state const [isEditing, setIsEditing] = useState(false); const [formMode, setFormMode] = useState<'add' | 'edit'>('add'); // True when the user is defining a device class outside the presets. const [isCustomType, setIsCustomType] = useState(false); const [formData, setFormData] = useState<{ id?: string; hostname: string; ip: string; location: string; notes: string; type: DeviceType; emergencySheet: string; }>({ hostname: '', ip: '', location: '', notes: '', type: 'Switch', emergencySheet: '', }); const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' => d.status; const cmkHostUrl = (d: Device) => checkmkEnabled && checkmkBaseUrl && d.cmkHostname ? `${checkmkBaseUrl}/index.py?host=${encodeURIComponent(d.cmkHostname)}` : null; const statusMeta = (s: 'online' | 'offline' | 'unknown') => { if (s === 'online') return { label: 'online', badge: 'bg-emerald-950/60 border-emerald-900/80 text-emerald-400', dot: 'bg-emerald-400' }; if (s === 'offline') return { label: 'offline', badge: 'bg-rose-950/60 border-rose-900/80 text-rose-400', dot: 'bg-rose-400' }; return { label: 'unknown', badge: 'bg-slate-800/60 border-slate-700 text-slate-400', dot: 'bg-slate-500' }; }; // Filtered devices list const filteredDevices = devices.filter(dev => { const matchesSearch = dev.hostname.toLowerCase().includes(searchTerm.toLowerCase()) || dev.ip.includes(searchTerm) || dev.location.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = typeFilter === 'all' || dev.type === typeFilter; return matchesSearch && matchesType; }); const handleOpenAdd = () => { setFormMode('add'); setIsCustomType(false); setFormData({ hostname: '', ip: '172.16.', location: '', notes: '', type: 'Switch', emergencySheet: `### EMERGENCY MANUAL [HOSTNAME] **Device Type:** [Enter Model] **Serial Number:** [Enter Serial Number] #### 1. Out-of-Band Console Connection * **Baud Rate:** 115200 * **Data Bits:** 8 * **Parity:** None * **Stop Bits:** 1 #### 2. Recovery / Hard Reset 1. Press and hold down the physical reset micro-button on the front panel. 2. Cycle power, wait 10 seconds, then release.` }); setIsEditing(true); }; const handleOpenEdit = (dev: Device) => { setFormMode('edit'); setIsCustomType(!DEVICE_CLASS_PRESETS.includes(dev.type)); setFormData({ id: dev.id, hostname: dev.hostname, ip: dev.ip, location: dev.location, notes: dev.notes, type: dev.type, emergencySheet: dev.emergencySheet }); setIsEditing(true); }; const handleSave = (e: React.FormEvent) => { e.preventDefault(); if (!formData.hostname.trim() || !formData.ip.trim() || !formData.type.trim()) return; if (formMode === 'add') { onAddDevice({ hostname: formData.hostname, ip: formData.ip, location: formData.location, notes: formData.notes, type: formData.type, status: 'unknown', emergencySheet: formData.emergencySheet, }); } else if (formMode === 'edit' && formData.id) { const match = devices.find(d => d.id === formData.id); if (match) { onUpdateDevice({ ...match, hostname: formData.hostname, ip: formData.ip, location: formData.location, notes: formData.notes, type: formData.type, emergencySheet: formData.emergencySheet, }); } } setIsEditing(false); }; // Safe internal renderer for markdown headings and code blocks to support the emergency sheet visually const renderEmergencySheetHtml = (text: string) => { if (!text) return

No emergency manual entry registered for this device node.

; const lines = text.split('\n'); return lines.map((line, idx) => { // Headers if (line.startsWith('### ')) { return

{line.replace('### ', '')}

; } if (line.startsWith('#### ')) { return
{line.replace('#### ', '')}
; } if (line.startsWith('**') && line.endsWith('**')) { return

{line.replace(/\*\*/g, '')}

; } // Bullet lists if (line.startsWith('* ') || line.startsWith('- ')) { return
{line.replace(/^[\*\-]\s+/, '')}
; } // Numeric lists if (/^\d+\s*\.\s/.test(line)) { return
{line.match(/^\d+/)?.[0]}. {line.replace(/^\d+\s*\.\s+/, '')}
; } // Codeblocks if (line.startsWith('`') && line.endsWith('`')) { return {line.replace(/\`/g, '')}; } if (line.trim() === '```bash' || line.trim() === '```') { return null; } // Inline formatting fallback if (line.includes('**')) { return (

{line.split('**').map((tok, ti) => { return ti % 2 === 1 ? {tok} : tok; })}

); } if (line.includes('`')) { return (

{line.split('`').map((tok, ti) => { return ti % 2 === 1 ? {tok} : tok; })}

); } return line.trim() === '' ?
:

{line}

; }); }; return (
{/* LEFT COLUMN: Device List & Controls */}
{/* Title */}

Inventory

Every switch, firewall and AP we own. the source of truth, until someone forgets to update it.

{/* Filter Toolbar */}
setSearchTerm(e.target.value)} className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none focus:border-emerald-500 transition-colors placeholder:text-slate-500" />
{['all', 'Switch', 'Firewall', 'Access-Point', 'Controller'].map((type) => ( ))}
{/* Device Listing Card Table */}
{filteredDevices.length === 0 ? (
grep came back empty. no boxes match that filter.
) : ( filteredDevices.map((device) => { const isSelected = selectedDevice?.id === device.id; return (
setSelectedDeviceId(device.id)} className={`p-4 border rounded-xl cursor-pointer transition-all flex justify-between items-center ${ isSelected ? 'bg-slate-900 border-emerald-500/80 shadow-[0_4px_12px_rgba(16,185,129,0.06)]' : 'bg-slate-900/40 border-slate-800 hover:border-slate-700 hover:bg-slate-900/60' }`} >
{/* Device Icon Circle */}
{device.hostname} {device.type}
{device.ip} {device.location}
{/* Right: Actions and Status */}
e.stopPropagation()}> {/* CheckMK Status Badge – only when CheckMK is enabled */} {checkmkEnabled && (() => { const m = statusMeta(effectiveStatus(device)); return (
{m.label}
); })()} {/* Action Panel */}
{cmkHostUrl(device) && ( )}
); }) )}
{/* RIGHT COLUMN: Notfallhandbuch & Technical Specs Details */}
{selectedDevice ? ( <> {/* Header Spec Block */}
SPECS ID: {selectedDevice.id.toUpperCase()}

{selectedDevice.hostname} Active Link State

Hostname: {selectedDevice.hostname}
IP Address: {selectedDevice.ip}
Location: {selectedDevice.location}
Node Class: {selectedDevice.type}

Description & Technical Notes:

{selectedDevice.notes || 'No description notes registered.'}
{/* CheckMK Monitoring Panel – only when CheckMK is enabled */} {checkmkEnabled && (
CheckMK Monitoring {(() => { const m = statusMeta(effectiveStatus(selectedDevice)); return ( {m.label} ); })()}
{cmkHostUrl(selectedDevice) && ( Open host in CheckMK )} {selectedDevice.lastCheckedAt && (

Last checked: {new Date(selectedDevice.lastCheckedAt).toLocaleString()}

)}
)}
{/* Emergency rescue guidelines sheet */}

Emergency Sheet & Disaster Recovery

RESCUE SHEET
{/* Markdown Content box */}
{renderEmergencySheetHtml(selectedDevice.emergencySheet)}
Console baud rates, break-glass logins and the reset-button dance - for when SSH is a distant memory.
) : (
Pick a box from the list to see its specs and break-glass playbook.
)}
{/* FORM MODAL: Add / Edit Equipment */} {isEditing && (

{formMode === 'add' ? 'Register New Hardware Node' : 'Modify Hardware Specifications'}

setFormData({ ...formData, hostname: e.target.value })} className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500 font-mono" placeholder="SW-CORE-03" />
setFormData({ ...formData, ip: e.target.value })} className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500 font-mono" placeholder="172.16.x.x" />
setFormData({ ...formData, location: e.target.value })} className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500" placeholder="Server Room R02, Rack C4..." />
{isCustomType && ( setFormData({ ...formData, type: e.target.value as DeviceType })} className="w-full mt-2 bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500" placeholder="Enter new device class (e.g. Router, Load-Balancer)" /> )}