[WIFI-12614] Dynamic VLAN support

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-05-17 18:03:42 +02:00
parent edcca87acf
commit a154fffcce
10 changed files with 547 additions and 102 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.10.0(42)",
"version": "2.10.0(44)",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.10.0(42)",
"version": "2.10.0(44)",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.18",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.10.0(42)",
"version": "2.10.0(44)",
"description": "",
"private": true,
"main": "index.tsx",

View File

@@ -39,6 +39,7 @@ type DeviceInterfaceStatistics = {
ack_signal_avg: number;
bssid: string;
connected: number;
dynamic_vlan?: number;
inactive: number;
ipaddr_v4: string;
rssi: number;
@@ -117,6 +118,13 @@ export type DeviceStatistics = {
transmit_ms: number;
tx_power: number;
}[];
dynamic_vlans?: {
vid: number;
rx_bytes: number;
rx_packets: number;
tx_bytes: number;
tx_packets: number;
}[];
unit?: {
load: [number, number, number];
localtime: number;

View File

@@ -69,7 +69,7 @@ const RestrictionsCard = ({ serialNumber }: Props) => {
</Tooltip>
) : null}
</CardHeader>
<CardBody p={0} display="block">
<CardBody display="block">
<Flex mt={2}>
<Heading size="sm" mr={2} my="auto">
{t('restrictions.countries')}:

View File

@@ -29,22 +29,44 @@ const getDivisionFactor = (maxBytes: number) => {
return { factor: 1024 * 1024 * 1024, unit: 'GB' };
};
const getDivisionFactorPackets = (maxPackets: number) => {
if (maxPackets < 1000) {
return { factor: 1, unit: '' };
}
if (maxPackets < 1000 * 1000) {
return { factor: 1000, unit: 'K' };
}
if (maxPackets < 1000 * 1000 * 1000) {
return { factor: 1000 * 1000, unit: 'M' };
}
return { factor: 1000 * 1000 * 1000, unit: 'G' };
};
type Props = {
data: {
tx: number[];
rx: number[];
packetsRx: number[];
packetsTx: number[];
recorded: number[];
maxTx: number;
maxRx: number;
maxTx: number;
maxPacketsRx: number;
maxPacketsTx: number;
removed?: boolean;
};
format: 'bytes' | 'packets';
};
const InterfaceChart = ({ data }: Props) => {
const InterfaceChart = ({ data, format }: Props) => {
const { colorMode } = useColorMode();
const { factor, unit } = getDivisionFactor(data.maxTx);
const packetsFactor = getDivisionFactorPackets(
data.maxPacketsTx > data.maxPacketsRx ? data.maxPacketsTx : data.maxPacketsRx,
);
const points: ChartData<'line', string[], string> = {
const bytesPoints: ChartData<'line', string[], string> = {
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
datasets: [
{
@@ -69,6 +91,38 @@ const InterfaceChart = ({ data }: Props) => {
},
],
};
const packetPoints: ChartData<'line', string[], string> = {
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
datasets: [
{
// Real 'Tx', but shown as 'Rx'
label: 'Tx',
data: data.packetsRx.map((rx) => rx.toString()),
borderColor: colorMode === 'light' ? 'rgba(99, 179, 237, 1)' : 'rgba(190, 227, 248, 1)', // blue-300 - blue-100
backgroundColor: colorMode === 'light' ? 'rgba(99, 179, 237, 0.3)' : 'rgba(190, 227, 248, 0.3)', // blue-300 - blue-100
tension: 0.5,
pointRadius: 0,
fill: 'start',
},
{
// Real 'Tx', but shown as 'Rx'
label: 'Rx',
data: data.packetsTx.map((tx) => tx.toString()),
borderColor: colorMode === 'light' ? 'rgba(72, 187, 120, 1)' : 'rgba(154, 230, 180, 1)', // green-400 - green-200
backgroundColor: colorMode === 'light' ? 'rgba(72, 187, 120, 0.3)' : 'rgba(154, 230, 180, 0.3)', // green-400 - green-200
tension: 0.5,
pointRadius: 0,
fill: 'start',
},
],
};
const packetsTick = (value: number) => {
if (packetsFactor.factor === 1) {
return value.toLocaleString();
}
return `${(value / packetsFactor.factor).toLocaleString()}${packetsFactor.unit}`;
};
return (
<Line
@@ -89,7 +143,10 @@ const InterfaceChart = ({ data }: Props) => {
intersect: false,
callbacks: {
label: (context) => `${context.dataset.label}: ${context.formattedValue} ${unit}`,
label:
format === 'bytes'
? (context) => `${context.dataset.label}: ${context.formattedValue} ${unit}`
: undefined,
},
},
},
@@ -109,7 +166,10 @@ const InterfaceChart = ({ data }: Props) => {
},
ticks: {
color: colorMode === 'dark' ? 'white' : undefined,
callback: (tickValue) => `${tickValue} ${unit}`,
callback:
format === 'bytes'
? (tickValue) => `${tickValue} ${unit}`
: (tickValue) => (typeof tickValue === 'number' ? packetsTick(tickValue) : tickValue),
},
},
},
@@ -118,7 +178,7 @@ const InterfaceChart = ({ data }: Props) => {
intersect: true,
},
}}
data={points}
data={format === 'bytes' ? bytesPoints : packetPoints}
/>
);
};

