[WIFI-10904] Connection statistics on the sidebar

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2022-09-22 19:54:21 +01:00
parent c6dee2252b
commit 8ead4c4708
9 changed files with 218 additions and 6 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.7.0(7)",
"version": "2.7.0(8)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.7.0(7)",
"version": "2.7.0(8)",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.7.0(7)",
"version": "2.7.0(8)",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",

View File

@@ -11,7 +11,7 @@ const WebSocketContext = React.createContext({
addDeviceListener: () => {},
});
export const WebSocketProvider = ({ children }) => {
export const WebSocketProvider = ({ children, setNewConnectionData }) => {
const { currentToken, endpoints } = useAuth();
const [isOpen, setIsOpen] = useState(false);
const ws = useRef(undefined);
@@ -20,6 +20,9 @@ export const WebSocketProvider = ({ children }) => {
const onMessage = useCallback((message) => {
const result = extractWebSocketResponse(message);
if (result?.type === 'device_connections_statistics') {
setNewConnectionData(result.content);
}
if (result?.type === 'NOTIFICATION') {
dispatch({ type: 'NEW_NOTIFICATION', notification: result.notification });
pushNotification(result.notification);
@@ -84,6 +87,7 @@ export const WebSocketProvider = ({ children }) => {
WebSocketProvider.propTypes = {
children: PropTypes.node.isRequired,
setNewConnectionData: PropTypes.func.isRequired,
};
export const useGlobalWebSocket = () => React.useContext(WebSocketContext);

View File

@@ -26,6 +26,9 @@ export const extractWebSocketResponse = (message) => {
if (data.command_response_id) {
return { data, type: 'COMMAND' };
}
if (data.notification.type === 'device_connections_statistics') {
return { content: data.notification.content, type: 'device_connections_statistics' };
}
} catch {
return undefined;
}

120
src/layout/Devices.js Normal file
View File

@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import axiosInstance from 'utils/axiosInstance';
import { useAuth } from 'ucentral-libs';
import { CPopover } from '@coreui/react';
import { extraCompactSecondsToDetailed, secondsToDetailed } from 'utils/helper';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
const propTypes = {
newData: PropTypes.instanceOf(Object),
};
const defaultProps = {
newData: undefined,
};
const SidebarDevices = ({ newData }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [stats, setStats] = useState();
const [lastUpdate, setLastUpdate] = useState();
const [lastTime, setLastTime] = useState();
const getInitialStats = async () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/devices?connectionStatistics=true`, options)
.then(({ data }) => {
setStats(data);
setLastUpdate(new Date());
})
.catch(() => {});
};
const getTime = () => {
if (lastTime === undefined || lastUpdate === undefined) return null;
const seconds = lastTime.getTime() - lastUpdate.getTime();
return Math.max(0, Math.floor(seconds / 1000));
};
useEffect(() => {
if (newData !== undefined && Object.keys(newData).length > 0) {
setStats({ ...newData });
setLastUpdate(new Date());
}
}, [newData]);
useEffect(() => {
getInitialStats();
}, []);
useEffect(() => {
const interval = setInterval(() => {
setLastTime(new Date());
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
if (!stats) {
return null;
}
return (
<div
style={{
position: 'absolute',
bottom: '0px',
width: '100%',
background: '#2f3d54 !important',
backgroundColor: '#2f3d54 !important',
borderTop: '3px solid #d8dbe0',
color: 'white',
textAlign: 'center',
paddingTop: '5px',
paddingBottom: '5px',
}}
>
<h3 style={{ marginBottom: '0px' }}>{stats?.connectedDevices ?? stats?.numberOfDevices}</h3>
<h6>Connected Devices</h6>
<CPopover
content={secondsToDetailed(
stats?.averageConnectionTime ?? stats?.averageConnectedTime,
t('common.day'),
t('common.days'),
t('common.hour'),
t('common.hours'),
t('common.minute'),
t('common.minutes'),
t('common.second'),
t('common.seconds'),
)}
>
<h3 style={{ marginBottom: '0px' }}>
{extraCompactSecondsToDetailed(
stats?.averageConnectionTime ?? stats?.averageConnectedTime,
t('common.day'),
t('common.days'),
t('common.seconds'),
)}
</h3>
</CPopover>
<h6>Avg. Connection Time</h6>
<h7 style={{ color: '#ebedef', fontStyle: 'italic' }}>{getTime()} seconds ago</h7>
</div>
);
};
SidebarDevices.propTypes = propTypes;
SidebarDevices.defaultProps = defaultProps;
export default React.memo(SidebarDevices);

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { CSidebar, CSidebarBrand, CSidebarNav } from '@coreui/react';
import PropTypes from 'prop-types';
import styles from './index.module.scss';
const Sidebar = ({
showSidebar,
setShowSidebar,
logo,
options,
redirectTo,
logoHeight,
logoWidth,
}) => (
<CSidebar show={showSidebar} onShowChange={(val) => setShowSidebar(val)}>
<CSidebarBrand className="d-md-down-none" to={redirectTo}>
<img
className={[styles.sidebarImgFull, 'c-sidebar-brand-full'].join(' ')}
style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }}
src={logo}
alt="OpenWifi"
/>
<img
className={[styles.sidebarImgMinimized, 'c-sidebar-brand-minimized'].join(' ')}
style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }}
src={logo}
alt="OpenWifi"
/>
</CSidebarBrand>
<CSidebarNav>{options}</CSidebarNav>
</CSidebar>
);
Sidebar.propTypes = {
showSidebar: PropTypes.string.isRequired,
setShowSidebar: PropTypes.func.isRequired,
logo: PropTypes.string.isRequired,
options: PropTypes.node.isRequired,
redirectTo: PropTypes.string.isRequired,
logoHeight: PropTypes.string,
logoWidth: PropTypes.string,
};
Sidebar.defaultProps = {
logoHeight: null,
logoWidth: null,
};
export default React.memo(Sidebar);

View File

@@ -0,0 +1,9 @@
.sidebarImgFull {
height: 75px;
width: 175px;
}
.sidebarImgMinimized {
height: 75px;
width: 75px;
}

View File

@@ -4,13 +4,20 @@ import routes from 'routes';
import { CSidebarNavItem } from '@coreui/react';
import { cilBarcode, cilRouter, cilSave, cilSettings, cilPeople } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { Header, Sidebar, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs';
import { Header, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs';
import { WebSocketProvider } from 'contexts/WebSocketProvider';
import Sidebar from './Sidebar';
import SidebarDevices from './Devices';
const TheLayout = () => {
const [showSidebar, setShowSidebar] = useState('responsive');
const { endpoints, currentToken, user, avatar, logout } = useAuth();
const { t, i18n } = useTranslation();
const [newConnectionData, setNewConnectionData] = useState();
const onConnectionDataChange = React.useCallback((newData) => {
setNewConnectionData({ ...newData });
}, []);
return (
<div className="c-app c-default-layout">
@@ -50,6 +57,7 @@ const TheLayout = () => {
to="/system"
icon={<CIcon content={cilSettings} size="xl" className="mr-3" />}
/>
<SidebarDevices newData={newConnectionData} />
</>
}
redirectTo="/devices"
@@ -71,7 +79,7 @@ const TheLayout = () => {
/>
<div className="c-body">
<ToastProvider>
<WebSocketProvider>
<WebSocketProvider setNewConnectionData={onConnectionDataChange}>
<PageContainer t={t} routes={routes} redirectTo="/devices" />
</WebSocketProvider>
</ToastProvider>

View File

@@ -130,6 +130,25 @@ export const compactSecondsToDetailed = (seconds, dayLabel, daysLabel, secondsLa
return finalString;
};
export const extraCompactSecondsToDetailed = (seconds) => {
let secondsLeft = seconds;
const days = Math.floor(secondsLeft / (3600 * 24));
secondsLeft -= days * (3600 * 24);
const hours = Math.floor(secondsLeft / 3600);
secondsLeft -= hours * 3600;
const minutes = Math.floor(secondsLeft / 60);
secondsLeft -= minutes * 60;
let finalString = '';
finalString = `${finalString}${prettyNumber(days)}:`;
finalString = `${finalString}${prettyNumber(hours)}:`;
finalString = `${finalString}${prettyNumber(minutes)}:`;
finalString = `${finalString}${prettyNumber(secondsLeft)}`;
return finalString;
};
export const validateEmail = (email) => {
const regex = /\S+@\S+\.\S+/;
return regex.test(email);