mirror of
				https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
				synced 2025-10-31 18:27:45 +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", | ||||
|   "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
	 Charles Bourque
					Charles Bourque