"use client"; import { useState, useEffect } from "react"; import { TimestampCell } from "@/components/ui/TimestampCell"; import { useFlows, useFlowsOptions } from "@/lib/api-with-context"; import { DataTable, Column } from "@/components/ui/DataTable"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { Loader2, Download, Search } from "lucide-react"; import { fmtBytes, formatAppLabel } from "@/lib/utils"; import { IpDetails } from "@/components/ui/IpDetails"; import { DeviceDetailModal } from "@/components/ui/DeviceDetailModal"; import { UniversalFilters, useUniversalFilterState, FilterConfig } from "@/components/ui/UniversalFilters"; import { explainAppOrPort } from "./explainFlow"; import { downloadCsv } from "@/components/ui/DataTableCsvHelper"; import { HelpTrigger } from "@/components/help/HelpTrigger"; export default function FlowsPage() { const [mounted, setMounted] = useState(false); const [selectedIp, setSelectedIp] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [ipSearch, setIpSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); const { filters, dateFrom, dateTo, handleFilterChange, setDateFrom, setDateTo, handleReset, activeCount } = useUniversalFilterState(); // Debounce search query to prevent database query spamming useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(ipSearch); }, 300); return () => clearTimeout(timer); }, [ipSearch]); // Reset page to 1 when filters, date picker, or search text changes useEffect(() => { setCurrentPage(1); }, [filters, dateFrom, dateTo, debouncedSearch]); const { data: optionsData } = useFlowsOptions(); const { data: flows = [], total = 0, isLoading } = useFlows(currentPage, 50, { ...filters, dateFrom, dateTo, search: debouncedSearch }); useEffect(() => { setMounted(true); document.title = "Flows | BackOne - Deep Package Inspection"; }, []); if (!mounted) return null; const opts = (key: string) => { if (!optionsData) return []; const mapping: Record = { protocol: optionsData.protocols, src_ip: optionsData.srcIps, dst_ip: optionsData.dstIps, dst_port: optionsData.dstPorts, app_label: optionsData.apps, domain: optionsData.domains }; return mapping[key] || []; }; const filterConfigs: FilterConfig[] = [ { id: "protocol", label: "Protocol", type: "select", value: filters.protocol, options: opts("protocol"), formatter: formatAppLabel }, { id: "src_ip", label: "Source IP", type: "select", value: filters.src_ip, options: opts("src_ip") }, { id: "dst_ip", label: "Dest IP", type: "select", value: filters.dst_ip, options: opts("dst_ip") }, { id: "dst_port", label: "Dest Port", type: "select", value: filters.dst_port, options: opts("dst_port") }, { id: "app", label: "App", type: "select", value: filters.app, options: opts("app_label"), formatter: formatAppLabel }, { id: "domain", label: "Domain", type: "select", value: filters.domain, options: opts("domain") }, { id: "sort_download", label: "Sort Download", type: "select", value: filters.sort_download, options: ["Descending", "Ascending"] }, { id: "sort_upload", label: "Sort Upload", type: "select", value: filters.sort_upload, options: ["Descending", "Ascending"] }, ]; const minDate = new Date(Date.now() - 30 * 24 * 3600 * 1000).toISOString().slice(0, 10); const handleExport = () => { const headers = ["Flow ID", "Src IP", "Dst IP", "Dst Port", "Protocol", "App / Domain", "Download (bytes)", "Upload (bytes)"]; const rows = [headers, ...flows.map(r => [r.flow_id || "", r.src_ip || "", r.dst_ip || "", r.dst_port ?? "", r.protocol || "", r.app_label || r.domain || `Port ${r.dst_port}`, r.bytes_download ?? 0, r.bytes_upload ?? 0])]; downloadCsv(`flows-${new Date().toISOString().slice(0,10)}.csv`, rows); }; const columns: Column[] = [ { header: "Last Seen", accessor: (row) => ( ), className: "w-[14%] text-center", tooltip: (row) => new Date(row.last_seen).toLocaleString() }, { header: "#", accessor: (row, i) => i + 1, className: "w-[4%] text-center" }, { header: "Flow ID", accessor: (row) => row.flow_id, className: "w-[11%] text-center font-mono text-xs truncate" }, { header: "Src IP", accessor: (row) => (
{row.src_label && ( {row.src_label} )}
), className: "w-[11%] text-center", tooltip: (row) => row.src_label ? `${row.src_ip} (${row.src_label})` : row.src_ip }, { header: "Dst IP", accessor: (row) => (
), className: "w-[13%] text-center", tooltip: (row) => row.dst_ip }, { header: "Dst Port", accessor: (row) => row.dst_port, className: "w-[7%] text-center font-mono text-xs" }, { header: "Protocol", accessor: (row) => row.protocol, className: "w-[7%] text-center text-xs" }, { header: "App / Domain", accessor: (row) => { const exp = explainAppOrPort(row.app_label, row.domain, row.dst_port); return (
{exp.main} {exp.sub && ( {exp.sub} )}
); }, className: "w-[18%] text-center", tooltip: (row) => { const exp = explainAppOrPort(row.app_label, row.domain, row.dst_port); return exp.sub ? `${exp.main} (${exp.sub})` : exp.main; } }, { header: "Download", accessor: (row) => {fmtBytes(row.bytes_download)}, className: "w-[8%] text-center", tooltip: (row) => `${fmtBytes(row.bytes_download)} (${(row.bytes_download || 0).toLocaleString()} bytes)` }, { header: "Upload", accessor: (row) => {fmtBytes(row.bytes_upload)}, className: "w-[7%] text-center", tooltip: (row) => `${fmtBytes(row.bytes_upload)} (${(row.bytes_upload || 0).toLocaleString()} bytes)` } ]; return (

Flows {isLoading && }

{ if (id === 'sort_download') { handleFilterChange('sort_download', val); handleFilterChange('sort_upload', 'All'); } else if (id === 'sort_upload') { handleFilterChange('sort_upload', val); handleFilterChange('sort_download', 'All'); } else { handleFilterChange(id, val); } }} showDateRange={true} dateFrom={dateFrom} dateTo={dateTo} minDate={minDate} onDateChange={(f, t) => { setDateFrom(f); setDateTo(t); }} activeFilterCount={activeCount} onReset={handleReset} />
Active Flows ({total.toLocaleString()})
setIpSearch(e.target.value)} className="w-[240px] rounded-lg border border-border bg-card/30 backdrop-blur-sm pl-9 pr-4 py-1.5 text-sm text-white placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:bg-card/50 transition-all shadow-sm" />
{selectedIp && ( setSelectedIp(null)} /> )}
); }