[WIFI-13315] Wi-Fi analysis fixes

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2024-01-16 19:17:03 +01:00
parent deb7715ea1
commit 810318b584
11 changed files with 201 additions and 25 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "3.0.1(2)",
"version": "3.0.1(5)",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "3.0.1(2)",
"version": "3.0.1(5)",
"license": "ISC",
"dependencies": {
"@chakra-ui/anatomy": "^2.1.1",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "3.0.1(2)",
"version": "3.0.1(5)",
"description": "",
"private": true,
"main": "index.tsx",

View File

@@ -9,6 +9,7 @@ import { AxiosError } from 'models/Axios';
import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device';
import { Note } from 'models/Note';
import { PageInfo } from 'models/Table';
import { DeviceCommandHistory } from './Commands';
export const DEVICE_PLATFORMS = ['ALL', 'AP', 'SWITCH'] as const;
export type DevicePlatform = (typeof DEVICE_PLATFORMS)[number];
@@ -461,3 +462,29 @@ export const useDeleteDeviceBatch = () => {
},
});
};
export type PowerCyclePort = {
/** Ex.: Ethernet0 */
name: string;
/** Cycle length in MS. Default is 10 000 */
cycle?: number;
};
export type PowerCycleRequest = {
serial: string;
when: number;
ports: PowerCyclePort[];
};
export const usePowerCycle = () => {
const queryClient = useQueryClient();
return useMutation(
(request: PowerCycleRequest) =>
axiosGw.post(`device/${request.serial}/powercycle`, request).then((res) => res.data as DeviceCommandHistory),
{
onSettled: () => {
queryClient.invalidateQueries(['commands']);
},
},
);
};

View File

