mirror of
				https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
				synced 2025-10-31 02:07:45 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			328 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			328 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import React, { useState, useEffect } from 'react';
 | |
| import { useTranslation } from 'react-i18next';
 | |
| import { useParams } from 'react-router-dom';
 | |
| import { useAuth } from 'ucentral-libs';
 | |
| import axiosInstance from 'utils/axiosInstance';
 | |
| import NetworkDiagram from 'components/NetworkDiagram';
 | |
| import { cleanBytesString, prettyDate, compactSecondsToDetailed } from 'utils/helper';
 | |
| import {
 | |
|   CButton,
 | |
|   CCard,
 | |
|   CCardBody,
 | |
|   CCardHeader,
 | |
|   CCol,
 | |
|   CModal,
 | |
|   CModalHeader,
 | |
|   CModalBody,
 | |
|   CModalTitle,
 | |
|   CRow,
 | |
|   CPopover,
 | |
| } from '@coreui/react';
 | |
| import CIcon from '@coreui/icons-react';
 | |
| import { cilSync, cilX } from '@coreui/icons';
 | |
| import RadioAnalysisTable from './RadioAnalysis';
 | |
| import WifiAnalysisTable from './WifiAnalysis';
 | |
| 
 | |
| const parseDbm = (value) => {
 | |
|   if (!value) return '-';
 | |
|   if (value > -150 && value < 100) return value;
 | |
|   return (4294967295 - value) * -1;
 | |
| };
 | |
| 
 | |