View File

@@ -0,0 +1,186 @@
import * as React from 'react';
import { useColorMode } from '@chakra-ui/react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
ChartData,
Filler,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler);
const getDivisionFactor = (maxBytes: number) => {
if (maxBytes < 1024) {
return { factor: 1, unit: 'B' };
}
if (maxBytes < 1024 * 1024) {
return { factor: 1024, unit: 'KB' };
}
if (maxBytes < 1024 * 1024 * 1024) {
return { factor: 1024 * 1024, unit: 'MB' };
}
return { factor: 1024 * 1024 * 1024, unit: 'GB' };
};
const getDivisionFactorPackets = (maxPackets: number) => {
if (maxPackets < 1000) {
return { factor: 1, unit: '' };
}
if (maxPackets < 1000 * 1000) {
return { factor: 1000, unit: 'K' };
}
if (maxPackets < 1000 * 1000 * 1000) {
return { factor: 1000 * 1000, unit: 'M' };
}
return { factor: 1000 * 1000 * 1000, unit: 'G' };
};
type Props = {
data: {
tx: number[];
rx: number[];
packetsRx: number[];
packetsTx: number[];
recorded: number[];
maxRx: number;
maxTx: number;
maxPacketsRx: number;
maxPacketsTx: number;
removed?: boolean;
};
format: 'bytes' | 'packets';
};
const VlanChart = ({ data, format }: Props) => {
const { colorMode } = useColorMode();
const { factor, unit } = getDivisionFactor(data.maxTx);
const packetsFactor = getDivisionFactorPackets(
data.maxPacketsTx > data.maxPacketsRx ? data.maxPacketsTx : data.maxPacketsRx,
);
const bytesPoints: ChartData<'line', string[], string> = {
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
datasets: [
{
// Real 'Tx', but shown as 'Rx'
label: 'Tx',
data: data.rx.map((tx) => (Math.floor((tx / factor) * 100) / 100).toFixed(2)),
borderColor: colorMode === 'light' ? 'rgba(99, 179, 237, 1)' : 'rgba(190, 227, 248, 1)', // blue-300 - blue-100
backgroundColor: colorMode === 'light' ? 'rgba(99, 179, 237, 0.3)' : 'rgba(190, 227, 248, 0.3)', // blue-300 - blue-100
tension: 0.5,
pointRadius: 0,
fill: 'start',
},
{
// Real 'Rx', but shown as 'Tx'
label: 'Rx',
data: data.tx.map((rx) => (Math.floor((rx / factor) * 100) / 100).toFixed(2)),
borderColor: colorMode === 'light' ? 'rgba(72, 187, 120, 1)' : 'rgba(154, 230, 180, 1)', // green-400 - green-200
backgroundColor: colorMode === 'light' ? 'rgba(72, 187, 120, 0.3)' : 'rgba(154, 230, 180, 0.3)', // green-400 - green-200
tension: 0.5,
pointRadius: 0,
fill: 'start',
},
],
};
const packetPoints: ChartData<'line', string[], string> = {
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
datasets: [
{
// Real 'Tx', but shown as 'Rx'
label: 'Tx',
data: data.packetsRx.map((rx) => rx.toString()),
borderColor: colorMode === 'light' ? 'rgba(99, 179, 237, 1)' : 'rgba(190, 227, 248, 1)', // blue-300 - blue-100
backgroundColor: colorMode === 'light' ? 'rgba(99, 179, 237, 0.3)' : 'rgba(190, 227, 248, 0.3)', // blue-300 - blue-100
tension: 0.5,
pointRadius: 0,
fill: 'start',
},
{
// Real 'Tx', but shown as 'Rx'
label: 'Rx',
data: data.packetsTx.map((tx) => tx.toString()),
borderColor: colorMode === 'light' ? 'rgba(72, 187, 120, 1)' : 'rgba(154, 230, 180, 1)', // green-400 - green-200
backgroundColor: colorMode === 'light' ? 'rgba(72, 187, 120, 0.3)' : 'rgba(154, 230, 180, 0.3)', // green-400 - green-200
tension: 0.5,
pointRadius: 0,
fill: 'start',
},
],
};
const packetsTick = (value: number) => {
if (packetsFactor.factor === 1) {
return value.toLocaleString();
}
return `${(value / packetsFactor.factor).toLocaleString()}${packetsFactor.unit}`;
};
return (
<Line
options={{
plugins: {
legend: {
position: 'top' as const,
labels: {
color: colorMode === 'dark' ? 'white' : undefined,
},
},
title: {
display: false,
},
tooltip: {
mode: 'index',
position: 'nearest',
intersect: false,
callbacks: {
label:
format === 'bytes'
? (context) => `${context.dataset.label}: ${context.formattedValue} ${unit}`
: undefined,
},
},
},
scales: {
x: {
grid: {
color: colorMode === 'dark' ? 'white' : undefined,
},
ticks: {
color: colorMode === 'dark' ? 'white' : undefined,
maxRotation: 10,
},
},
y: {
grid: {
color: colorMode === 'dark' ? 'white' : undefined,
},
ticks: {
color: colorMode === 'dark' ? 'white' : undefined,
callback:
format === 'bytes'
? (tickValue) => `${tickValue} ${unit}`
: (tickValue) => (typeof tickValue === 'number' ? packetsTick(tickValue) : tickValue),
},
},
},
hover: {
mode: 'nearest',
intersect: true,
},
}}
data={format === 'bytes' ? bytesPoints : packetPoints}
/>
);
};
export default React.memo(VlanChart);