@@ -129,11 +129,21 @@ export type DeviceStatistics = {
channel: number;
band?: string[];
channel_width: string;
noise: number;
noise?: number;
phy: string;
receive_ms: number;
transmit_ms: number;
temperature?: number;
tx_power: number;
frequency?: number[];
survey?: {
busy: number;
frequency: number;
noise: number;
time: number;
time_rx: number;
time_tx: number;
}[];
}[];
dynamic_vlans?: {
vid: number;

View File

@@ -0,0 +1,70 @@
import * as React from 'react';
import { IconButton, Tooltip, useToast } from '@chakra-ui/react';
import { Power } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { usePowerCycle } from 'hooks/Network/Devices';
import { useNotification } from 'hooks/useNotification';
import { DeviceLinkState } from 'hooks/Network/Statistics';
type Props = {
state: DeviceLinkState & { name: string };
deviceSerialNumber: string;
};
const LinkStateTableActions = ({ state, deviceSerialNumber }: Props) => {
const { t } = useTranslation();
const powerCycle = usePowerCycle();
const toast = useToast();
const { successToast, apiErrorToast } = useNotification();
const onPowerCycle = () => {
powerCycle.mutate(
{ serial: deviceSerialNumber, when: 0, ports: [{ name: state.name, cycle: 10 * 1000 }] },
{
onSuccess: (data) => {
if (data.errorCode === 0) {
successToast({
description: `Power cycle started for port ${state.name} for 10s`,
});
} else if (data.errorCode === 1) {
toast({
id: `powercycle-warning-${deviceSerialNumber}`,
title: 'Warning',
description: `${data?.errorText ?? 'Unknown Warning'}`,
status: 'warning',
duration: 5000,
isClosable: true,
position: 'top-right',
});
} else {
toast({
id: `powercycle-error-${deviceSerialNumber}`,
title: t('common.error'),
description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
},
onError: (e) => apiErrorToast({ e }),
},
);
};
return (
<Tooltip label="Power Cycle" placement="auto-start">
<IconButton
aria-label="Power Cycle"
icon={<Power size={20} />}
colorScheme="green"
onClick={onPowerCycle}
isLoading={powerCycle.isLoading}
size="xs"
/>
</Tooltip>
);
};
export default LinkStateTableActions;

View File

@@ -5,18 +5,23 @@ import DataCell from 'components/TableCells/DataCell';
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
import { DataGrid } from 'components/DataTables/DataGrid';
import { uppercaseFirstLetter } from 'helpers/stringHelper';
import LinkStateTableActions from './Actions';
type Row = DeviceLinkState & { name: string };
const dataCell = (v: number) => <DataCell bytes={v} />;
const actionCell = (row: Row, serialNumber: string) => (
<LinkStateTableActions state={row} deviceSerialNumber={serialNumber} />
);
type Props = {
statistics?: Row[];
refetch: () => void;
isFetching: boolean;
type: 'upstream' | 'downstream';
serialNumber: string;
};
const LinkStateTable = ({ statistics, refetch, isFetching, type }: Props) => {
const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber }: Props) => {
const tableController = useDataGrid({
tableSettingsId: 'switch.link-state.table',
defaultOrder: [
@@ -31,6 +36,8 @@ const LinkStateTable = ({ statistics, refetch, isFetching, type }: Props) => {
'tx_bytes',
'tx_dropped',
'tx_error',
'tx_packets',
'actions',
],
defaultSortBy: [{ id: 'name', desc: false }],
});
@@ -144,6 +151,12 @@ const LinkStateTable = ({ statistics, refetch, isFetching, type }: Props) => {
customWidth: '35px',
},
},
{
id: 'actions',
header: '',
accessorKey: '',
cell: ({ cell }) => actionCell(cell.row.original, serialNumber),
},
],
[],
);

View File

@@ -69,6 +69,7 @@ const SwitchPortExamination = ({ serialNumber }: Props) => {
refetch={getStats.refetch}
isFetching={getStats.isFetching}
type="upstream"
serialNumber={serialNumber}
/>
) : (
<Spinner size="xl" />
@@ -81,6 +82,7 @@ const SwitchPortExamination = ({ serialNumber }: Props) => {
refetch={getStats.refetch}
isFetching={getStats.isFetching}
type="downstream"
serialNumber={serialNumber}
/>
) : (
<Spinner size="xl" />

View File

@@ -166,7 +166,7 @@ const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => {
customWidth: '35px',
},
],
[t],
[t, ouis],
);
return (

View File

@@ -17,14 +17,18 @@ export type ParsedRadio = {
activeMs: string;
busyMs: string;
receiveMs: string;
sendMs: string;
phy: string;
frequency: string;
temperature: string;
};
type Props = {
data?: ParsedRadio[];
isSingle?: boolean;
};
const WifiAnalysisRadioTable = ({ data }: Props) => {
const WifiAnalysisRadioTable = ({ data, isSingle }: Props) => {
const { t } = useTranslation();
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
@@ -44,19 +48,27 @@ const WifiAnalysisRadioTable = ({ data }: Props) => {
},
{
id: 'channel',
Header: 'Ch',
Header: 'Ch.',
Footer: '',
accessor: 'channel',
customWidth: '35px',
},
{
id: 'channelWidth',
Header: t('controller.wifi.channel_width'),
Header: 'Ch. W',
Footer: '',
accessor: 'channelWidth',
customWidth: '35px',
disableSortBy: true,
},
{
id: 'tx-power',
Header: 'Tx Pow.',
Footer: '',
accessor: 'txPower',
customWidth: '35px',
disableSortBy: true,
},
{
id: 'noise',
Header: t('controller.wifi.noise'),
@@ -67,25 +79,49 @@ const WifiAnalysisRadioTable = ({ data }: Props) => {
},
{
id: 'activeMs',
Header: t('controller.wifi.active_ms'),
Header: 'Active (ms)',
Footer: '',
accessor: 'activeMs',
customWidth: '35px',
customWidth: '105px',
disableSortBy: true,
},
{
id: 'busyMs',
Header: t('controller.wifi.busy_ms'),
Header: 'Busy (ms)',
Footer: '',
accessor: 'busyMs',
customWidth: '35px',
customWidth: '105px',
disableSortBy: true,
},
{
id: 'receiveMs',
Header: t('controller.wifi.receive_ms'),
Header: 'Receive (ms)',
Footer: '',
accessor: 'receiveMs',
customWidth: '105px',
disableSortBy: true,
},
{
id: 'sendMs',
Header: 'Send (ms)',
Footer: '',
accessor: 'sendMs',
customWidth: '105px',
disableSortBy: true,
},
{
id: 'temperature',
Header: 'Temp.',
Footer: '',
accessor: 'temperature',
customWidth: '35px',
disableSortBy: true,
},
{
id: 'frequency',
Header: 'Frequency',
Footer: '',
accessor: 'frequency',
customWidth: '35px',
disableSortBy: true,
},
@@ -97,7 +133,7 @@ const WifiAnalysisRadioTable = ({ data }: Props) => {
<>
<Flex>
<Heading size="sm" mt={2} my="auto">
{t('configurations.radios')} ({data?.length})
{isSingle ? 'Radio' : `${t('configurations.radios')} (${data?.length})`}
</Heading>
<Spacer />
<ColumnPicker

View File

@@ -16,11 +16,29 @@ type Props = {
serialNumber: string;
};
const parseRadios = (t: (str: string) => string, data: { data: DeviceStatistics; recorded: number }) => {
const parseRadios = (_: (str: string) => string, data: { data: DeviceStatistics; recorded: number }) => {
const radios: ParsedRadio[] = [];
if (data.data.radios) {
for (let i = 0; i < data.data.radios.length; i += 1) {
const radio = data.data.radios[i];
let temperature = radio?.temperature;
if (temperature) temperature = temperature > 1000 ? Math.round(temperature / 1000) : temperature;
const tempNoise = radio?.noise ?? radio?.survey?.[0]?.noise;
const noise = tempNoise ? parseDbm(tempNoise) : '-';
const tempActiveMs = radio?.survey?.[0]?.time ?? radio?.active_ms;
const activeMs = tempActiveMs?.toLocaleString() ?? '-';
const tempBusyMs = radio?.survey?.[0]?.busy ?? radio?.busy_ms;
const busyMs = tempBusyMs?.toLocaleString() ?? '-';
const tempReceiveMs = radio?.survey?.[0]?.time_rx ?? radio?.receive_ms;
const receiveMs = tempReceiveMs?.toLocaleString() ?? '-';
const tempSendMs = radio?.survey?.[0]?.time_tx;
const sendMs = tempSendMs?.toLocaleString() ?? '-';
if (radio) {
radios.push({
recorded: data.recorded,
@@ -29,12 +47,15 @@ const parseRadios = (t: (str: string) => string, data: { data: DeviceStatistics;
deductedBand: radio.channel && radio.channel > 16 ? '5G' : '2G',
channel: radio.channel,
channelWidth: radio.channel_width,
noise: radio.noise ? parseDbm(radio.noise) : '-',
noise,
txPower: radio.tx_power ?? '-',
activeMs: compactSecondsToDetailed(radio?.active_ms ? Math.floor(radio.active_ms / 1000) : 0, t),
busyMs: compactSecondsToDetailed(radio?.busy_ms ? Math.floor(radio.busy_ms / 1000) : 0, t),
receiveMs: compactSecondsToDetailed(radio?.receive_ms ? Math.floor(radio.receive_ms / 1000) : 0, t),
activeMs,
busyMs,
receiveMs,
sendMs,
phy: radio.phy,
temperature: temperature ? temperature.toString() : '-',
frequency: radio.frequency?.join(', ') ?? '-',
});
}
}

View File

@@ -325,11 +325,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<DeviceSummary serialNumber={serialNumber} />
<DeviceDetails serialNumber={serialNumber} />
<DeviceStatisticsCard serialNumber={serialNumber} />
{getDevice.data?.deviceType === 'AP' ? (
<WifiAnalysisCard serialNumber={serialNumber} />
) : (
<SwitchPortExamination serialNumber={serialNumber} />
)}
{getDevice.data?.deviceType === 'AP' ? <WifiAnalysisCard serialNumber={serialNumber} /> : null}
{getDevice.data?.deviceType === 'SWITCH' ? <SwitchPortExamination serialNumber={serialNumber} /> : null}
<DeviceLogsCard serialNumber={serialNumber} />
{getDevice.data && getDevice.data?.hasRADIUSSessions > 0 ? (
<RadiusClientsCard serialNumber={serialNumber} />