mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 09:22:21 +00:00
Merge pull request #110 from stephb9959/main
[WIFI-10904] Connection statistics on the sidebar
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
120
src/layout/Devices.js
Normal 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);
|
||||
49
src/layout/Sidebar/index.js
Normal file
49
src/layout/Sidebar/index.js
Normal 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);
|
||||
9
src/layout/Sidebar/index.module.scss
Normal file
9
src/layout/Sidebar/index.module.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.sidebarImgFull {
|
||||
height: 75px;
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.sidebarImgMinimized {
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user