mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 09:22:21 +00:00
Merge pull request #208 from stephb9959/main
[WIFI-13315] Wi-Fi analysis fixes
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
70
src/pages/Device/SwitchPortExamination/Actions.tsx
Normal file
70
src/pages/Device/SwitchPortExamination/Actions.tsx
Normal 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;
|
||||||
@@ -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),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => {
|
|||||||
customWidth: '35px',
|
customWidth: '35px',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t],
|
[t, ouis],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(', ') ?? '-',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user