Files
2026-06-11 20:33:44 -05:00

126 lines
4.2 KiB
TypeScript

/**
* Network Traffic Dashboard — Main Tab
* Ball Studios <https://git.balls.studio>
*/
import React, { useEffect, useState, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import http from '@/api/http';
import LiveTrafficCards from './LiveTrafficCards';
import TrafficChart from './TrafficChart';
import PortTable from './PortTable';
import FlowGraph from './FlowGraph';
interface LiveStats {
rx_bytes: number;
tx_bytes: number;
recorded_at: string;
}
interface HistoryPoint {
rx_bytes: number;
tx_bytes: number;
recorded_at: string;
}
type Resolution = '5m' | '1h' | '1d';
export default function NetworkTab() {
const { id: serverUuid } = useParams<{ id: string }>();
const [live, setLive] = useState<LiveStats | null>(null);
const [history, setHistory] = useState<HistoryPoint[]>([]);
const [ports, setPorts] = useState<any[]>([]);
const [flows, setFlows] = useState<any[]>([]);
const [resolution, setRes] = useState<Resolution>('5m');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchLive = useCallback(async () => {
if (!serverUuid) return;
try {
const { data } = await http.get(`/api/client/servers/${serverUuid}/addons/network/live`);
setLive(data.data);
} catch {
// silently fail on live poll errors
}
}, [serverUuid]);
const fetchHistory = useCallback(async () => {
if (!serverUuid) return;
const { data } = await http.get(`/api/client/servers/${serverUuid}/addons/network/history`, {
params: { resolution },
});
setHistory(data.data ?? []);
}, [serverUuid, resolution]);
const fetchPorts = useCallback(async () => {
if (!serverUuid) return;
const { data } = await http.get(`/api/client/servers/${serverUuid}/addons/network/ports`);
setPorts(data.data ?? []);
}, [serverUuid]);
const fetchFlows = useCallback(async () => {
if (!serverUuid) return;
const { data } = await http.get(`/api/client/servers/${serverUuid}/addons/network/flows`);
setFlows(data.data ?? []);
}, [serverUuid]);
// Initial load
useEffect(() => {
setLoading(true);
setError(null);
Promise.all([fetchHistory(), fetchPorts(), fetchFlows()])
.catch(() => setError('Failed to load network data.'))
.finally(() => setLoading(false));
}, [fetchHistory, fetchPorts, fetchFlows]);
// Live polling — every 10s
useEffect(() => {
fetchLive();
const id = setInterval(fetchLive, 10_000);
return () => clearInterval(id);
}, [fetchLive]);
if (loading) return <div className='addon-loading'>Loading network data</div>;
if (error) return <div className='addon-error'>{error}</div>;
return (
<div className='addon-network'>
<h2 className='addon-section-title'>🌐 Network Traffic</h2>
{/* Live cards */}
<LiveTrafficCards live={live} />
{/* Historical chart */}
<div className='addon-card'>
<div className='addon-card-header'>
<span>Bandwidth Over Time</span>
<div className='addon-resolution-tabs'>
{(['5m', '1h', '1d'] as Resolution[]).map(r => (
<button
key={r}
className={`addon-tab ${resolution === r ? 'active' : ''}`}
onClick={() => setRes(r)}
>{r}</button>
))}
</div>
</div>
<TrafficChart data={history} />
</div>
{/* Port breakdown */}
<div className='addon-card'>
<div className='addon-card-header'>Top Ports</div>
<PortTable ports={ports} />
</div>
{/* Flow graph */}
<div className='addon-card'>
<div className='addon-card-header'>Container Traffic Flow</div>
<FlowGraph flows={flows} />
</div>
</div>
);
}