v1.1: Add CIDR subnet filtering and Agent filter in Devices page

This commit is contained in:
ypratama committed 2026-08-27 12:02:01 +07:00
commit 966058e2fe
422 files changed
+54656

No files matched your search

@@ -0,0 +1,241 @@
"use client";
import { useState, useEffect } from "react";
import { Modal } from "@/components/ui/Modal";
import { fmtBytes } from "@/lib/utils";
import { getViewAsHeaders } from "@/lib/admin-api";
import { Loader2, Monitor, Shield, Tag, Cpu, Clock, Network, ArrowDownToLine } from "lucide-react";
interface IpHistoryItem {
ip_address: string;
first_seen: string;
last_seen: string;
download: number;
upload: number;
flows: number;
}
interface DeviceMacDetailsModalProps {
isOpen: boolean;
onClose: () => void;
macAddress: string;
customLabel: string | null;
defaultLabel: string;
agentName: string;
manufacturer: string;
deviceType: string;
}
export default function DeviceMacDetailsModal({
isOpen,
onClose,
macAddress,
customLabel,
defaultLabel,
agentName,
manufacturer,
deviceType,
}: DeviceMacDetailsModalProps) {
const [ips, setIps] = useState<IpHistoryItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isExporting, setIsExporting] = useState(false);
const exportToPdf = async () => {
setIsExporting(true);
try {
const { pdf } = await import("@react-pdf/renderer");
const { default: DeviceMacPdfDocument } = await import("./DeviceMacPdfDocument");
const reportDateStr = new Date().toLocaleString("id-ID", { timeZone: "Asia/Jakarta", day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) + " WIB";
const doc = <DeviceMacPdfDocument macAddress={macAddress} customLabel={customLabel} defaultLabel={defaultLabel} agentName={agentName} manufacturer={manufacturer} deviceType={deviceType} ips={ips} reportDateStr={reportDateStr} />;
const blob = await pdf(doc).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `Device_MAC_Details_${macAddress.replace(/[:]/g, "")}.pdf`;
link.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error("Failed to export PDF:", err);
alert("Failed to generate PDF report. Please try again.");
} finally {
setIsExporting(false);
}
};
useEffect(() => {
if (!isOpen || !macAddress) return;
setIsLoading(true);
setError(null);
fetch(`/api/dashboard/devices/mac-details?mac=${encodeURIComponent(macAddress)}`, { headers: getViewAsHeaders() })
.then((res) => res.json())
.then((data) => {
if (data.ok) setIps(data.ips || []);
else setError(data.error || "Failed to load device details.");
})
.catch((err) => setError(err.message || "An error occurred while loading details."))
.finally(() => setIsLoading(false));
}, [isOpen, macAddress]);
const DateCell = ({ dateStr }: { dateStr?: string }) => {
if (!dateStr) return <span>-</span>;
try {
const d = new Date(dateStr);
const datePart = d.toLocaleDateString("id-ID", { day: "2-digit", month: "2-digit", year: "numeric" });
const timePart = d.toLocaleTimeString("id-ID", { hour: "2-digit", minute: "2-digit" }) + " WIB";
return (
<div className="flex flex-col items-center justify-center leading-tight py-0.5" title={`${datePart} ${timePart}`}>
<span className="font-medium text-slate-300 text-[11px] whitespace-nowrap">{datePart}</span>
<span className="text-[10px] text-slate-500 whitespace-nowrap">{timePart}</span>
</div>
);
} catch { return <span>{dateStr}</span>; }
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="🔍 Device Network Details"
>
<div className="space-y-6 max-h-[75vh] overflow-y-auto pr-1 custom-scrollbar">
{/* Profile Card */}
<div className="grid grid-cols-2 gap-3 sm:gap-4">
<div className="p-3 bg-white/[0.02] border border-white/5 rounded-xl space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-500 flex items-center gap-1">
<Tag className="w-3 h-3" /> MAC Address
</span>
<code className="text-xs text-white font-mono block truncate" title={macAddress}>{macAddress}</code>
</div>
<div className="p-3 bg-white/[0.02] border border-white/5 rounded-xl space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-500 flex items-center gap-1">
<Cpu className="w-3 h-3" /> Manufacturer
</span>
<span className="text-xs text-slate-200 block truncate" title={manufacturer}>
{manufacturer || "Unknown"}
</span>
</div>
<div className="p-3 bg-white/[0.02] border border-white/5 rounded-xl space-y-1 col-span-2">
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-500 flex items-center gap-1">
<Monitor className="w-3 h-3" /> Device Labels
</span>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-[10px] text-slate-500 font-medium">Custom:</span>
{customLabel ? (
<span className="text-xs font-semibold text-cyan-400 bg-cyan-400/10 border border-cyan-400/20 px-2 py-0.5 rounded-full">
{customLabel}
</span>
) : (
<span className="text-xs text-slate-500 italic">No label configured</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] text-slate-500 font-medium">System:</span>
<span className="text-xs text-slate-400 truncate">{defaultLabel}</span>
</div>
</div>
</div>
<div className="p-3 bg-white/[0.02] border border-white/5 rounded-xl space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-500 flex items-center gap-1">
<Shield className="w-3 h-3" /> Device Type
</span>
<span className="text-xs text-slate-200 block capitalize truncate" title={deviceType}>{deviceType || "Unknown"}</span>
</div>
<div className="p-3 bg-white/[0.02] border border-white/5 rounded-xl space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-500 flex items-center gap-1">
<Network className="w-3 h-3" /> Network Agent
</span>
<span className="text-xs text-purple-400 font-semibold block truncate" title={agentName}>
{agentName}
</span>
</div>
</div>
{/* IPs Section */}
<div className="space-y-3">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-400 flex items-center gap-2">
🔗 Associated IP Addresses
</h4>
{isLoading ? (
<div className="py-8 flex justify-center items-center gap-2 text-slate-400 text-xs">
<Loader2 className="w-4 h-4 animate-spin text-primary" />
Resolving active IP address histories...
</div>
) : error ? (
<div className="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-400 rounded-lg text-xs">
⚠️ {error}
</div>
) : ips.length === 0 ? (
<div className="py-8 text-center text-slate-500 italic text-xs border border-white/5 rounded-xl bg-white/[0.01]">
No IP addresses recorded for this MAC address.
</div>
) : (
<div className="border border-white/10 rounded-xl overflow-x-auto overflow-y-auto max-h-60 custom-scrollbar bg-[#090D16]">
<table className="w-full text-xs text-slate-300 border-collapse table-fixed min-w-[520px]">
<thead className="bg-[#0F172A] text-slate-300 uppercase text-[9px] tracking-wider border-b border-white/10 sticky top-0 z-10">
<tr className="bg-[#0F172A]">
<th className="px-2.5 py-2.5 text-center w-[25%]">IP Address</th>
<th className="px-2 py-2.5 text-center w-[20%]">First Seen</th>
<th className="px-2 py-2.5 text-center w-[20%]">Last Seen</th>
<th className="px-2 py-2.5 text-center w-[23%]">Traffic (Down / Up)</th>
<th className="px-2 py-2.5 text-center w-[12%]">Flows</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{ips.map((ip) => (
<tr key={ip.ip_address} className="hover:bg-white/[0.02] transition-colors">
<td className="px-2.5 py-2.5 font-mono text-center text-slate-200">
<div className="truncate w-full text-center font-semibold text-cyan-300 text-xs" title={ip.ip_address}>
{ip.ip_address}
</div>
</td>
<td className="px-2 py-2.5 text-center text-slate-400">
<DateCell dateStr={ip.first_seen} />
</td>
<td className="px-2 py-2.5 text-center text-slate-400">
<DateCell dateStr={ip.last_seen} />
</td>
<td className="px-2 py-2.5 text-center font-mono text-[11px] whitespace-nowrap">
<span className="text-cyan-400 font-semibold">{fmtBytes(ip.download)}</span>
<span className="text-slate-600 mx-1">/</span>
<span className="text-emerald-400 font-semibold">{fmtBytes(ip.upload)}</span>
</td>
<td className="px-2 py-2.5 text-center font-mono text-cyan-400 font-semibold text-xs">
{ip.flows?.toLocaleString() ?? 0}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div className="flex items-center justify-between pt-4 border-t border-white/5">
<button
type="button"
disabled={isExporting || isLoading}
onClick={exportToPdf}
className="flex items-center gap-1.5 px-3 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-semibold transition-all disabled:opacity-50"
>
{isExporting ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <ArrowDownToLine className="w-3.5 h-3.5" />}
<span>{isExporting ? "Generating..." : "Export PDF"}</span>
</button>
<button
type="button"
onClick={onClose}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg text-xs font-semibold transition-all"
>
Close Details
</button>
</div>
</div>
</Modal>
);
}