feat(site): implement dynamic site selection dropdown in sidebar and enable multi-site monitoring

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-13 16:57:45 +07:00
1 parent be6bc14190
commit 4882108068
125 files changed
+5666 -4305

No files matched your search

+108 -10
View File
@@ -1,13 +1,15 @@
"use client";
import { useState, useEffect } from "react";
import { TimestampCell } from "@/components/ui/TimestampCell";
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 { 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";
function explainAppOrPort(appLabel: string | null, domain: string | null, port: number) {
const label = appLabel || domain || "";
@@ -19,7 +21,7 @@ function explainAppOrPort(appLabel: string | null, domain: string | null, port:
"3306": "MySQL/MariaDB Database Server",
"5432": "PostgreSQL Database Server",
"1521": "Oracle Database Server",
"27017": "MongoDB Database Server",
"27017": "Database Server",
"6379": "Redis Key-Value Cache",
"80": "Unencrypted Web Traffic (HTTP)",
"443": "Encrypted Web Traffic (HTTPS/TLS)",
@@ -60,6 +62,12 @@ export default function FlowsPage() {
const [mounted, setMounted] = useState(false);
const [selectedIp, setSelectedIp] = useState<string | null>(null);
const { data, isLoading } = useFlows();
const {
filters, dateFrom, dateTo,
handleFilterChange, setDateFrom, setDateTo,
handleReset, activeCount
} = useUniversalFilterState();
useEffect(() => {
setMounted(true);
@@ -67,7 +75,79 @@ export default function FlowsPage() {
if (!mounted) return null;
const all = data || [];
// Extract unique options
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: "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 filteredData = all.filter((row: any) => {
if (filters.protocol && filters.protocol !== "All" && row.protocol !== filters.protocol) return false;
if (filters.src_ip && filters.src_ip !== "All" && row.src_ip !== filters.src_ip) return false;
if (filters.dst_ip && filters.dst_ip !== "All" && row.dst_ip !== filters.dst_ip) return false;
if (filters.dst_port && filters.dst_port !== "All" && String(row.dst_port) !== filters.dst_port) return false;
if (filters.app && filters.app !== "All" && row.app_label !== filters.app) return false;
if (filters.domain && filters.domain !== "All" && row.domain !== filters.domain) return false;
if (dateFrom || dateTo) {
const d = row.last_seen ? new Date(row.last_seen).toISOString().slice(0, 10) : "";
if (dateFrom && d < dateFrom) return false;
if (dateTo && d > dateTo) return false;
}
return true;
});
let processedData = [...filteredData];
const sortUpload = filters.sort_upload || "All";
const sortDownload = filters.sort_download || "All";
if (sortUpload !== "All") {
processedData.sort((a, b) => sortUpload === "Descending" ? (b.bytes_upload ?? 0) - (a.bytes_upload ?? 0) : (a.bytes_upload ?? 0) - (b.bytes_upload ?? 0));
} else if (sortDownload !== "All") {
processedData.sort((a, b) => sortDownload === "Descending" ? (b.bytes_download ?? 0) - (a.bytes_download ?? 0) : (a.bytes_download ?? 0) - (b.bytes_download ?? 0));
} else {
processedData.sort((a, b) => {
const timeA = a.last_seen ? new Date(a.last_seen).getTime() : 0;
const timeB = b.last_seen ? new Date(b.last_seen).getTime() : 0;
return timeB - timeA;
});
}
const minTime = all.reduce((min: number, r: any) => {
if (r.last_seen) {
const time = new Date(r.last_seen).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 columns: Column<typeof data[0]>[] = [
{
header: "Last Seen",
accessor: (row) => (
<TimestampCell
timestamp={row.last_seen}
firstSeen={row.first_seen}
showActiveStatus={true}
/>
),
className: "w-[120px]"
},
{ header: "#", accessor: (row, i) => i + 1 },
{ header: "Flow ID", accessor: (row) => row.flow_id },
{
@@ -117,15 +197,11 @@ export default function FlowsPage() {
},
{
header: "Download",
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>,
sortable: true,
sortAccessor: (row) => row.bytes_download ?? 0
accessor: (row) => <span className="text-blue-400">{fmtBytes(row.bytes_download)}</span>
},
{
header: "Upload",
accessor: (row) => <span className="text-green-500">{fmtBytes(row.bytes_upload)}</span>,
sortable: true,
sortAccessor: (row) => row.bytes_upload ?? 0
accessor: (row) => <span className="text-green-500">{fmtBytes(row.bytes_upload)}</span>
}
];
@@ -137,14 +213,36 @@ export default function FlowsPage() {
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
</div>
<UniversalFilters
filters={filterConfigs}
onChange={(id, val) => {
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}
/>
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="text-white">Active Flows ({data.length})</CardTitle>
<CardTitle className="text-white">Active Flows ({processedData.length})</CardTitle>
</CardHeader>
<CardContent>
<DataTable
data={data}
data={processedData}
columns={columns}
searchPlaceholder="Search flows by IP, App, or Domain..."
searchFilter={(row, query) => {