From 6e68b13c06cf30b0c5a9381835d71b6f0ed34e2c Mon Sep 17 00:00:00 2001 From: nessi Date: Tue, 24 Mar 2026 18:06:38 +0100 Subject: [PATCH] feat: add tunnel throughput distribution card with traffic summary stats and device bandwidth visualization Add traffic overview card to dashboard main grid showing total received, sent, and combined tunnel throughput. Display top 5 devices with horizontal bar chart visualization proportional to busiest device total. Calculate busiestDeviceTotal from topDevices max combined rx/tx bytes. Add dashboard-traffic-summary grid with three stat cards, dashboard-traffic-list with device rows showing name --- .../src/features/dashboard/DashboardPage.tsx | 60 +++++++++++++++- admin-web/src/styles/global.css | 71 ++++++++++++++++++- 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/admin-web/src/features/dashboard/DashboardPage.tsx b/admin-web/src/features/dashboard/DashboardPage.tsx index 9b19189..e4377d0 100644 --- a/admin-web/src/features/dashboard/DashboardPage.tsx +++ b/admin-web/src/features/dashboard/DashboardPage.tsx @@ -5,6 +5,7 @@ import { ArrowUpRight, FileClock, Network, + TrendingUp, Shield, Users } from "lucide-react"; @@ -94,6 +95,10 @@ export function DashboardPage() { .sort((left, right) => (right.rx_bytes + right.tx_bytes) - (left.rx_bytes + left.tx_bytes)) .slice(0, 5); + const busiestDeviceTotal = topDevices.length + ? Math.max(...topDevices.map((device) => (device.rx_bytes ?? 0) + (device.tx_bytes ?? 0))) + : 0; + const recentAudit = [...auditEvents] .sort((left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime()) .slice(0, 6); @@ -112,6 +117,7 @@ export function DashboardPage() { totalRX, totalTX, topDevices, + busiestDeviceTotal, recentAudit }; }, [auditQuery.data, devicesQuery.data, gatewaysQuery.data, policiesQuery.data, servicesQuery.data, usersQuery.data]); @@ -192,6 +198,56 @@ export function DashboardPage() {
+ +
+
+

Traffic overview

+

Tunnel throughput distribution

+
+
+ +
+
+
+
+ Received + {formatBytes(summary.totalRX)} +
+
+ Sent + {formatBytes(summary.totalTX)} +
+
+ Combined + {formatBytes(summary.totalRX + summary.totalTX)} +
+
+
+ {isLoading ?

Loading traffic overview...

: null} + {!isLoading && summary.topDevices.length === 0 ?

No tunnel traffic has been recorded yet.

: null} + {summary.topDevices.map((device) => { + const total = (device.rx_bytes ?? 0) + (device.tx_bytes ?? 0); + const width = summary.busiestDeviceTotal > 0 ? `${Math.max(8, Math.round((total / summary.busiestDeviceTotal) * 100))}%` : "0%"; + + return ( +
+
+ {device.name} + {device.assigned_ip ?? "No VPN IP"} ยท {formatBytes(total)} +
+
+ +
+
+ RX {formatBytes(device.rx_bytes ?? 0)} + TX {formatBytes(device.tx_bytes ?? 0)} +
+
+ ); + })} +
+
+
@@ -217,7 +273,9 @@ export function DashboardPage() { ))}
+
+
@@ -243,9 +301,7 @@ export function DashboardPage() { ))}
-
-
diff --git a/admin-web/src/styles/global.css b/admin-web/src/styles/global.css index 47a3998..5c161fb 100644 --- a/admin-web/src/styles/global.css +++ b/admin-web/src/styles/global.css @@ -524,7 +524,7 @@ button { .dashboard-main-grid { display: grid; - grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr); + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } @@ -570,6 +570,68 @@ button { color: var(--muted); } +.dashboard-traffic-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 18px; +} + +.dashboard-traffic-stat { + display: grid; + gap: 6px; + padding: 16px; + border-radius: 16px; + border: 1px solid var(--line); + background: rgba(8, 14, 30, 0.58); +} + +.dashboard-traffic-stat strong { + font-size: 1.2rem; +} + +.dashboard-traffic-list { + display: grid; + gap: 14px; +} + +.dashboard-traffic-row { + display: grid; + grid-template-columns: minmax(0, 220px) minmax(0, 1fr) auto; + align-items: center; + gap: 16px; + padding: 14px 16px; + border-radius: 16px; + border: 1px solid var(--line); + background: rgba(8, 14, 30, 0.58); +} + +.dashboard-traffic-copy, +.dashboard-traffic-split { + display: grid; + gap: 4px; +} + +.dashboard-traffic-copy span, +.dashboard-traffic-split small { + color: var(--muted); +} + +.dashboard-traffic-bar { + position: relative; + height: 10px; + border-radius: 999px; + overflow: hidden; + background: rgba(255, 255, 255, 0.06); +} + +.dashboard-traffic-bar-fill { + position: absolute; + inset: 0 auto 0 0; + border-radius: inherit; + background: linear-gradient(90deg, var(--accent) 0%, var(--accent-strong) 60%, var(--accent-violet) 100%); +} + .dashboard-posture-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -717,7 +779,12 @@ button { .dashboard-bottom-grid, .dashboard-stats-grid, .dashboard-posture-grid, - .dashboard-hero-metrics { + .dashboard-hero-metrics, + .dashboard-traffic-summary { + grid-template-columns: 1fr; + } + + .dashboard-traffic-row { grid-template-columns: 1fr; }