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 ? (