View File

@@ -8,6 +8,7 @@ import InterfaceChart from './InterfaceChart';
import DeviceMemoryChart from './MemoryChart';
import { useStatisticsCard } from './useStatisticsCard';
import ViewLastStatsModal from './ViewLastStatsModal';
import VlanChart from './VlanChart';
import { RefreshButton } from 'components/Buttons/RefreshButton';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
@@ -42,6 +43,7 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
const { time, setTime, parsedData, isLoading, selected, onSelectInterface, refresh } = useStatisticsCard({
serialNumber,
});
const [formatChosen, setFormatChosen] = React.useState<'bytes' | 'packets'>('bytes');
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
@@ -50,15 +52,29 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
setTime(undefined);
};
const onFormatChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFormatChosen(e.target.value as 'bytes' | 'packets');
};
const interfaces = React.useMemo(() => {
if (!parsedData) return undefined;
return Object.entries(parsedData.interfaces).map(([name, data]) => (
<Box hidden={name !== selected} key={uuid()}>
<InterfaceChart data={data} />
<InterfaceChart data={data} format={formatChosen} />
</Box>
));
}, [parsedData, selected]);
}, [parsedData, selected, formatChosen]);
const vlans = React.useMemo(() => {
if (!parsedData) return undefined;
return Object.entries(parsedData.vlans).map(([name, data]) => (
<Box hidden={`VLAN-${name}` !== selected} key={uuid()}>
<VlanChart data={data} format={formatChosen} />
</Box>
));
}, [parsedData, selected, formatChosen]);
const memory = React.useMemo(() => {
if (!parsedData) return undefined;
@@ -76,7 +92,13 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
<Heading size="md">{t('configurations.statistics')}</Heading>
<Spacer />
<HStack>
<Select value={selected} onChange={onSelectInterface}>
{selected === 'memory' ? null : (
<Select value={formatChosen} onChange={onFormatChange} w="112px">
<option value="bytes">Data</option>
<option value="packets">Packets</option>
</Select>
)}
<Select value={selected} onChange={onSelectInterface} w="unset">
{parsedData?.interfaces
? Object.keys(parsedData.interfaces).map((v) => (
<option value={v} key={uuid()}>
@@ -84,6 +106,13 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
</option>
))
: null}
{parsedData?.vlans
? Object.keys(parsedData.vlans).map((v) => (
<option value={`VLAN-${v}`} key={uuid()}>
VLAN - {v}
</option>
))
: null}
<option value="memory">{t('statistics.memory')}</option>
</Select>
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
@@ -123,6 +152,7 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
<Box>
{selected === 'memory' && memory}
{interfaces}
{vlans}
</Box>
</LoadingOverlay>
)}

