diff --git a/package-lock.json b/package-lock.json
index bd54515..fc5fbc7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index e587daf..281e7a9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
- "version": "3.0.1(2)",
+ "version": "3.0.1(5)",
"description": "",
"private": true,
"main": "index.tsx",
diff --git a/src/hooks/Network/Devices.ts b/src/hooks/Network/Devices.ts
index b91c3f4..c88e98e 100644
--- a/src/hooks/Network/Devices.ts
+++ b/src/hooks/Network/Devices.ts
@@ -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']);
+ },
+ },
+ );
+};
diff --git a/src/hooks/Network/Statistics.ts b/src/hooks/Network/Statistics.ts
index 4292fbb..43aab83 100644
--- a/src/hooks/Network/Statistics.ts
+++ b/src/hooks/Network/Statistics.ts
@@ -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;
diff --git a/src/pages/Device/SwitchPortExamination/Actions.tsx b/src/pages/Device/SwitchPortExamination/Actions.tsx
new file mode 100644
index 0000000..586561f
--- /dev/null
+++ b/src/pages/Device/SwitchPortExamination/Actions.tsx
@@ -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 (
+
+ }
+ colorScheme="green"
+ onClick={onPowerCycle}
+ isLoading={powerCycle.isLoading}
+ size="xs"
+ />
+
+ );
+};
+
+export default LinkStateTableActions;
diff --git a/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx b/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx
index 02d8022..03928f2 100644
--- a/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx
+++ b/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx
@@ -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) => ;
+const actionCell = (row: Row, serialNumber: string) => (
+
+);
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),
+ },
],
[],
);
diff --git a/src/pages/Device/SwitchPortExamination/index.tsx b/src/pages/Device/SwitchPortExamination/index.tsx
index 884749f..88c3728 100644
--- a/src/pages/Device/SwitchPortExamination/index.tsx
+++ b/src/pages/Device/SwitchPortExamination/index.tsx
@@ -69,6 +69,7 @@ const SwitchPortExamination = ({ serialNumber }: Props) => {
refetch={getStats.refetch}
isFetching={getStats.isFetching}
type="upstream"
+ serialNumber={serialNumber}
/>
) : (
@@ -81,6 +82,7 @@ const SwitchPortExamination = ({ serialNumber }: Props) => {
refetch={getStats.refetch}
isFetching={getStats.isFetching}
type="downstream"
+ serialNumber={serialNumber}
/>
) : (
diff --git a/src/pages/Device/WifiAnalysis/AssocationsTable.tsx b/src/pages/Device/WifiAnalysis/AssocationsTable.tsx
index 1044912..af4d26a 100644
--- a/src/pages/Device/WifiAnalysis/AssocationsTable.tsx
+++ b/src/pages/Device/WifiAnalysis/AssocationsTable.tsx
@@ -166,7 +166,7 @@ const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => {
customWidth: '35px',
},
],
- [t],
+ [t, ouis],
);
return (
diff --git a/src/pages/Device/WifiAnalysis/RadiosTable.tsx b/src/pages/Device/WifiAnalysis/RadiosTable.tsx
index 1ce9fc2..d09550a 100644
--- a/src/pages/Device/WifiAnalysis/RadiosTable.tsx
+++ b/src/pages/Device/WifiAnalysis/RadiosTable.tsx
@@ -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([]);
@@ -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) => {
<>
- {t('configurations.radios')} ({data?.length})
+ {isSingle ? 'Radio' : `${t('configurations.radios')} (${data?.length})`}
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(', ') ?? '-',
});
}
}
diff --git a/src/pages/Device/Wrapper.tsx b/src/pages/Device/Wrapper.tsx
index 91d5dc7..28fde44 100644
--- a/src/pages/Device/Wrapper.tsx
+++ b/src/pages/Device/Wrapper.tsx
@@ -325,11 +325,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
- {getDevice.data?.deviceType === 'AP' ? (
-
- ) : (
-
- )}
+ {getDevice.data?.deviceType === 'AP' ? : null}
+ {getDevice.data?.deviceType === 'SWITCH' ? : null}
{getDevice.data && getDevice.data?.hasRADIUSSessions > 0 ? (