Merge pull request #208 from stephb9959/main

[WIFI-13315] Wi-Fi analysis fixes
This commit is contained in:
Charles Bourque
2024-01-16 19:17:52 +01:00
committed by GitHub
11 changed files with 201 additions and 25 deletions

4
package-lock.json generated
View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import { AxiosError } from 'models/Axios';
import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device'; import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device';
import { Note } from 'models/Note'; import { Note } from 'models/Note';
import { PageInfo } from 'models/Table'; import { PageInfo } from 'models/Table';
import { DeviceCommandHistory } from './Commands';
export const DEVICE_PLATFORMS = ['ALL', 'AP', 'SWITCH'] as const; export const DEVICE_PLATFORMS = ['ALL', 'AP', 'SWITCH'] as const;
export type DevicePlatform = (typeof DEVICE_PLATFORMS)[number]; 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; channel: number;
band?: string[]; band?: string[];
channel_width: string; channel_width: string;
noise: number; noise?: number;
phy: string; phy: string;
receive_ms: number; receive_ms: number;
transmit_ms: number; transmit_ms: number;
temperature?: number;
tx_power: number; tx_power: number;
frequency?: number[];
survey?: {
busy: number;
frequency: number;
noise: number;
time: number;
time_rx: number;
time_tx: number;
}[];
}[]; }[];
dynamic_vlans?: { dynamic_vlans?: {
vid: number; 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 { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
import { DataGrid } from 'components/DataTables/DataGrid'; import { DataGrid } from 'components/DataTables/DataGrid';
import { uppercaseFirstLetter } from 'helpers/stringHelper'; import { uppercaseFirstLetter } from 'helpers/stringHelper';
import LinkStateTableActions from './Actions';
type Row = DeviceLinkState & { name: string }; type Row = DeviceLinkState & { name: string };
const dataCell = (v: number) => <DataCell bytes={v} />; const dataCell = (v: number) => <DataCell bytes={v} />;
const actionCell = (row: Row, serialNumber: string) => (
<LinkStateTableActions state={row} deviceSerialNumber={serialNumber} />
);
type Props = { type Props = {
statistics?: Row[]; statistics?: Row[];
refetch: () => void; refetch: () => void;
isFetching: boolean; isFetching: boolean;
type: 'upstream' | 'downstream'; type: 'upstream' | 'downstream';
serialNumber: string;
}; };
const LinkStateTable = ({ statistics, refetch, isFetching, type }: Props) => { const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber }: Props) => {
const tableController = useDataGrid({ const tableController = useDataGrid({
tableSettingsId: 'switch.link-state.table', tableSettingsId: 'switch.link-state.table',
defaultOrder: [ defaultOrder: [
@@ -31,6 +36,8 @@ const LinkStateTable = ({ statistics, refetch, isFetching, type }: Props) => {
'tx_bytes', 'tx_bytes',
'tx_dropped', 'tx_dropped',
'tx_error', 'tx_error',
'tx_packets',
'actions',
], ],
defaultSortBy: [{ id: 'name', desc: false }], defaultSortBy: [{ id: 'name', desc: false }],
}); });
@@ -144,6 +151,12 @@ const LinkStateTable = ({ statistics, refetch, isFetching, type }: Props) => {
customWidth: '35px', 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} refetch={getStats.refetch}
isFetching={getStats.isFetching} isFetching={getStats.isFetching}
type="upstream" type="upstream"
serialNumber={serialNumber}
/> />
) : ( ) : (
<Spinner size="xl" /> <Spinner size="xl" />
@@ -81,6 +82,7 @@ const SwitchPortExamination = ({ serialNumber }: Props) => {
refetch={getStats.refetch} refetch={getStats.refetch}
isFetching={getStats.isFetching} isFetching={getStats.isFetching}
type="downstream" type="downstream"
serialNumber={serialNumber}
/> />
) : ( ) : (
<Spinner size="xl" /> <Spinner size="xl" />

View File

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

View File

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

View File

@@ -16,11 +16,29 @@ type Props = {
serialNumber: string; 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[] = []; const radios: ParsedRadio[] = [];
if (data.data.radios) { if (data.data.radios) {
for (let i = 0; i < data.data.radios.length; i += 1) { for (let i = 0; i < data.data.radios.length; i += 1) {
const radio = data.data.radios[i]; 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) { if (radio) {
radios.push({ radios.push({
recorded: data.recorded, recorded: data.recorded,
@@ -29,12 +47,15 @@ const parseRadios = (t: (str: string) => string, data: { data: DeviceStatistics;
deductedBand: radio.channel && radio.channel > 16 ? '5G' : '2G', deductedBand: radio.channel && radio.channel > 16 ? '5G' : '2G',
channel: radio.channel, channel: radio.channel,
channelWidth: radio.channel_width, channelWidth: radio.channel_width,
noise: radio.noise ? parseDbm(radio.noise) : '-', noise,
txPower: radio.tx_power ?? '-', txPower: radio.tx_power ?? '-',
activeMs: compactSecondsToDetailed(radio?.active_ms ? Math.floor(radio.active_ms / 1000) : 0, t), activeMs,
busyMs: compactSecondsToDetailed(radio?.busy_ms ? Math.floor(radio.busy_ms / 1000) : 0, t), busyMs,
receiveMs: compactSecondsToDetailed(radio?.receive_ms ? Math.floor(radio.receive_ms / 1000) : 0, t), receiveMs,
sendMs,
phy: radio.phy, phy: radio.phy,
temperature: temperature ? temperature.toString() : '-',
frequency: radio.frequency?.join(', ') ?? '-',
}); });
} }
} }

View File

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