mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-28 17:02:21 +00:00
[WIFI-13315] Wi-Fi analysis fixes
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "3.0.1(2)",
|
||||
"version": "3.0.1(5)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
|
||||
@@ -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']);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
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 { 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),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -166,7 +166,7 @@ const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => {
|
||||
customWidth: '35px',
|
||||
},
|
||||
],
|
||||
[t],
|
||||
[t, ouis],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(', ') ?? '-',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user