Files
homelable/frontend/src/components/modals/PendingDeviceModal.tsx
T

174 lines
5.9 KiB
TypeScript

import { Globe, Router, Server, Layers, Box, Container, HardDrive, Cpu, Wifi, Circle, Network } from 'lucide-react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
interface Service {
port: number
protocol: string
service_name: string
icon?: string | null
category?: string | null
}
export interface PendingDevice {
id: string
ip: string
mac: string | null
hostname: string | null
os: string | null
services: Service[]
suggested_type: string | null
status: string
discovery_source: string | null
discovered_at: string
}
interface PendingDeviceModalProps {
device: PendingDevice | null
onClose: () => void
onApprove: (device: PendingDevice) => void
onHide: (device: PendingDevice) => void
onIgnore: (device: PendingDevice) => void
}
const TYPE_ICONS: Record<string, React.ElementType> = {
isp: Globe,
router: Router,
server: Server,
proxmox: Layers,
vm: Box,
lxc: Container,
nas: HardDrive,
iot: Cpu,
ap: Wifi,
switch: Network,
generic: Circle,
}
const CATEGORY_COLORS: Record<string, string> = {
hypervisor: '#ff6e00',
nas: '#39d353',
automation: '#a855f7',
containers: '#00d4ff',
network: '#39d353',
security: '#f85149',
monitoring: '#e3b341',
database: '#a855f7',
web: '#00d4ff',
media: '#ff6e00',
iot: '#e3b341',
}
function categoryColor(category: string | null | undefined) {
if (!category) return '#8b949e'
return CATEGORY_COLORS[category.toLowerCase()] ?? '#8b949e'
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-baseline gap-2 text-xs">
<span className="text-muted-foreground w-20 shrink-0">{label}</span>
<span className="font-mono text-foreground break-all">{value}</span>
</div>
)
}
export function PendingDeviceModal({ device, onClose, onApprove, onHide, onIgnore }: PendingDeviceModalProps) {
if (!device) return null
const TypeIcon = TYPE_ICONS[device.suggested_type ?? 'generic'] ?? Circle
const handleApprove = () => { onApprove(device) }
const handleHide = () => { onHide(device); onClose() }
const handleIgnore = () => { onIgnore(device); onClose() }
return (
<Dialog open={!!device} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="bg-[#161b22] border-[#30363d] text-foreground max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-sm font-semibold">
<TypeIcon size={15} className="text-[#00d4ff] shrink-0" />
{device.hostname ?? device.ip}
</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.hostname && <InfoRow label="Hostname" value={device.hostname} />}
{device.mac && <InfoRow label="MAC" value={device.mac} />}
{device.os && <InfoRow label="OS" value={device.os} />}
{device.suggested_type && (
<InfoRow label="Type" value={device.suggested_type} />
)}
{device.discovery_source && (
<InfoRow label="Source" value={device.discovery_source.toUpperCase()} />
)}
<InfoRow label="Discovered" value={new Date(device.discovered_at.endsWith('Z') ? device.discovered_at : device.discovered_at + 'Z').toLocaleString()} />
</div>
{/* Services */}
<div>
<p className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
Services found ({device.services.length})
</p>
{device.services.length === 0 ? (
<p className="text-xs text-muted-foreground italic">No services detected</p>
) : (
<div className="flex flex-col gap-1 max-h-40 overflow-y-auto">
{device.services.map((svc, i) => (
<div
key={i}
className="flex items-center gap-2 px-2.5 py-1.5 rounded bg-[#21262d] border border-[#30363d] text-xs"
>
<span
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ backgroundColor: categoryColor(svc.category) }}
/>
<span className="font-mono text-[#00d4ff] w-10 shrink-0">{svc.port}</span>
<span className="text-muted-foreground w-7 shrink-0">{svc.protocol}</span>
<span className="text-foreground truncate">{svc.service_name}</span>
{svc.category && (
<span className="ml-auto text-[10px] shrink-0" style={{ color: categoryColor(svc.category) }}>
{svc.category}
</span>
)}
</div>
))}
</div>
)}
</div>
{/* Actions */}
<div className="flex gap-2 pt-1">
<Button
size="sm"
className="flex-1 bg-[#39d353]/15 text-[#39d353] hover:bg-[#39d353]/25 border border-[#39d353]/30"
onClick={handleApprove}
>
Approve
</Button>
<Button
size="sm"
variant="ghost"
className="flex-1 text-muted-foreground hover:text-foreground hover:bg-[#30363d]"
onClick={handleHide}
>
Hide
</Button>
<Button
size="sm"
variant="ghost"
className="flex-1 text-[#f85149] hover:text-[#f85149] hover:bg-[#f85149]/10"
onClick={handleIgnore}
>
Delete
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}