"use client"; import { useState, useEffect } from "react"; import { useFlows } 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 { fmtBytes } from "@/lib/utils"; import { IpDetails } from "@/components/ui/IpDetails"; import { DeviceDetailModal } from "@/components/ui/DeviceDetailModal"; function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) { const label = appLabel || domain || ""; const portStr = String(port); const matches: Record = { "1433": "MSSQL Database Server", "1434": "MSSQL Monitor Server", "3306": "MySQL/MariaDB Database Server", "5432": "PostgreSQL Database Server", "1521": "Oracle Database Server", "27017": "MongoDB Database Server", "6379": "Redis Key-Value Cache", "80": "Unencrypted Web Traffic (HTTP)", "443": "Encrypted Web Traffic (HTTPS/TLS)", "22": "SSH Remote Management / SFTP", "21": "FTP File Upload/Download (Unencrypted)", "23": "Telnet Command Shell (Insecure)", "25": "SMTP Mail Delivery", "587": "Secure SMTP Mail Delivery", "110": "POP3 Mail Storage Retrieval", "993": "Secure IMAP Mail Service", "53": "DNS Server Query (Domain Lookup)", "123": "NTP Network Clock Synchronizer", "161": "SNMP Network Device Monitoring", "3389": "RDP Remote Desktop Management", "445": "SMB Windows File Sharing", "137": "NetBIOS Name Service", "138": "NetBIOS Datagram Service", "139": "NetBIOS Session Service", "1812": "RADIUS Authentication Server", "1813": "RADIUS Accounting Server" }; let mainLabel = label; let subLabel = matches[portStr] || (label.toLowerCase().includes("tls") || label.toLowerCase().includes("https") ? "Encrypted Connection (SSL/TLS)" : ""); if (appLabel && appLabel.includes("(") && domain && domain.startsWith("Port ")) { mainLabel = appLabel; subLabel = domain; } return { main: mainLabel || `Port ${port}`, sub: subLabel }; } export default function FlowsPage() { const [mounted, setMounted] = useState(false); const [selectedIp, setSelectedIp] = useState(null); const { data, isLoading } = useFlows(); useEffect(() => { setMounted(true); }, []); if (!mounted) return null; const columns: Column[] = [ { header: "#", accessor: (row, i) => i + 1 }, { header: "Flow ID", accessor: (row) => row.flow_id }, { header: "Src IP", accessor: (row) => ( ) }, { header: "Dst IP", accessor: (row) => (
) }, { header: "Dst Port", accessor: (row) => row.dst_port }, { header: "Protocol", accessor: (row) => row.protocol }, { header: "App / Domain", accessor: (row) => { const exp = explainAppOrPort(row.app_label, row.domain, row.dst_port); return (
{exp.main} {exp.sub && ( {exp.sub} )}
); } }, { header: "Download", accessor: (row) => {fmtBytes(row.bytes_download)}, sortable: true, sortAccessor: (row) => row.bytes_download ?? 0 }, { header: "Upload", accessor: (row) => {fmtBytes(row.bytes_upload)}, sortable: true, sortAccessor: (row) => row.bytes_upload ?? 0 } ]; return (

Active Flows {isLoading && }

Active Flows ({data.length}) { const q = query.toLowerCase(); return ( (row.src_ip || "").toLowerCase().includes(q) || (row.dst_ip || "").toLowerCase().includes(q) || (row.app_label || "").toLowerCase().includes(q) || (row.domain || "").toLowerCase().includes(q) || (row.protocol || "").toLowerCase().includes(q) ); }} csvExport={{ filename: `flows-${new Date().toISOString().slice(0,10)}.csv`, headers: ["Flow ID", "Src IP", "Dst IP", "Dst Port", "Protocol", "App / Domain", "Download (bytes)", "Upload (bytes)"], rowSerializer: (row) => [ row.flow_id || "", row.src_ip || "", row.dst_ip || "", row.dst_port ?? "", row.protocol || "", row.app_label || row.domain || `Port ${row.dst_port}`, row.bytes_download ?? 0, row.bytes_upload ?? 0, ], }} /> {selectedIp && ( setSelectedIp(null)} /> )}
); }