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

+240
View File
@@ -0,0 +1,240 @@
"use client";
import { useState, useEffect } from "react";
import { TimestampCell } from "@/components/ui/TimestampCell";
import { useEvents } from "@/lib/api-with-context";
import { DataTable, Column } from "@/components/ui/DataTable";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Loader2, AlertCircle, Info, AlertTriangle } from "lucide-react";
import { DeviceDetailModal } from "@/components/ui/DeviceDetailModal";
import { UniversalFilters, useUniversalFilterState, FilterConfig } from "@/components/ui/UniversalFilters";
import { HelpTrigger } from "@/components/help/HelpTrigger";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";
function EventsContent() {
const [mounted, setMounted] = useState(false);
const [selectedIp, setSelectedIp] = useState<string | null>(null);
const [selectedMac, setSelectedMac] = useState<string | null>(null);
const { data, isLoading } = useEvents();
const searchParams = useSearchParams();
const highlightId = searchParams.get("highlight");
const {
filters, dateFrom, dateTo,
handleFilterChange, setDateFrom, setDateTo,
handleReset, activeCount
} = useUniversalFilterState();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
const all = data || [];
const opts = (key: string) => {
const raw = Array.from(new Set(all.map((r: any) => String(r[key] || "")).filter(Boolean)));
return raw.sort();
};
const filterConfigs: FilterConfig[] = [
{ id: "severity", label: "Severity", type: "select", value: filters.severity, options: opts("severity") },
{ id: "event_type", label: "Event Type", type: "select", value: filters.event_type, options: opts("event_type") },
{ id: "source_ip", label: "Source IP", type: "select", value: filters.source_ip, options: opts("source_ip") },
{ id: "mac_address", label: "MAC Address", type: "select", value: filters.mac_address, options: opts("mac_address") },
];
const filteredData = all.filter((row: any) => {
if (filters.severity && filters.severity !== "All" && row.severity !== filters.severity) return false;
if (filters.event_type && filters.event_type !== "All" && row.event_type !== filters.event_type) return false;
if (filters.source_ip && filters.source_ip !== "All" && row.source_ip !== filters.source_ip) return false;
if (filters.mac_address && filters.mac_address !== "All" && row.mac_address !== filters.mac_address) return false;
if (dateFrom || dateTo) {
const d = row.timestamp ? new Date(row.timestamp).toISOString().slice(0, 10) : "";
if (dateFrom && d < dateFrom) return false;
if (dateTo && d > dateTo) return false;
}
return true;
});
const minTime = all.reduce((min: number, r: any) => {
if (r.timestamp) {
const time = new Date(r.timestamp).getTime();
if (isFinite(time) && time < min) return time;
}
return min;
}, Infinity);
const minDate = minTime !== Infinity ? new Date(minTime).toISOString().slice(0, 10) : "2024-01-01";
const getSeverityIcon = (severity: string) => {
switch (severity.toLowerCase()) {
case 'critical':
case 'high':
return <AlertCircle className="w-4 h-4 text-red-500" />;
case 'medium':
case 'warning':
return <AlertTriangle className="w-4 h-4 text-yellow-500" />;
default:
return <Info className="w-4 h-4 text-blue-500" />;
}
};
const columns: Column<typeof data[0]>[] = [
{ header: "#", accessor: (row, i) => i + 1, className: "w-[4%] text-center" },
{
header: "Timestamp (WIB)",
accessor: (row) => <TimestampCell timestamp={row.timestamp} showActiveStatus={false} />,
className: "w-[15%] text-center"
},
{
header: "Severity",
accessor: (row) => (
<div className="flex items-center gap-2 justify-center">
{getSeverityIcon(row.severity)}
<span className="capitalize">{row.severity}</span>
</div>
),
className: "w-[10%] text-center"
},
{ header: "Event Type", accessor: (row) => row.event_type, className: "w-[12%] text-center" },
{
header: "MAC Address",
accessor: (row) => row.mac_address ? (
<button
onClick={() => setSelectedMac(row.mac_address)}
className="font-mono text-xs text-emerald-400 hover:text-emerald-300 hover:underline text-center w-full focus:outline-none truncate"
>
{row.mac_address}
</button>
) : <span className="text-slate-500">-</span>,
className: "w-[14%] text-center"
},
{
header: "Source IP",
accessor: (row) => row.source_ip ? (
<button
onClick={() => setSelectedIp(row.source_ip)}
className="text-blue-400 hover:text-blue-300 hover:underline font-mono text-xs text-center w-full focus:outline-none truncate"
>
{row.source_ip}
</button>
) : <span className="text-slate-500">-</span>,
className: "w-[12%] text-center"
},
{
header: "Message",
accessor: (row) => {
const msg = row.message;
const mac = row.mac_address;
let content;
if (!mac || !msg.includes(mac)) {
content = <span className="text-slate-200 text-sm truncate">{msg}</span>;
} else {
// Highlight MAC in message with teal color
const parts = msg.split(mac);
content = (
<span className="text-slate-200 text-sm truncate">
{parts[0]}
<span className="font-mono text-emerald-400 font-medium">{mac}</span>
{parts[1]}
</span>
);
}
return (
<div title={msg} className="truncate w-full block text-center cursor-help">
{content}
</div>
);
},
className: "w-[33%] text-center"
}
];
return (
<div className="space-y-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-3 sm:justify-start sm:gap-4 w-full">
<h2 className="text-xl sm:text-2xl font-bold tracking-tight text-white flex items-center gap-2 sm:gap-3 truncate">
<span>Network Events</span>
{isLoading && <Loader2 className="w-4 h-4 sm:w-5 sm:h-5 animate-spin text-primary flex-shrink-0" />}
</h2>
{/* Mobile Actions: Inline next to Title */}
<div className="flex items-center gap-1.5 flex-shrink-0 sm:hidden">
<HelpTrigger pageId="events" />
</div>
</div>
</div>
{/* Laptop/Tablet Actions: Floats on the right */}
<div className="hidden sm:flex items-center gap-2 sm:gap-3 flex-shrink-0">
<HelpTrigger pageId="events" />
</div>
</div>
<UniversalFilters
filters={filterConfigs}
onChange={handleFilterChange}
showDateRange={true}
dateFrom={dateFrom}
dateTo={dateTo}
minDate={minDate}
onDateChange={(f, t) => { setDateFrom(f); setDateTo(t); }}
activeFilterCount={activeCount}
onReset={handleReset}
/>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Recent Events ({filteredData.length})</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={filteredData}
columns={columns}
searchPlaceholder="Search by event type, severity, MAC, IP, or message..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.event_type || "").toLowerCase().includes(q) ||
(row.severity || "").toLowerCase().includes(q) ||
(row.source_ip || "").toLowerCase().includes(q) ||
(row.mac_address || "").toLowerCase().includes(q) ||
(row.message || "").toLowerCase().includes(q)
);
}}
getRowClassName={(row) => {
return highlightId && row.id.toString() === highlightId
? "bg-yellow-500/10 border-l-2 border-yellow-500 animate-pulse font-semibold"
: "";
}}
pageSize={50}
/>
</CardContent>
</Card>
{selectedIp && (
<DeviceDetailModal ip={selectedIp} onClose={() => setSelectedIp(null)} />
)}
{selectedMac && (
<DeviceDetailModal ip="" mac={selectedMac} onClose={() => setSelectedMac(null)} />
)}
</div>
);
}
export default function EventsPage() {
return (
<Suspense fallback={<div className="flex items-center justify-center p-12"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>}>
<EventsContent />
</Suspense>
);
}