| const WifiAnalysis = () => {
 | |
|   const { t } = useTranslation();
 | |
|   const { deviceId } = useParams();
 | |
|   const { currentToken, endpoints } = useAuth();
 | |
|   const [loading, setLoading] = useState(false);
 | |
|   const [showModal, setShowModal] = useState(false);
 | |
|   const [tableTime, setTableTime] = useState('');
 | |
|   const [parsedAssociationStats, setParsedAssociationStats] = useState([]);
 | |
|   const [selectedAssociationStats, setSelectedAssociationStats] = useState(null);
 | |
|   const [parsedRadioStats, setParsedRadioStats] = useState([]);
 | |
|   const [selectedRadioStats, setSelectedRadioStats] = useState(null);
 | |
|   const [range, setRange] = useState(19);
 | |
| 
 | |
|   const toggleModal = () => {
 | |
|     setShowModal(!showModal);
 | |
|   };
 | |
| 
 | |
|   const secondsToLabel = (seconds) =>
 | |
|     compactSecondsToDetailed(seconds, t('common.day'), t('common.days'), t('common.seconds'));
 | |
| 
 | |
|   const extractIp = (json, station) => {
 | |
|     const ips = {
 | |
|       ipV4: [],
 | |
|       ipV6: [],
 | |
|     };
 | |
|     for (const obj of json.interfaces) {
 | |
|       if ('clients' in obj) {
 | |
|         for (const client of obj.clients) {
 | |
|           if (client.mac === station) {
 | |
|             ips.ipV4 = ips.ipV4.concat(client.ipv4_addresses ?? []);
 | |
|             ips.ipV6 = ips.ipV6.concat(client.ipv6_addresses ?? []);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return ips;
 | |
|   };
 | |
| 
 | |
|   const getVendors = async (bssids) => {
 | |
|     setLoading(true);
 | |
|     setRange(19);
 | |
| 
 | |
|     const options = {
 | |
|       headers: {
 | |
|         Accept: 'application/json',
 | |
|         Authorization: `Bearer ${currentToken}`,
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     return axiosInstance
 | |
|       .get(`${endpoints.owgw}/api/v1/ouis?macList=${bssids.join(',')}`, options)
 | |
|       .then((response) => {
 | |
|         const newObj = bssids;
 | |
|         for (const tag of response.data.tagList) {
 | |
|           newObj[tag.tag] = tag.value === '' ? '-' : tag.value;
 | |
|         }
 | |
|         return newObj;
 | |
|       })
 | |
|       .catch(() => ({}));
 | |
|   };
 | |
| 
 | |
|   const parseAssociationStats = async (json) => {
 | |
|     const newParsedAssociationStats = [];
 | |
|     const newParsedRadioStats = [];
 | |
|     const bssidObj = {};
 | |
| 
 | |
|     for (const stat of json.data) {
 | |
|       const associations = [];
 | |
|       const radios = [];
 | |
|       const timeStamp = prettyDate(stat.recorded);
 | |
| 
 | |
|       if (stat.data.radios !== undefined) {
 | |
|         for (let i = 0; i < stat.data.radios.length; i += 1) {
 | |
|           const radio = stat.data.radios[i];
 | |
|           radios.push({
 | |
|             timeStamp,
 | |
|             radio: i,
 | |
|             channel: radio.channel,
 | |
|             channelWidth: radio.channel_width,
 | |
|             noise: radio.noise ? parseDbm(radio.noise) : '-',
 | |
|             txPower: radio.tx_power ?? '-',
 | |
|             activeMs: secondsToLabel(radio?.active_ms ? Math.floor(radio.active_ms / 1000) : 0),
 | |
|             busyMs: secondsToLabel(radio?.busy_ms ? Math.floor(radio.busy_ms / 1000) : 0),
 | |
|             receiveMs: secondsToLabel(radio?.receive_ms ? Math.floor(radio.receive_ms / 1000) : 0),
 | |
|           });
 | |
|         }
 | |
|         newParsedRadioStats.push(radios);
 | |
|       }
 | |
| 
 | |
|       // Looping through the interfaces
 | |
|       for (const deviceInterface of stat.data.interfaces) {
 | |
|         if ('ssids' in deviceInterface) {
 | |
|           for (const ssid of deviceInterface.ssids) {
 | |
|             // Information common between all associations
 | |
|             const radioInfo = {
 | |
|               found: false,
 | |
|             };
 | |
| 
 | |
|             if (ssid.phy !== undefined) {
 | |
|               radioInfo.radio = stat.data.radios.findIndex((element) => element.phy === ssid.phy);
 | |
|               radioInfo.found = radioInfo.radio !== undefined;
 | |
|               radioInfo.radioIndex = radioInfo.radio;
 | |
|             }
 | |
| 
 | |
|             if (!radioInfo.found && ssid.radio !== undefined) {
 | |
|               const radioArray = ssid.radio.$ref.split('/');
 | |
|               const radioIndex = radioArray !== undefined ? radioArray[radioArray.length - 1] : '-';
 | |
|               radioInfo.found = stat.data.radios[radioIndex] !== undefined;
 | |
|               radioInfo.radio = radioIndex;
 | |
|               radioInfo.radioIndex = radioIndex;
 | |
|             }
 | |
| 
 | |
|             if (!radioInfo.found) {
 | |
|               radioInfo.radio = '-';
 | |
|             }
 | |
| 
 | |
|             if ('associations' in ssid) {
 | |
|               for (const association of ssid.associations) {
 | |
|                 bssidObj[association.station] = 0;
 | |
| 
 | |
|                 const data = {
 | |
|                   radio: radioInfo,
 | |
|                   ...extractIp(stat.data, association.station),
 | |
|                   station: association.station,
 | |
|                   ssid: ssid.ssid,
 | |
|                   rssi: association.rssi ? parseDbm(association.rssi) : '-',
 | |
|                   mode: ssid.mode,
 | |
|                   rxBytes: cleanBytesString(association.rx_bytes, 0),
 | |
|                   rxRate: association.rx_rate.bitrate,
 | |
|                   rxMcs: association.rx_rate.mcs ?? '-',
 | |
|                   rxNss: association.rx_rate.nss ?? '-',
 | |
|                   txBytes: cleanBytesString(association.tx_bytes, 0),
 | |
|                   txMcs: association.tx_rate.mcs ?? '-',
 | |
|                   txNss: association.tx_rate.nss ?? '-',
 | |
|                   txRate: association.tx_rate.bitrate ?? '-',
 | |
|                   timeStamp,
 | |
|                 };
 | |
|                 associations.push(data);
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       newParsedAssociationStats.push(associations);
 | |
|     }
 | |
| 
 | |
|     // Adding Vendor info to associations
 | |
|     const vendors = await getVendors(Object.keys(bssidObj));
 | |
| 
 | |
|     for (let i = 0; i < newParsedAssociationStats.length; i += 1) {
 | |
|       for (let y = 0; y < newParsedAssociationStats[i].length; y += 1) {
 | |
|         newParsedAssociationStats[i][y].vendor =
 | |
|           vendors[newParsedAssociationStats[i][y].station] ?? '-';
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Radio Stats
 | |
|     const ascOrderedRadioStats = newParsedRadioStats.reverse();
 | |
|     setParsedRadioStats(ascOrderedRadioStats);
 | |
|     setSelectedRadioStats(ascOrderedRadioStats[ascOrderedRadioStats.length - 1]);
 | |
| 
 | |
|     const ascOrderedAssociationStats = newParsedAssociationStats.reverse();
 | |
|     setParsedAssociationStats(ascOrderedAssociationStats);
 | |
|     setSelectedAssociationStats(ascOrderedAssociationStats[ascOrderedAssociationStats.length - 1]);
 | |
| 
 | |
|     setRange(ascOrderedRadioStats.length > 0 ? ascOrderedRadioStats.length - 1 : 0);
 | |
|     setTableTime(
 | |
|       ascOrderedRadioStats.length > 0
 | |
|         ? ascOrderedRadioStats[ascOrderedRadioStats.length - 1][0]?.timeStamp
 | |
|         : '',
 | |
|     );
 | |
|     setLoading(false);
 | |
|   };
 | |
| 
 | |
|   const getLatestAssociationStats = () => {
 | |
|     setLoading(true);
 | |
|     setRange(19);
 | |
| 
 | |
|     const options = {
 | |
|       headers: {
 | |
|         Accept: 'application/json',
 | |
|         Authorization: `Bearer ${currentToken}`,
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     axiosInstance
 | |
|       .get(`${endpoints.owgw}/api/v1/device/${deviceId}/statistics?newest=true&limit=20`, options)
 | |
|       .then((response) => {
 | |
|         parseAssociationStats(response.data);
 | |
|       })
 | |
|       .catch(() => {
 | |
|         setLoading(false);
 | |
|       });
 | |
|   };
 | |
| 
 | |
|   const updateSelectedStats = (index) => {
 | |
|     setTableTime(parsedRadioStats[index][0].timeStamp);
 | |
|     setSelectedAssociationStats(parsedAssociationStats[index]);
 | |
|     setSelectedRadioStats(parsedRadioStats[index]);
 | |
|   };
 | |
| 
 | |
|   useEffect(() => {
 | |
|     if (deviceId && deviceId.length > 0) {
 | |
|       getLatestAssociationStats();
 | |
|     }
 | |
|   }, [deviceId]);
 | |
| 
 | |
|   return (
 | |
|     <div>
 | |
|       <CCard className="mb-0">
 | |
|         <CCardHeader className="dark-header d-flex flex-row-reverse align-items-center">
 | |
|           <div className="pl-2">
 | |
|             <CPopover content={t('common.refresh')}>
 | |
|               <CButton size="sm" color="info" onClick={getLatestAssociationStats}>
 | |
|                 <CIcon content={cilSync} />
 | |
|               </CButton>
 | |
|             </CPopover>
 | |
|           </div>
 | |
|           <div>
 | |
|             <CButton color="info" size="sm" onClick={toggleModal}>
 | |
|               {t('wifi_analysis.network_diagram')}
 | |
|             </CButton>
 | |
|           </div>
 | |
|         </CCardHeader>
 | |
|         <CCardBody>
 | |
|           {!loading && parsedAssociationStats.length === 0 ? (
 | |
|             <div className="text-center">
 | |
|               <h3>{t('wifi_analysis.waiting_for_data')}</h3>
 | |
|             </div>
 | |
|           ) : (
 | |
|             <>
 | |
|               <CRow className="mb-4">
 | |
|                 <CCol className="text-center">
 | |
|                   <input
 | |
|                     type="range"
 | |
|                     style={{ width: '80%' }}
 | |
|                     className="form-range"
 | |
|                     min="0"
 | |
|                     max={range}
 | |
|                     step="1"
 | |
|                     onChange={(e) => updateSelectedStats(e.target.value)}
 | |
|                     defaultValue={range}
 | |
|                     disabled={!selectedRadioStats}
 | |
|                   />
 | |
|                   <h5>
 | |
|                     {t('common.timestamp')}: {tableTime}
 | |
|                   </h5>
 | |
|                 </CCol>
 | |
|               </CRow>
 | |
|               <div className="overflow-auto" style={{ height: 'calc(100vh - 300px)' }}>
 | |
|                 <h5 className="pb-3 text-center">{t('wifi_analysis.radios')}</h5>
 | |
|                 <RadioAnalysisTable
 | |
|                   data={selectedRadioStats ?? []}
 | |
|                   loading={loading}
 | |
|                   range={range}
 | |
|                 />
 | |
|                 <h5 className="pt-5 pb-3 text-center">{t('wifi_analysis.associations')}</h5>
 | |
|                 <WifiAnalysisTable
 | |
|                   t={t}
 | |
|                   data={selectedAssociationStats ?? []}
 | |
|                   loading={loading}
 | |
|                   range={range}
 | |
|                 />
 | |
|               </div>
 | |
|             </>
 | |
|           )}
 | |
|         </CCardBody>
 | |
|       </CCard>
 | |
|       <CModal size="xl" show={showModal} onClose={toggleModal}>
 | |
|         <CModalHeader className="p-1">
 | |
|           <CModalTitle className="pl-1 pt-1">{t('wifi_analysis.network_diagram')}</CModalTitle>
 | |
|           <div className="text-right">
 | |
|             <CPopover content={t('common.close')}>
 | |
|               <CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
 | |
|                 <CIcon content={cilX} />
 | |
|               </CButton>
 | |
|             </CPopover>
 | |
|           </div>
 | |
|         </CModalHeader>
 | |
|         <CModalBody>
 | |
|           {showModal ? (
 | |
|             <NetworkDiagram
 | |
|               show={showModal}
 | |
|               radios={selectedRadioStats}
 | |
|               associations={selectedAssociationStats}
 | |
|             />
 | |
|           ) : (
 | |
|             <div />
 | |
|           )}
 | |
|         </CModalBody>
 | |
|       </CModal>
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default WifiAnalysis;
 | 
