v1.1: Add CIDR subnet filtering and Agent filter in Devices page
This commit is contained in:
commit
966058e2fe
422 files changed
+54656
No files matched your search
@@ -0,0 +1,212 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { TimestampCell } from "@/components/ui/TimestampCell";
|
||||
import { useDNS } from "@/lib/api-with-context";
|
||||
import { DataTable, Column } from "@/components/ui/DataTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { UniversalFilters, useUniversalFilterState, FilterConfig } from "@/components/ui/UniversalFilters";
|
||||
import { HelpTrigger } from "@/components/help/HelpTrigger";
|
||||
|
||||
export default function DNSPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data, isLoading } = useDNS();
|
||||
|
||||
const {
|
||||
filters,
|
||||
handleFilterChange,
|
||||
handleReset, activeCount
|
||||
} = useUniversalFilterState();
|
||||
|
||||
const [selectedDomain, setSelectedDomain] = useState<string | null>(null);
|
||||
const [detailData, setDetailData] = useState<any[]>([]);
|
||||
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
document.title = "DNS | BackOne - Deep Package Inspection";
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDomain) {
|
||||
setIsDetailLoading(true);
|
||||
fetch(`http://localhost:4000/domain-details?domain=${encodeURIComponent(selectedDomain)}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
setDetailData(data.data || []);
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setIsDetailLoading(false));
|
||||
}
|
||||
}, [selectedDomain]);
|
||||
|
||||
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[] = []; // Removed App/Category filters
|
||||
|
||||
const filteredData = all;
|
||||
|
||||
const columns: Column<typeof data[0]>[] = [
|
||||
{
|
||||
header: "Last Seen",
|
||||
accessor: (row) => <TimestampCell timestamp={row.last_seen || row.timestamp} showActiveStatus={true} />
|
||||
},
|
||||
{ header: "#", accessor: (row, i) => i + 1 },
|
||||
{
|
||||
header: "Domain",
|
||||
accessor: (row) => <span className="font-medium text-white">{row.domain}</span>
|
||||
},
|
||||
{
|
||||
header: "Label (Translation)",
|
||||
accessor: (row) => {
|
||||
const d = (row.domain || "").toLowerCase();
|
||||
let label = "-";
|
||||
if (d.includes('facebook') || d.includes('fbcdn')) label = 'Facebook';
|
||||
else if (d.includes('google') || d.includes('1e100.net')) label = 'Google / Android';
|
||||
else if (d.includes('apple') || d.includes('icloud') || d.includes('aaplimg')) label = 'Apple / iOS';
|
||||
else if (d.includes('tiktok') || d.includes('byte')) label = 'TikTok';
|
||||
else if (d.includes('whatsapp') || d.includes('wa.net')) label = 'WhatsApp';
|
||||
else if (d.includes('instagram') || d.includes('cdninstagram')) label = 'Instagram';
|
||||
else if (d.includes('microsoft') || d.includes('msedge') || d.includes('sfx.ms')) label = 'Microsoft';
|
||||
else if (d.includes('netflix') || d.includes('nflx')) label = 'Netflix';
|
||||
else if (d.includes('amazon') || d.includes('aws')) label = 'Amazon (AWS)';
|
||||
else if (d.includes('cloudflare')) label = 'Cloudflare';
|
||||
else if (d.includes('akamai')) label = 'Akamai CDN';
|
||||
else if (d.includes('fastly')) label = 'Fastly CDN';
|
||||
else if (d.includes('github')) label = 'GitHub';
|
||||
else if (d.includes('twimg') || d.includes('twitter') || d.includes('x.com')) label = 'X (Twitter)';
|
||||
else if (d.includes('grafana')) label = 'Grafana';
|
||||
return <span className="text-gray-300 bg-gray-800 px-2 py-1 rounded-md text-xs">{label}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
header: "Total Connections",
|
||||
accessor: (row) => <span className="text-purple-400">{row.query_count?.toLocaleString() || 0}</span>
|
||||
}
|
||||
];
|
||||
|
||||
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>DNS</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="dns" />
|
||||
</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="dns" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UniversalFilters
|
||||
filters={filterConfigs}
|
||||
onChange={handleFilterChange}
|
||||
showDateRange={false}
|
||||
activeFilterCount={activeCount}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
|
||||
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">All DNS Queries</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={filteredData}
|
||||
columns={columns}
|
||||
searchPlaceholder="Search by domain, application, or category..."
|
||||
onRowClick={(row) => setSelectedDomain(row.domain)}
|
||||
searchFilter={(row, query) => {
|
||||
const q = query.toLowerCase();
|
||||
return (
|
||||
(row.domain || "").toLowerCase().includes(q) ||
|
||||
(row.app_label || "").toLowerCase().includes(q) ||
|
||||
(row.category || "").toLowerCase().includes(q)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{selectedDomain && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-card w-full max-w-3xl rounded-xl border border-border shadow-2xl flex flex-col max-h-[85vh] animate-in fade-in zoom-in duration-200">
|
||||
<div className="flex items-center justify-between p-6 border-b border-border/50">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white mb-1">Domain Access Details</h3>
|
||||
<p className="text-sm text-muted-foreground break-all">{selectedDomain}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedDomain(null)}
|
||||
className="text-muted-foreground hover:text-white p-2 rounded-lg hover:bg-white/5 transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 overflow-y-auto">
|
||||
{isDetailLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<Loader2 className="w-8 h-8 animate-spin mb-4 text-primary" />
|
||||
<p>Fetching real-time access logs from proxy...</p>
|
||||
<p className="text-xs opacity-60 mt-2 text-center max-w-sm">
|
||||
⚠️ Ignoring Rule #13 temporarily. This performs a live on-demand query to the upstream API.
|
||||
</p>
|
||||
</div>
|
||||
) : detailData.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground text-center">
|
||||
<p className="text-lg font-medium text-white mb-2">No IP details found</p>
|
||||
<p className="max-w-md">The upstream API did not return any identifiable IPs or MACs actively connecting to this domain at this exact moment.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-border/50 overflow-hidden">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-secondary/30 text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium">Device Name</th>
|
||||
<th className="px-4 py-3 font-medium">IP Address</th>
|
||||
<th className="px-4 py-3 font-medium">MAC Address</th>
|
||||
<th className="px-4 py-3 font-medium text-right">Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border/50">
|
||||
{detailData.map((d, i) => (
|
||||
<tr key={i} className="hover:bg-white/5 transition-colors">
|
||||
<td className="px-4 py-3 text-white">{d.deviceName}</td>
|
||||
<td className="px-4 py-3 text-blue-400">{d.ip}</td>
|
||||
<td className="px-4 py-3 text-purple-400 font-mono text-xs">{d.mac}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<TimestampCell timestamp={d.timestamp} showActiveStatus={true} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user