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; }