View File

@@ -37,9 +37,21 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
const parsedData = React.useMemo(() => {
if (!getStats.data && !getCustomStats.data) return undefined;
try {
const data: Record<
string,
{ tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number; removed?: boolean }
{
tx: number[];
rx: number[];
packetsRx: number[];
packetsTx: number[];
recorded: number[];
maxRx: number;
maxTx: number;
maxPacketsRx: number;
maxPacketsTx: number;
removed?: boolean;
}
> = {};
const memoryData = {
used: [] as number[],
@@ -49,8 +61,29 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
total: [] as number[],
recorded: [] as number[],
};
const vlanData: Record<
string,
{
tx: number[];
rx: number[];
packetsRx: number[];
packetsTx: number[];
recorded: number[];
maxRx: number;
maxTx: number;
maxPacketsRx: number;
maxPacketsTx: number;
removed?: boolean;
}
> = {};
const previousRx: { [key: string]: number } = {};
const previousTx: { [key: string]: number } = {};
const previousPacketsRx: { [key: string]: number } = {};
const previousPacketsTx: { [key: string]: number } = {};
const previousVlanRx: { [key: string]: number } = {};
const previousVlanTx: { [key: string]: number } = {};
const previousVlanPacketsRx: { [key: string]: number } = {};
const previousVlanPacketsTx: { [key: string]: number } = {};
let dataToLoop = getCustomStats.data ?? getStats.data?.data;
if (dataToLoop && !getCustomStats.data) {
@@ -67,8 +100,17 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
}
previousRx[inter.name] = inter.counters?.rx_bytes ?? 0;
previousTx[inter.name] = inter.counters?.tx_bytes ?? 0;
previousPacketsRx[inter.name] = inter.counters?.rx_packets ?? 0;
previousPacketsTx[inter.name] = inter.counters?.tx_packets ?? 0;
}
for (const vlan of stat.data.dynamic_vlans ?? []) {
previousVlanRx[vlan.vid] = vlan.rx_bytes ?? 0;
previousVlanTx[vlan.vid] = vlan.tx_bytes ?? 0;
previousVlanPacketsRx[vlan.vid] = vlan.rx_packets ?? 0;
previousVlanPacketsTx[vlan.vid] = vlan.tx_packets ?? 0;
}
} else {
// Memory
const newMem = extractMemory(stat.data);
memoryData.used.push(newMem.used ?? 0);
memoryData.buffered.push(newMem.buffered ?? 0);
@@ -77,18 +119,100 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
memoryData.total.push(newMem.total ?? 0);
memoryData.recorded.push(stat.recorded);
// Vlans
for (const vlan of stat.data.dynamic_vlans ?? []) {
const rx = vlan.rx_bytes ?? 0;
const tx = vlan.tx_bytes ?? 0;
const packetsRx = vlan.rx_packets ?? 0;
const packetsTx = vlan?.tx_packets ?? 0;
let rxDelta = rx - (previousVlanRx[vlan.vid] ?? 0);
if (rxDelta < 0) rxDelta = 0;
let txDelta = tx - (previousVlanTx[vlan.vid] ?? 0);
if (txDelta < 0) txDelta = 0;
let packetsRxDelta = packetsRx - (previousVlanPacketsRx[vlan.vid] ?? 0);
if (packetsRxDelta < 0) packetsRxDelta = 0;
let packetsTxDelta = packetsTx - (previousVlanPacketsTx[vlan.vid] ?? 0);
if (packetsTxDelta < 0) packetsTxDelta = 0;
if (vlanData[vlan.vid] === undefined)
vlanData[vlan.vid] = {
rx: [rxDelta],
tx: [txDelta],
packetsRx: [packetsRxDelta],
packetsTx: [packetsTxDelta],
recorded: [stat.recorded],
maxTx: 0,
maxRx: 0,
maxPacketsRx: 0,
maxPacketsTx: 0,
};
else {
if (vlanData[vlan.vid] && !vlanData[vlan.vid]?.removed && vlanData[vlan.vid]?.recorded.length === 1) {
vlanData[vlan.vid]?.tx.shift();
vlanData[vlan.vid]?.rx.shift();
vlanData[vlan.vid]?.packetsTx.shift();
vlanData[vlan.vid]?.packetsRx.shift();
vlanData[vlan.vid]?.recorded.shift();
// @ts-ignore
vlanData[vlan.vid].maxRx = rxDelta;
// @ts-ignore
vlanData[vlan.vid].maxTx = txDelta;
// @ts-ignore
vlanData[vlan.vid].removed = true;
}
vlanData[vlan.vid]?.rx.push(rxDelta);
vlanData[vlan.vid]?.tx.push(txDelta);
vlanData[vlan.vid]?.packetsRx.push(packetsRxDelta);
vlanData[vlan.vid]?.packetsTx.push(packetsTxDelta);
vlanData[vlan.vid]?.recorded.push(stat.recorded);
// @ts-ignore
if (vlanData[vlan.vid] !== undefined && txDelta > vlanData[vlan.vid].maxTx) {
// @ts-ignore
vlanData[vlan.vid].maxTx = txDelta;
}
// @ts-ignore
if (vlanData[vlan.vid] !== undefined && rxDelta > vlanData[vlan.vid].maxRx) {
// @ts-ignore
vlanData[vlan.vid].maxRx = rxDelta;
}
// @ts-ignore
if (vlanData[vlan.vid] !== undefined && packetsTxDelta > vlanData[vlan.vid].maxPacketsTx) {
// @ts-ignore
vlanData[vlan.vid].maxPacketsTx = packetsTxDelta;
}
// @ts-ignore
if (vlanData[vlan.vid] !== undefined && packetsRxDelta > vlanData[vlan.vid].maxPacketsRx) {
// @ts-ignore
vlanData[vlan.vid].maxPacketsRx = packetsRxDelta;
}
}
previousVlanRx[vlan.vid] = rx;
previousVlanTx[vlan.vid] = tx;
previousVlanPacketsRx[vlan.vid] = packetsRx;
previousVlanPacketsTx[vlan.vid] = packetsTx;
}
// Interfaces
for (const inter of stat.data.interfaces ?? []) {
const isInterUpstream = inter.name?.substring(0, 2) === 'up';
let rx = inter.counters?.rx_bytes ?? 0;
let tx = inter.counters?.tx_bytes ?? 0;
let packetsRx = inter.counters?.rx_packets ?? 0;
let packetsTx = inter.counters?.tx_packets ?? 0;
if (inter['counters-aggregate']) {
rx = inter['counters-aggregate'].rx_bytes;
tx = inter['counters-aggregate'].tx_bytes;
packetsRx = inter['counters-aggregate'].rx_packets;
packetsTx = inter['counters-aggregate'].tx_packets;
} else if (isInterUpstream) {
for (const ssid of inter.ssids ?? []) {
rx += ssid.counters?.rx_bytes ?? 0;
tx += ssid.counters?.tx_bytes ?? 0;
packetsRx += ssid.counters?.rx_packets ?? 0;
packetsTx += ssid.counters?.tx_packets ?? 0;
}
}
@@ -96,18 +220,29 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
if (rxDelta < 0) rxDelta = 0;
let txDelta = tx - (previousTx[inter.name] ?? 0);
if (txDelta < 0) txDelta = 0;
let packetsRxDelta = packetsRx - (previousPacketsRx[inter.name] ?? 0);
if (packetsRxDelta < 0) packetsRxDelta = 0;
let packetsTxDelta = packetsTx - (previousPacketsTx[inter.name] ?? 0);
if (packetsTxDelta < 0) packetsTxDelta = 0;
if (data[inter.name] === undefined)
data[inter.name] = {
rx: [rxDelta],
tx: [txDelta],
packetsRx: [packetsRxDelta],
packetsTx: [packetsTxDelta],
recorded: [stat.recorded],
maxTx: txDelta,
maxRx: rxDelta,
maxTx: 0,
maxRx: 0,
maxPacketsRx: 0,
maxPacketsTx: 0,
};
else {
if (data[inter.name] && !data[inter.name]?.removed && data[inter.name]?.recorded.length === 1) {
data[inter.name]?.tx.shift();
data[inter.name]?.rx.shift();
data[inter.name]?.packetsTx.shift();
data[inter.name]?.packetsRx.shift();
data[inter.name]?.recorded.shift();
// @ts-ignore
data[inter.name].maxRx = rxDelta;
@@ -119,22 +254,37 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
data[inter.name]?.rx.push(rxDelta);
data[inter.name]?.tx.push(txDelta);
data[inter.name]?.packetsRx.push(packetsRxDelta);
data[inter.name]?.packetsTx.push(packetsTxDelta);
data[inter.name]?.recorded.push(stat.recorded);
// @ts-ignore
if (data[inter.name] !== undefined && txDelta > data[inter.name].maxTx) data[inter.name].maxTx = txDelta;
// @ts-ignore
if (data[inter.name] !== undefined && rxDelta > data[inter.name].maxRx) data[inter.name].maxRx = rxDelta;
// @ts-ignore
if (data[inter.name] !== undefined && packetsTxDelta > data[inter.name].maxPacketsTx)
// @ts-ignore
data[inter.name].maxPacketsTx = packetsTxDelta;
// @ts-ignore
if (data[inter.name] !== undefined && packetsRxDelta > data[inter.name].maxPacketsRx)
// @ts-ignore
data[inter.name].maxPacketsRx = packetsRxDelta;
}
previousRx[inter.name] = rx;
previousTx[inter.name] = tx;
previousPacketsRx[inter.name] = packetsRx;
previousPacketsTx[inter.name] = packetsTx;
}
}
}
return {
interfaces: data,
memory: memoryData,
vlans: vlanData,
};
} catch (e) {
return undefined;
}
}, [getStats.data, getCustomStats.data]);
const refresh = React.useCallback(() => {

View File

@@ -26,14 +26,16 @@ export type ParsedAssociation = {
txMcs: number | string;
txNss: number | string;
recorded: number;
dynamicVlan?: number;
};
type Props = {
data?: ParsedAssociation[];
ouis?: Record<string, string>;
isSingle?: boolean;
};
const WifiAnalysisAssocationsTable = ({ data, ouis }: Props) => {
const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => {
const { t } = useTranslation();
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
@@ -56,7 +58,7 @@ const WifiAnalysisAssocationsTable = ({ data, ouis }: Props) => {
Header: '',
Footer: '',
accessor: 'radio.index',
Cell: ({ cell }) => indexCell(cell.row.original),
Cell: ({ cell }) => indexCell(cell.row.original) ?? '',
customWidth: '35px',
alwaysShow: true,
disableSortBy: true,
@@ -79,6 +81,14 @@ const WifiAnalysisAssocationsTable = ({ data, ouis }: Props) => {
customWidth: '35px',
disableSortBy: true,
},
{
id: 'dynamicVlan',
Header: 'VLAN',
Footer: '',
Cell: (v) => (v.cell.row.original.dynamicVlan !== undefined ? `${v.cell.row.original.dynamicVlan}` : '-'),
accessor: 'txBytes',
customWidth: '35px',
},
{
id: 'mode',
Header: t('controller.wifi.mode'),
@@ -151,7 +161,7 @@ const WifiAnalysisAssocationsTable = ({ data, ouis }: Props) => {
<>
<Flex mt={2}>
<Heading size="sm" my="auto">
{t('devices.associations')} ({data?.length})
{isSingle ? 'Association' : `${t('devices.associations')} (${data?.length})`}
</Heading>
<Spacer />
<ColumnPicker

View File

@@ -83,6 +83,7 @@ const parseAssociations = (data: { data: DeviceStatistics; recorded: number }, r
txMcs: association.tx_rate.mcs ?? '-',
txNss: association.tx_rate.nss ?? '-',
recorded: data.recorded,
dynamicVlan: association.dynamic_vlan,
});
}
}