"use client"; import { useEffect, useState, useCallback } from "react"; import { createPortal } from "react-dom"; import { X, Loader2, Database, AlertCircle, Globe, Monitor, Cpu, Fingerprint, Hash, Terminal, Radio } from "lucide-react"; import { useMetadataDetail, MetadataDetailType } from "@/lib/api-metadata-detail"; import { Pagination } from "@/components/ui/Pagination"; import { fmtBytes, getKnownDomainService } from "@/lib/utils"; import { COLS, getColGroup, TableHead, TableRow } from "./MetadataDetailTable"; interface TypeConfig { title: string; subtitle: string; icon: React.ElementType; color: string; } const TYPE_CONFIG: Record = { sni_hostname: { title: "SNI Hostname", subtitle: "Devices that accessed this TLS hostname", icon: Globe, color: "blue" }, ssl_server_cn: { title: "SSL Common Name", subtitle: "Devices that connected to this certificate", icon: Globe, color: "blue" }, quic_hostname: { title: "QUIC Hostname", subtitle: "Devices using this QUIC destination", icon: Globe, color: "purple" }, netbios_hostname:{ title: "NetBIOS Hostname", subtitle: "Device records with this NetBIOS name", icon: Monitor, color: "cyan" }, os_label: { title: "Operating System", subtitle: "Devices running this operating system", icon: Cpu, color: "emerald" }, dhcp_fingerprint:{ title: "DHCP Fingerprint", subtitle: "Agents with this device fingerprint class", icon: Fingerprint, color: "amber" }, http_useragent: { title: "HTTP User Agent", subtitle: "Agent records for this User Agent string", icon: Globe, color: "orange" }, bittorrent_hash: { title: "BitTorrent Hash", subtitle: "Agent records for this torrent info hash", icon: Hash, color: "red" }, ssh_version: { title: "SSH Version", subtitle: "SSH client and server connections", icon: Terminal, color: "violet" }, mdns_hostname: { title: "mDNS Hostname", subtitle: "Agent records for this mDNS announcement", icon: Radio, color: "teal" }, }; const COLOR_MAP: Record = { blue: { icon: "text-blue-400", ring: "ring-blue-500/30", glow: "bg-blue-500" }, purple: { icon: "text-purple-400", ring: "ring-purple-500/30", glow: "bg-purple-500" }, cyan: { icon: "text-cyan-400", ring: "ring-cyan-500/30", glow: "bg-cyan-500" }, emerald:{ icon: "text-emerald-400", ring: "ring-emerald-500/30", glow: "bg-emerald-500" }, amber: { icon: "text-amber-400", ring: "ring-amber-500/30", glow: "bg-amber-500" }, orange: { icon: "text-orange-400", ring: "ring-orange-500/30", glow: "bg-orange-500" }, red: { icon: "text-red-400", ring: "ring-red-500/30", glow: "bg-red-500" }, violet: { icon: "text-violet-400", ring: "ring-violet-500/30", glow: "bg-violet-500" }, teal: { icon: "text-teal-400", ring: "ring-teal-500/30", glow: "bg-teal-500" }, }; interface Props { type: MetadataDetailType | null; value: string | null; timeRange: string; onClose: () => void; } const PAGE_SIZE = 50; export function MetadataDetailPanel({ type, value, timeRange, onClose }: Props) { const isOpen = !!(type && value); const { data, isLoading, error } = useMetadataDetail(type, value, timeRange); const [isMounted, setIsMounted] = useState(false); const [isClosing, setIsClosing] = useState(false); const [visible, setVisible] = useState(false); const [page, setPage] = useState(1); useEffect(() => { setIsMounted(true); }, []); const handleClose = useCallback(() => { setIsClosing(true); setTimeout(() => { setVisible(false); setIsClosing(false); document.body.style.overflow = "unset"; onClose(); }, 480); }, [onClose]); useEffect(() => { if (isOpen) { setVisible(true); setIsClosing(false); setPage(1); document.body.style.overflow = "hidden"; } }, [isOpen]); useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") handleClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [handleClose]); useEffect(() => () => { document.body.style.overflow = "unset"; }, []); if (!isMounted || !visible || !type || !value) return null; const cfg = TYPE_CONFIG[type]; const colors = COLOR_MAP[cfg.color]; const Icon = cfg.icon; const group = getColGroup(type); const cols = COLS[group]; const totalPages = Math.max(1, Math.ceil(data.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const pageStart = (safePage - 1) * PAGE_SIZE; const pagedData = data.slice(pageStart, pageStart + PAGE_SIZE); const totalDownload = data.reduce((s, r) => s + (r.download || 0), 0); const totalUpload = data.reduce((s, r) => s + (r.upload || 0), 0); return createPortal(
e.stopPropagation()} >

{cfg.title}

{value}

{getKnownDomainService(value) && ( {getKnownDomainService(value)} )}

{cfg.subtitle}

{!isLoading && data.length > 0 && (
{data.length.toLocaleString()} {" "}{data.length === 1 ? "record" : "records"} Total Download:{" "} {fmtBytes(totalDownload)} Total Upload:{" "} {fmtBytes(totalUpload)}
)}
{isLoading ? (

Loading data from Database...

) : error ? (

{error}

) : data.length === 0 ? (

No Data

No records found for this entry in Database

) : ( {pagedData.map((row, i) => ( ))}
)}
{!isLoading && !error && data.length > 0 && (
)}
, document.body ); }