Merge pull request #2741 from Owaiseimdad/Network-tab-implementation-FE

Network tab implementation fe
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-08-14 09:22:09 -07:00
commit e32e72de7a
10 files changed
+899 -75

No files matched your search

@@ -88,7 +88,51 @@ PercentTick.propTypes = {
*/
const getFormattedPercentage = (value) => {
if (typeof value !== "number") return value;
return `${(value * 100).toFixed(2)}.%`;
return `${(value * 100).toFixed(2)}%`;
};
/**
* Custom tick component for rendering network bytes per second.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
*/
export const NetworkTick = ({ x, y, payload, index, formatter }) => {
const theme = useTheme();
if (index === 0) return null;
if (formatter === undefined) {
formatter = (value, space = false) => {
if (typeof value !== "number") return value;
// need to add space between value and unit
return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`;
};
}
return (
<Text
x={x - 20}
y={y}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatter(payload?.value, true)}
</Text>
);
};
NetworkTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
formatter: PropTypes.func,
};
/**
@@ -112,6 +156,7 @@ export const InfrastructureTooltip = ({
yLabel,
dotColor,
dateRange,
formatter = getFormattedPercentage,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
@@ -166,8 +211,8 @@ export const InfrastructureTooltip = ({
sx={{ opacity: 0.8 }}
>
{yIdx >= 0
? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}`
: `${yLabel} ${getFormattedPercentage(payload[0].payload[yKey])}`}
? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}`
: `${yLabel} ${formatter(payload[0].payload[yKey])}`}
</Typography>
</Stack>
</Box>
@@ -0,0 +1,132 @@
import PropTypes from "prop-types";
import { Stack, Typography } from "@mui/material";
import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart";
import {
TzTick,
InfrastructureTooltip,
NetworkTick,
} from "../../../../../Components/Charts/Utils/chartUtils";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
const NetworkCharts = ({ ethernetData, dateRange }) => {
const theme = useTheme();
const { t } = useTranslation();
const { formatBytesPerSecondString, formatPacketsPerSecondString } = useHardwareUtils();
if (!ethernetData?.length) {
return <Typography>{t("noNetworkStatsAvailable")}</Typography>;
}
const configs = [
{
type: "network-bytes",
data: ethernetData,
dataKeys: ["bytesPerSec"],
heading: t("dataReceived"),
strokeColor: theme.palette.info.main,
gradientStartColor: theme.palette.info.main,
yLabel: t("rate"),
xTick: <TzTick dateRange={dateRange} />,
yTick: <NetworkTick formatter={formatBytesPerSecondString} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.info.main}
yKey={"bytesPerSec"}
yLabel={t("dataRate") + ": "}
dateRange={dateRange}
formatter={formatBytesPerSecondString}
/>
),
},
{
type: "network-packets",
data: ethernetData,
dataKeys: ["packetsPerSec"],
heading: t("packetsReceivedRate"),
strokeColor: theme.palette.success.main,
gradientStartColor: theme.palette.success.main,
yLabel: t("rate"),
xTick: <TzTick dateRange={dateRange} />,
yTick: <NetworkTick formatter={formatPacketsPerSecondString} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.success.main}
yKey={"packetsPerSec"}
yLabel={t("packetsPerSecond") + ": "}
dateRange={dateRange}
formatter={formatPacketsPerSecondString}
/>
),
},
{
type: "network-errors",
data: ethernetData,
dataKeys: ["errors"],
heading: t("networkErrors"),
strokeColor: theme.palette.error.main,
gradientStartColor: theme.palette.error.main,
yLabel: t("errors"),
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.error.main}
yKey={"errors"}
yLabel={t("errors") + ": "}
dateRange={dateRange}
formatter={(value) => Math.round(value).toLocaleString()}
/>
),
},
{
type: "network-drops",
data: ethernetData,
dataKeys: ["drops"],
heading: t("networkDrops"),
strokeColor: theme.palette.warning.main,
gradientStartColor: theme.palette.warning.main,
yLabel: t("drops"),
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.warning.main}
yKey={"drops"}
yLabel={t("drops") + ": "}
dateRange={dateRange}
formatter={(value) => Math.round(value).toLocaleString()}
/>
),
},
];
return (
<Stack
direction={"row"}
gap={theme.spacing(8)}
flexWrap="wrap"
sx={{
"& > *": {
flexBasis: `calc(50% - ${theme.spacing(8)})`,
maxWidth: `calc(50% - ${theme.spacing(8)})`,
},
}}
>
{configs.map((config) => (
<InfraAreaChart
key={config.type}
config={config}
/>
))}
</Stack>
);
};
NetworkCharts.propTypes = {
ethernetData: PropTypes.array.isRequired,
dateRange: PropTypes.string.isRequired,
};
export default NetworkCharts;
@@ -0,0 +1,81 @@
import PropTypes from "prop-types";
import StatusBoxes from "../../../../../Components/StatusBoxes";
import StatBox from "../../../../../Components/StatBox";
import { Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
function formatNumber(num) {
return num != null ? num.toLocaleString() : "0";
}
const NetworkStatBoxes = ({ shouldRender, net, ifaceName }) => {
const { t } = useTranslation();
const { formatBytes } = useHardwareUtils();
const filtered = net?.filter((iface) => iface.name === ifaceName) || [];
if (!net?.length) {
return <Typography>{t("noNetworkStatsAvailable")}</Typography>;
}
return (
<StatusBoxes
shouldRender={shouldRender}
flexWrap="wrap"
>
{filtered
.map((iface) => [
<StatBox
key={`${iface.name}-bytes-sent`}
heading={t("bytesSent")}
subHeading={formatBytes(iface.bytes_sent)}
/>,
<StatBox
key={`${iface.name}-bytes-recv`}
heading={t("bytesReceived")}
subHeading={formatBytes(iface.bytes_recv)}
/>,
<StatBox
key={`${iface.name}-packets-sent`}
heading={t("packetsSent")}
subHeading={formatNumber(iface.packets_sent)}
/>,
<StatBox
key={`${iface.name}-packets-recv`}
heading={t("packetsReceived")}
subHeading={formatNumber(iface.packets_recv)}
/>,
<StatBox
key={`${iface.name}-err-in`}
heading={t("errorsIn")}
subHeading={formatNumber(iface.err_in)}
/>,
<StatBox
key={`${iface.name}-err-out`}
heading={t("errorsOut")}
subHeading={formatNumber(iface.err_out)}
/>,
])
.flat()}
</StatusBoxes>
);
};
NetworkStatBoxes.propTypes = {
shouldRender: PropTypes.bool.isRequired,
net: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
bytes_sent: PropTypes.number,
bytes_recv: PropTypes.number,
packets_sent: PropTypes.number,
packets_recv: PropTypes.number,
err_in: PropTypes.number,
err_out: PropTypes.number,
})
),
ifaceName: PropTypes.string.isRequired,
};
export default NetworkStatBoxes;
@@ -0,0 +1,110 @@
import PropTypes from "prop-types";
import { useState, useEffect } from "react";
import { FormControl, InputLabel, Select, MenuItem, Box } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import NetworkStatBoxes from "./NetworkStatBoxes";
import NetworkCharts from "./NetworkCharts";
import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader";
const getAvailableInterfaces = (net) => {
return (net || []).map((iface) => iface.name).filter(Boolean);
};
const getNetworkInterfaceData = (checks, ifaceName) => {
if (!ifaceName) return [];
// Transform backend data structure for the selected interface
// Backend already calculates deltas, we just reshape the data
return (checks || [])
.map((check) => {
const networkInterface = (check.net || []).find(
(iface) => iface.name === ifaceName
);
if (!networkInterface) return null;
return {
_id: check._id,
bytesPerSec: networkInterface.deltaBytesRecv,
packetsPerSec: networkInterface.deltaPacketsRecv,
errors: networkInterface.deltaErrOut ?? 0,
drops: networkInterface.deltaDropOut ?? 0,
};
})
.filter(Boolean);
};
const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => {
const { t } = useTranslation();
const theme = useTheme();
const availableInterfaces = getAvailableInterfaces(net);
const [selectedInterface, setSelectedInterface] = useState("");
// Set default interface when data loads
useEffect(() => {
if (availableInterfaces.length > 0 && !selectedInterface) {
setSelectedInterface(availableInterfaces[0]);
}
}, [availableInterfaces, selectedInterface]);
const ethernetData = getNetworkInterfaceData(checks, selectedInterface);
return (
<>
<NetworkStatBoxes
shouldRender={!isLoading}
net={net}
ifaceName={selectedInterface}
/>
<Box
display="flex"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
>
{availableInterfaces.length > 0 && (
<FormControl
variant="outlined"
size="small"
sx={{ minWidth: 200 }}
>
<InputLabel>{t("networkInterface")}</InputLabel>
<Select
value={selectedInterface}
onChange={(e) => setSelectedInterface(e.target.value)}
label={t("networkInterface")}
>
{availableInterfaces.map((interfaceName) => (
<MenuItem
key={interfaceName}
value={interfaceName}
>
{interfaceName}
</MenuItem>
))}
</Select>
</FormControl>
)}
<MonitorTimeFrameHeader
isLoading={isLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
</Box>
<NetworkCharts
ethernetData={ethernetData}
dateRange={dateRange}
/>
</>
);
};
Network.propTypes = {
net: PropTypes.array,
checks: PropTypes.array,
isLoading: PropTypes.bool.isRequired,
dateRange: PropTypes.string.isRequired,
setDateRange: PropTypes.func.isRequired,
};
export default Network;
@@ -0,0 +1,56 @@
import {
Card,
CardContent,
Skeleton,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
} from "@mui/material";
const SkeletonLayout = () => {
return (
<Card>
<CardContent>
<Skeleton
variant="text"
width={180}
height={32}
/>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Bytes Sent</TableCell>
<TableCell>Bytes Received</TableCell>
<TableCell>Packets Sent</TableCell>
<TableCell>Packets Received</TableCell>
<TableCell>Errors In</TableCell>
<TableCell>Errors Out</TableCell>
<TableCell>Drops In</TableCell>
<TableCell>Drops Out</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Array.from({ length: 5 }).map((_, idx) => (
<TableRow key={idx}>
{Array.from({ length: 9 }).map((__, colIdx) => (
<TableCell key={colIdx}>
<Skeleton
variant="text"
width={80}
height={24}
/>
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};
export default SkeletonLayout;
@@ -57,7 +57,7 @@ const useHardwareUtils = () => {
if (GB >= 1) {
return (
<>
{Number(GB.toFixed(0))}
{Number(GB.toFixed(2))}
{space ? " " : ""}
<Typography component="span">{t("gb")}</Typography>
</>
@@ -65,7 +65,7 @@ const useHardwareUtils = () => {
} else {
return (
<>
{Number(MB.toFixed(0))}
{Number(MB.toFixed(2))}
{space ? " " : ""}
<Typography component="span">{t("mb")}</Typography>
</>
@@ -73,6 +73,53 @@ const useHardwareUtils = () => {
}
};
const formatBytesPerSecondString = (bytesPerSec, space = false) => {
if (
bytesPerSec === undefined ||
bytesPerSec === null ||
typeof bytesPerSec !== "number" ||
bytesPerSec === 0
) {
return `0${space ? " " : ""}B/s`;
}
const GB = bytesPerSec / (1024 * 1024 * 1024);
const MB = bytesPerSec / (1024 * 1024);
const KB = bytesPerSec / 1024;
if (GB >= 1) {
return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`;
} else if (MB >= 1) {
return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`;
} else if (KB >= 1) {
return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`;
} else {
return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`;
}
};
const formatPacketsPerSecondString = (packetsPerSec, space = false) => {
if (
packetsPerSec === undefined ||
packetsPerSec === null ||
typeof packetsPerSec !== "number" ||
packetsPerSec === 0
) {
return `0${space ? " " : ""}pps`;
}
const M = packetsPerSec / (1000 * 1000);
const K = packetsPerSec / 1000;
if (M >= 1) {
return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`;
} else if (K >= 1) {
return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`;
} else {
return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`;
}
};
/**
* Converts a decimal value to a percentage
*
@@ -134,6 +181,8 @@ const useHardwareUtils = () => {
decimalToPercentage,
buildTemps,
getDimensions,
formatBytesPerSecondString,
formatPacketsPerSecondString,
};
};
@@ -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,10 @@ 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 +89,51 @@ const InfrastructureDetails = () => {
monitor={monitor}
triggerUpdate={triggerUpdate}
/>
<StatusBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
<GaugeBoxes
isLoading={isLoading}
monitor={monitor}
/>
<MonitorTimeFrameHeader
isLoading={isLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<AreaChartBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
<TabContext value={tab}>
<CustomTabList
value={tab}
onChange={(e, v) => setTab(v)}
>
<Tab
label={t("details")}
value="details"
/>
<Tab
label={t("network")}
value="network"
/>
</CustomTabList>
{tab === "details" && (
<>
<StatusBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
<GaugeBoxes
isLoading={isLoading}
monitor={monitor}
/>
<MonitorTimeFrameHeader
isLoading={isLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<AreaChartBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
</>
)}
{tab === "network" && (
<NetworkStats
net={monitor?.stats?.aggregateData?.latestCheck?.net || []}
isLoading={isLoading}
checks={monitor?.stats?.checks}
dateRange={dateRange}
setDateRange={setDateRange}
/>
)}
</TabContext>
</Stack>
);
};
+22
View File
@@ -202,6 +202,13 @@
"avgCpuTemperature": "Average CPU Temperature",
"bar": "Bar",
"basicInformation": "Basic Information",
"bytesPerSecond": "Bytes per second",
"bytesReceived": "Bytes Received",
"bytesSent": "Bytes Sent",
"dataReceived": "Data Received",
"dataSent": "Data Sent",
"dataRate": "Data Rate",
"rate": "Rate",
"bulkImport": {
"fallbackPage": "Import a file to upload a list of servers in bulk",
"invalidFileType": "Invalid file type",
@@ -334,6 +341,15 @@
},
"disk": "Disk",
"diskUsage": "Disk Usage",
"drops": "Drops",
"errors": "Errors",
"errorsIn": "Errors In",
"errorsOut": "Errors Out",
"networkErrors": "Network Errors",
"networkDrops": "Network Drops",
"networkInterface": "Network Interface",
"selectInterface": "Select Interface",
"details": "Details",
"displayName": "Display name",
"distributedRightCategoryTitle": "Monitor",
"distributedStatusHeaderText": "Real-time, real-device coverage",
@@ -639,7 +655,9 @@
},
"ms": "ms",
"navControls": "Controls",
"network": "Network",
"nextWindow": "Next window",
"noNetworkStatsAvailable": "No network stats available.",
"notFoundButton": "Go to the main dashboard",
"notificationConfig": {
"description": "Select the notifications channels you want to use",
@@ -751,6 +769,10 @@
"passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.",
"saving": "Saving..."
},
"packetsPerSecond": "Packets per second",
"packetsReceived": "Packets Received",
"packetsReceivedRate": "Packets Received Rate",
"packetsSent": "Packets Sent",
"pause": "Pause",
"pingMonitoring": "Ping monitoring",
"pingMonitoringDescription": "Check whether your server is available or not.",
+6 -3
View File
@@ -330,6 +330,7 @@ class MonitorModule {
try {
const monitor = await this.Monitor.findById(monitorId);
const dates = this.getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
@@ -337,13 +338,15 @@ class MonitorModule {
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const monitorStats = {
const stats = hardwareStats[0];
return {
...monitor.toObject(),
stats: hardwareStats[0],
stats,
};
return monitorStats;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getHardwareDetailsById";
@@ -263,6 +263,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
net: {
$push: "$net",
},
updatedAts: {
$push: "$updatedAt",
},
},
},
{
@@ -378,90 +381,383 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
},
net: {
$map: {
input: { $range: [0, "$$netCount"] },
input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] },
as: "netIndex",
in: {
name: {
$arrayElemAt: [
{
$map: {
input: "$net",
as: "netArray",
in: { $arrayElemAt: ["$$netArray.name", "$$netIndex"] },
input: { $arrayElemAt: ["$net", 0] },
as: "iface",
in: "$$iface.name",
},
},
0,
"$$netIndex",
],
},
avgBytesSent: {
$avg: {
$map: {
input: "$net",
as: "netArray",
in: {
$arrayElemAt: ["$$netArray.bytes_sent", "$$netIndex"],
bytesSentPerSecond: {
$let: {
vars: {
first: {
$arrayElemAt: [
{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_sent" } },
"$$netIndex",
],
},
last: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
as: "iface",
in: "$$iface.bytes_sent",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
avgBytesRecv: {
$avg: {
$map: {
input: "$net",
as: "netArray",
in: {
$arrayElemAt: ["$$netArray.bytes_recv", "$$netIndex"],
deltaBytesRecv: {
$let: {
vars: {
first: {
$arrayElemAt: [
{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_recv" } },
"$$netIndex",
],
},
last: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
as: "iface",
in: "$$iface.bytes_recv",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
avgPacketsSent: {
$avg: {
$map: {
input: "$net",
as: "netArray",
in: {
$arrayElemAt: ["$$netArray.packets_sent", "$$netIndex"],
deltaPacketsSent: {
$let: {
vars: {
first: {
$arrayElemAt: [
{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.packets_sent" } },
"$$netIndex",
],
},
last: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
as: "iface",
in: "$$iface.packets_sent",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
avgPacketsRecv: {
$avg: {
$map: {
input: "$net",
as: "netArray",
in: {
$arrayElemAt: ["$$netArray.packets_recv", "$$netIndex"],
deltaPacketsRecv: {
$let: {
vars: {
first: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", 0] },
as: "iface",
in: "$$iface.packets_recv",
},
},
"$$netIndex",
],
},
last: {
$arrayElemAt: [
{
$map: {
input: {
$arrayElemAt: [
"$net",
{
$subtract: [{ $size: "$net" }, 1],
},
],
},
as: "iface",
in: "$$iface.packets_recv",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{
$subtract: [{ $size: "$updatedAts" }, 1],
},
],
},
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
avgErrIn: {
$avg: {
$map: {
input: "$net",
as: "netArray",
in: {
$arrayElemAt: ["$$netArray.err_in", "$$netIndex"],
deltaErrIn: {
$let: {
vars: {
first: {
$arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.err_in" } }, "$$netIndex"],
},
last: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
as: "iface",
in: "$$iface.err_in",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
avgErrOut: {
$avg: {
$map: {
input: "$net",
as: "netArray",
in: {
$arrayElemAt: ["$$netArray.err_out", "$$netIndex"],
deltaErrOut: {
$let: {
vars: {
first: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", 0] },
as: "iface",
in: "$$iface.err_out",
},
},
"$$netIndex",
],
},
last: {
$arrayElemAt: [
{
$map: {
input: {
$arrayElemAt: [
"$net",
{
$subtract: [{ $size: "$net" }, 1],
},
],
},
as: "iface",
in: "$$iface.err_out",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{
$subtract: [{ $size: "$updatedAts" }, 1],
},
],
},
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
deltaDropIn: {
$let: {
vars: {
first: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", 0] },
as: "iface",
in: "$$iface.drop_in",
},
},
"$$netIndex",
],
},
last: {
$arrayElemAt: [
{
$map: {
input: {
$arrayElemAt: [
"$net",
{
$subtract: [{ $size: "$net" }, 1],
},
],
},
as: "iface",
in: "$$iface.drop_in",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{
$subtract: [{ $size: "$updatedAts" }, 1],
},
],
},
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
deltaDropOut: {
$let: {
vars: {
first: {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", 0] },
as: "iface",
in: "$$iface.drop_out",
},
},
"$$netIndex",
],
},
last: {
$arrayElemAt: [
{
$map: {
input: {
$arrayElemAt: [
"$net",
{
$subtract: [{ $size: "$net" }, 1],
},
],
},
as: "iface",
in: "$$iface.drop_out",
},
},
"$$netIndex",
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{
$subtract: [{ $size: "$updatedAts" }, 1],
},
],
},
},
in: {
$cond: [
{ $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] },
{
$divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }],
},
0,
],
},
},
},
},
},
},
firstUpdatedAt: 1,
lastUpdatedAt: 1,
},
},
],