126 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|