mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-30 17:57:46 +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",
|
"name": "ucentral-client",
|
||||||
"version": "2.7.0(7)",
|
"version": "2.7.0(8)",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.7.0(7)",
|
"version": "2.7.0(8)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coreui/coreui": "^3.4.0",
|
"@coreui/coreui": "^3.4.0",
|
||||||
"@coreui/icons": "^2.0.1",
|
"@coreui/icons": "^2.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.7.0(7)",
|
"version": "2.7.0(8)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coreui/coreui": "^3.4.0",
|
"@coreui/coreui": "^3.4.0",
|
||||||
"@coreui/icons": "^2.0.1",
|
"@coreui/icons": "^2.0.1",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const WebSocketContext = React.createContext({
|
|||||||
addDeviceListener: () => {},
|
addDeviceListener: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WebSocketProvider = ({ children }) => {
|
export const WebSocketProvider = ({ children, setNewConnectionData }) => {
|
||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const ws = useRef(undefined);
|
const ws = useRef(undefined);
|
||||||
@@ -20,6 +20,9 @@ export const WebSocketProvider = ({ children }) => {
|
|||||||
|
|
||||||
const onMessage = useCallback((message) => {
|
const onMessage = useCallback((message) => {
|
||||||
const result = extractWebSocketResponse(message);
|
const result = extractWebSocketResponse(message);
|
||||||
|
if (result?.type === 'device_connections_statistics') {
|
||||||
|
setNewConnectionData(result.content);
|
||||||
|
}
|
||||||
if (result?.type === 'NOTIFICATION') {
|
if (result?.type === 'NOTIFICATION') {
|
||||||
dispatch({ type: 'NEW_NOTIFICATION', notification: result.notification });
|
dispatch({ type: 'NEW_NOTIFICATION', notification: result.notification });
|
||||||
pushNotification(result.notification);
|
pushNotification(result.notification);
|
||||||
@@ -84,6 +87,7 @@ export const WebSocketProvider = ({ children }) => {
|
|||||||
|
|
||||||
WebSocketProvider.propTypes = {
|
WebSocketProvider.propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
|
setNewConnectionData: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGlobalWebSocket = () => React.useContext(WebSocketContext);
|
export const useGlobalWebSocket = () => React.useContext(WebSocketContext);
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export const extractWebSocketResponse = (message) => {
|
|||||||
if (data.command_response_id) {
|
if (data.command_response_id) {
|
||||||
return { data, type: 'COMMAND' };
|
return { data, type: 'COMMAND' };
|
||||||
}
|
}
|
||||||
|
if (data.notification.type === 'device_connections_statistics') {
|
||||||
|
return { content: data.notification.content, type: 'device_connections_statistics' };
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
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 { CSidebarNavItem } from '@coreui/react';
|
||||||
import { cilBarcode, cilRouter, cilSave, cilSettings, cilPeople } from '@coreui/icons';
|
import { cilBarcode, cilRouter, cilSave, cilSettings, cilPeople } from '@coreui/icons';
|
||||||
import CIcon from '@coreui/icons-react';
|
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 { WebSocketProvider } from 'contexts/WebSocketProvider';
|
||||||
|
import Sidebar from './Sidebar';
|
||||||
|
import SidebarDevices from './Devices';
|
||||||
|
|
||||||
const TheLayout = () => {
|
const TheLayout = () => {
|
||||||
const [showSidebar, setShowSidebar] = useState('responsive');
|
const [showSidebar, setShowSidebar] = useState('responsive');
|
||||||
const { endpoints, currentToken, user, avatar, logout } = useAuth();
|
const { endpoints, currentToken, user, avatar, logout } = useAuth();
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const [newConnectionData, setNewConnectionData] = useState();
|
||||||
|
|
||||||
|
const onConnectionDataChange = React.useCallback((newData) => {
|
||||||
|
setNewConnectionData({ ...newData });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="c-app c-default-layout">
|
<div className="c-app c-default-layout">
|
||||||
@@ -50,6 +57,7 @@ const TheLayout = () => {
|
|||||||
to="/system"
|
to="/system"
|
||||||
icon={<CIcon content={cilSettings} size="xl" className="mr-3" />}
|
icon={<CIcon content={cilSettings} size="xl" className="mr-3" />}
|
||||||
/>
|
/>
|
||||||
|
<SidebarDevices newData={newConnectionData} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
redirectTo="/devices"
|
redirectTo="/devices"
|
||||||
@@ -71,7 +79,7 @@ const TheLayout = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="c-body">
|
<div className="c-body">
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<WebSocketProvider>
|
<WebSocketProvider setNewConnectionData={onConnectionDataChange}>
|
||||||
<PageContainer t={t} routes={routes} redirectTo="/devices" />
|
<PageContainer t={t} routes={routes} redirectTo="/devices" />
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
|||||||
@@ -130,6 +130,25 @@ export const compactSecondsToDetailed = (seconds, dayLabel, daysLabel, secondsLa
|
|||||||
return finalString;
|
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) => {
|
export const validateEmail = (email) => {
|
||||||
const regex = /\S+@\S+\.\S+/;
|
const regex = /\S+@\S+\.\S+/;
|
||||||
return regex.test(email);
|
return regex.test(email);
|
||||||
|
|||||||
Reference in New Issue
Block a user