diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx new file mode 100644 index 000000000..497fa1410 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -0,0 +1,59 @@ +// NetworkCharts.jsx +import { Grid, Card, CardContent, Typography } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import AreaChart from "../../../../../Components/Charts/AreaChart"; +import { TzTick } from '../../../../../Components/Charts/Utils/chartUtils'; + +const BytesTick = ({ x, y, payload }) => { + const value = payload.value; + const label = + value >= 1024 ** 3 + ? `${(value / 1024 ** 3).toFixed(2)} GB` + : value >= 1024 ** 2 + ? `${(value / 1024 ** 2).toFixed(2)} MB` + : `${(value / 1024).toFixed(2)} KB`; + + return {label}; +}; + +const NetworkCharts = ({ eth0Data, dateRange }) => { + const theme = useTheme(); + const textColor = theme.palette.primary.contrastTextTertiary; + + const charts = [ + { title: "eth0 Bytes/sec", key: "bytesPerSec", color: theme.palette.info.main, yTick: }, + { title: "eth0 Packets/sec", key: "packetsPerSec", color: theme.palette.success.main }, + { title: "eth0 Errors", key: "errors", color: theme.palette.error.main }, + { title: "eth0 Drops", key: "drops", color: theme.palette.warning.main } + ]; + + return ( + + {charts.map((chart) => ( + + + + + {chart.title} + + } + strokeColor={chart.color} + gradient + gradientStartColor={chart.color} + gradientEndColor="#fff" + height={200} + /> + + + + ))} + + ); +}; + +export default NetworkCharts; \ No newline at end of file diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx new file mode 100644 index 000000000..abe67fa8e --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -0,0 +1,82 @@ +// NetworkStatBoxes.jsx +import DataUsageIcon from "@mui/icons-material/DataUsage"; +import NetworkCheckIcon from "@mui/icons-material/NetworkCheck"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; +import { Typography } from "@mui/material"; + +const INTERFACE_LABELS = { + en0: "Ethernet/Wi-Fi (Primary)", + wlan0: "Wi-Fi (Secondary)", +}; + +function formatBytes(bytes) { + if (bytes === 0 || bytes == null) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; +} + +// Format numbers with commas +function formatNumber(num) { + return num != null ? num.toLocaleString() : "0"; +} + +const NetworkStatBoxes = ({ shouldRender, net }) => { + const filtered = net?.filter( + (iface) => iface.name === "en0" || iface.name === "wlan0" + ) || []; + + if (!net?.length) { + return No network stats available.; + } + + return ( + + {filtered.map((iface) => ( + <> + + + + + + + + ))} + + ); +}; + +export default NetworkStatBoxes; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx new file mode 100644 index 000000000..ecb62e769 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -0,0 +1,100 @@ +import NetworkStatBoxes from "./NetworkStatBoxes"; +import NetworkCharts from "./NetworkCharts"; +import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; + +function filterByDateRange(data, dateRange) { + if (!Array.isArray(data)) return []; + const now = Date.now(); + let cutoff; + switch (dateRange) { + case "recent": + cutoff = now - 2 * 60 * 60 * 1000; // last 2 hours + break; + case "day": + cutoff = now - 24 * 60 * 60 * 1000; // last 24 hours + break; + case "week": + cutoff = now - 7 * 24 * 60 * 60 * 1000; // last 7 days + break; + case "month": + cutoff = now - 30 * 24 * 60 * 60 * 1000; // last 30 days + break; + default: + cutoff = 0; + } + return data.filter((d) => new Date(d.time).getTime() >= cutoff); +} + +const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { + const eth0Data = getEth0TimeSeries(checks); + const xAxisFormatter = getXAxisFormatter(checks); + const filteredEth0Data = filterByDateRange(eth0Data, dateRange); + + return ( + <> + + + + + ); +}; + +export default Network; + +/* ---------- Helper functions ---------- */ +function getEth0TimeSeries(checks) { + const sorted = [...(checks || [])].sort((a, b) => new Date(a._id) - new Date(b._id)); + const series = []; + let prev = null; + + for (const check of sorted) { + const eth = (check.net || []).find((iface) => iface.name === "en0"); + if (!eth) { + prev = check; + continue; + } + if (prev) { + const prevEth = (prev.net || []).find((iface) => iface.name === "en0"); + const t1 = new Date(check._id); + const t0 = new Date(prev._id); + if (!prevEth || isNaN(t1) || isNaN(t0)) { + prev = check; + continue; + } + const dt = (t1 - t0) / 1000; + if (dt > 0) { + series.push({ + time: check._id, + bytesPerSec: (eth.bytesSent - prevEth.bytesSent) / dt, + packetsPerSec: (eth.packetsSent - prevEth.packetsSent) / dt, + errors: (eth.errIn ?? 0) + (eth.errOut ?? 0), + drops: (eth.dropIn ?? 0) + (eth.dropOut ?? 0), + }); + } + } + prev = check; + } + + return series; +} + +function getXAxisFormatter(checks) { + if (!checks || checks.length === 0) return (val) => val; + const sorted = [...checks].sort((a, b) => new Date(a._id) - new Date(b._id)); + const first = new Date(sorted[0]._id); + const last = new Date(sorted[sorted.length - 1]._id); + const diffDays = (last - first) / (1000 * 60 * 60 * 24); + + return diffDays > 2 + ? (val) => new Date(val).toLocaleDateString(undefined, { month: "short", day: "numeric" }) + : (val) => + new Date(val).toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); +} diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx new file mode 100644 index 000000000..52fcfad87 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx @@ -0,0 +1,56 @@ +import { + Card, + CardContent, + Skeleton, + Table, + TableHead, + TableRow, + TableCell, + TableBody, +} from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + + + Name + Bytes Sent + Bytes Received + Packets Sent + Packets Received + Errors In + Errors Out + Drops In + Drops Out + + + + {Array.from({ length: 5 }).map((_, idx) => ( + + {Array.from({ length: 9 }).map((__, colIdx) => ( + + + + ))} + + ))} + +
+
+
+ ); +}; + +export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index 30840851f..78b78fa78 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -1,5 +1,5 @@ // Components -import { Stack, Typography } from "@mui/material"; +import { Stack, Typography, Tab } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; @@ -7,6 +7,9 @@ import StatusBoxes from "./Components/StatusBoxes"; import GaugeBoxes from "./Components/GaugeBoxes"; import AreaChartBoxes from "./Components/AreaChartBoxes"; import GenericFallback from "../../../Components/GenericFallback"; +import NetworkStats from "./Components/NetworkStats"; +import CustomTabList from "../../../Components/Tab"; +import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; @@ -22,11 +25,11 @@ const BREADCRUMBS = [ { name: "details", path: "" }, ]; const InfrastructureDetails = () => { - // Redux state // Local state const [dateRange, setDateRange] = useState("recent"); const [trigger, setTrigger] = useState(false); + const [tab, setTab] = useState("details"); // Utils const theme = useTheme(); @@ -87,23 +90,51 @@ const InfrastructureDetails = () => { monitor={monitor} triggerUpdate={triggerUpdate} /> - - - - + + setTab(v)} + > + + + + {tab === "details" && ( + <> + + + + + + )} + {tab === "network" && ( + + )} + ); };