From 8ead4c4708193554534ac279ab35aa1322d4d4c4 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 22 Sep 2022 19:54:21 +0100 Subject: [PATCH] [WIFI-10904] Connection statistics on the sidebar Signed-off-by: Charles --- package-lock.json | 4 +- package.json | 2 +- src/contexts/WebSocketProvider/index.js | 6 +- src/contexts/WebSocketProvider/utils.js | 3 + src/layout/Devices.js | 120 ++++++++++++++++++++++++ src/layout/Sidebar/index.js | 49 ++++++++++ src/layout/Sidebar/index.module.scss | 9 ++ src/layout/index.js | 12 ++- src/utils/helper.js | 19 ++++ 9 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 src/layout/Devices.js create mode 100644 src/layout/Sidebar/index.js create mode 100644 src/layout/Sidebar/index.module.scss diff --git a/package-lock.json b/package-lock.json index 94feac1..0988379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ea4dc96..189b234 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/contexts/WebSocketProvider/index.js b/src/contexts/WebSocketProvider/index.js index 1dc3fa5..b687526 100644 --- a/src/contexts/WebSocketProvider/index.js +++ b/src/contexts/WebSocketProvider/index.js @@ -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); diff --git a/src/contexts/WebSocketProvider/utils.js b/src/contexts/WebSocketProvider/utils.js index 9014cd2..a65c9a8 100644 --- a/src/contexts/WebSocketProvider/utils.js +++ b/src/contexts/WebSocketProvider/utils.js @@ -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; } diff --git a/src/layout/Devices.js b/src/layout/Devices.js new file mode 100644 index 0000000..7050630 --- /dev/null +++ b/src/layout/Devices.js @@ -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 ( +
+

{stats?.connectedDevices ?? stats?.numberOfDevices}

+
Connected Devices
+ +

+ {extraCompactSecondsToDetailed( + stats?.averageConnectionTime ?? stats?.averageConnectedTime, + t('common.day'), + t('common.days'), + t('common.seconds'), + )} +

+
+
Avg. Connection Time
+ {getTime()} seconds ago +
+ ); +}; + +SidebarDevices.propTypes = propTypes; +SidebarDevices.defaultProps = defaultProps; + +export default React.memo(SidebarDevices); diff --git a/src/layout/Sidebar/index.js b/src/layout/Sidebar/index.js new file mode 100644 index 0000000..33f523c --- /dev/null +++ b/src/layout/Sidebar/index.js @@ -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, +}) => ( + setShowSidebar(val)}> + + OpenWifi + OpenWifi + + {options} + +); + +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); diff --git a/src/layout/Sidebar/index.module.scss b/src/layout/Sidebar/index.module.scss new file mode 100644 index 0000000..2cadc41 --- /dev/null +++ b/src/layout/Sidebar/index.module.scss @@ -0,0 +1,9 @@ +.sidebarImgFull { + height: 75px; + width: 175px; +} + +.sidebarImgMinimized { + height: 75px; + width: 75px; +} diff --git a/src/layout/index.js b/src/layout/index.js index 8cd98b4..c13038d 100644 --- a/src/layout/index.js +++ b/src/layout/index.js @@ -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 (
@@ -50,6 +57,7 @@ const TheLayout = () => { to="/system" icon={} /> + } redirectTo="/devices" @@ -71,7 +79,7 @@ const TheLayout = () => { />
- + diff --git a/src/utils/helper.js b/src/utils/helper.js index 9815d32..3bd59aa 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -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);