Merge pull request #28 from stephb9959/main

2.6.88
This commit is contained in:
Charles
2022-03-28 21:32:20 +01:00
committed by GitHub
19 changed files with 286 additions and 115 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "wlan-cloud-owprov-ui",
"version": "2.6.83",
"version": "2.6.88",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "wlan-cloud-owprov-ui",
"version": "2.6.83",
"version": "2.6.88",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^1.1.1",

View File

@@ -1,6 +1,6 @@
{
"name": "wlan-cloud-owprov-ui",
"version": "2.6.83",
"version": "2.6.88",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -27,6 +27,7 @@
"verify_phone_number": "Verify your phone number"
},
"analytics": {
"airtime": "Airtime",
"analyze_sub_venues": "Monitor Sub Venues",
"associations": "Associations",
"associations_explanation": "Total associations",
@@ -36,7 +37,9 @@
"average_memory_explanation": "Average percentage of used memory",
"average_uptime": "Average Uptime",
"average_uptime_explanation": "Average device uptime (DD:HH:MM:SS)",
"band": "Band",
"board": "Analytics Board",
"channel": "Channel",
"connected": "connected",
"connection_explanation": "{{connectedCount}} connected, {{disconnectedCount}} not connected",
"connection_percentage": "{{count}}% connected",
@@ -51,11 +54,15 @@
"interval": "Interval",
"last_connection": "Last Connection",
"last_contact": "Last Contact",
"last_disconnection": "Last Disconnection",
"last_firmware_explanation": "Most common firmware running on the devices analyzed",
"last_health": "Last Health",
"last_ping": "Last Ping",
"live_view": "Live View",
"memory": "Memory",
"memory_used": "Memory Used",
"noise": "Noise",
"radio": "Radio",
"raw_analytics_data": "Raw Analytics Data",
"raw_data": "Raw Data",
"retention": "Retention",

View File

@@ -20,10 +20,10 @@ import {
} from '@chakra-ui/react';
import routes from 'router/routes';
import { useAuth } from 'contexts/AuthProvider';
import { t } from 'i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { uppercaseFirstLetter } from 'utils/stringHelper';
import { MapTrifold } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
const propTypes = {
secondary: PropTypes.bool.isRequired,
@@ -32,6 +32,7 @@ const propTypes = {
};
const Navbar = ({ secondary, toggleSidebar, isSidebarOpen }) => {
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const [scrolled, setScrolled] = useState(false);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
import { t } from 'i18next';
import { UploadSimple } from 'phosphor-react';
import ImportConfigurationModal from './Modal';
@@ -11,6 +11,7 @@ const propTypes = {
};
const ImportConfigurationButton = ({ setConfig, isDisabled }) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const breakpoint = useBreakpoint();

View File

@@ -21,7 +21,7 @@ import {
} from '@chakra-ui/react';
import isEqual from 'react-fast-compare';
import { ArrowSquareOut, Circle, Heart } from 'phosphor-react';
import { t } from 'i18next';
import { useTranslation } from 'react-i18next';
import { useGetGatewayUi } from 'hooks/Network/Endpoints';
import AssociationsTable from './AssociationsTable';
@@ -46,6 +46,7 @@ const propTypes = {
};
const DeviceNode = ({ data, isConnectable }) => {
const { t } = useTranslation();
const bgColor = useColorModeValue('blue.200', 'blue.200');
const { data: gwUi } = useGetGatewayUi();

View File

@@ -19,7 +19,7 @@ import {
} from '@chakra-ui/react';
import isEqual from 'react-fast-compare';
import { WifiHigh } from 'phosphor-react';
import { t } from 'i18next';
import { useTranslation } from 'react-i18next';
const propTypes = {
data: PropTypes.shape({
@@ -31,6 +31,7 @@ const propTypes = {
};
const EntityNode = ({ data, isConnectable }) => {
const { t } = useTranslation();
const bgColor = useColorModeValue('teal.200', 'teal.400');
if (data?.id === '0000-0000-0000') {

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { useColorMode } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import SimpleStatDisplay from 'components/StatisticsDisplay/SimpleStatDisplay';
import { errorColor, successColor, warningColor } from 'utils/colors';
const propTypes = {
data: PropTypes.instanceOf(Object).isRequired,
@@ -15,9 +14,11 @@ const HealthStat = ({ data, handleModalClick }) => {
const { colorMode } = useColorMode();
const getHealthColor = () => {
if (data.avgHealth >= 90) return successColor(colorMode);
if (data.avgHealth >= 70) return warningColor(colorMode);
return errorColor(colorMode);
if (data.avgHealth >= 90)
return colorMode === 'light' ? 'var(--chakra-colors-green-200)' : 'var(--chakra-colors-green-400)';
if (data.avgHealth >= 70)
return colorMode === 'light' ? 'var(--chakra-colors-yellow-200)' : 'var(--chakra-colors-yellow-400)';
return colorMode === 'light' ? 'var(--chakra-colors-red-200)' : 'var(--chakra-colors-red-400)';
};
return (

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { useColorMode } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import SimpleStatDisplay from 'components/StatisticsDisplay/SimpleStatDisplay';
import { errorColor, successColor, warningColor } from 'utils/colors';
const propTypes = {
data: PropTypes.instanceOf(Object).isRequired,
@@ -15,9 +14,11 @@ const MemoryStat = ({ data, handleModalClick }) => {
const { colorMode } = useColorMode();
const getMemoryColor = () => {
if (data.avgMemoryUsed < 65) return successColor(colorMode);
if (data.avgMemoryUsed < 80) return warningColor(colorMode);
return errorColor(colorMode);
if (data.avgMemoryUsed < 65)
return colorMode === 'light' ? 'var(--chakra-colors-green-200)' : 'var(--chakra-colors-green-400)';
if (data.avgMemoryUsed < 80)
return colorMode === 'light' ? 'var(--chakra-colors-yellow-200)' : 'var(--chakra-colors-yellow-400)';
return colorMode === 'light' ? 'var(--chakra-colors-red-200)' : 'var(--chakra-colors-red-400)';
};
return (

View File

@@ -48,7 +48,7 @@ const AssociationCircle = ({ node, style, handleClicks }) => (
<Text ml={2}>{node?.data?.name.split('/')[0]}</Text>
</PopoverHeader>
<PopoverBody>
<Heading size="sm">RSSI {node.data.details.rssi}</Heading>
<Heading size="sm">RSSI {node.data.details.rssi} db</Heading>
<Heading size="sm">TX {bytesString(node.data.details.tx_bytes)}</Heading>
<Heading size="sm">RX {bytesString(node.data.details.rx_bytes)}</Heading>
</PopoverBody>

View File

@@ -15,9 +15,10 @@ import {
Text,
Tooltip,
} from '@chakra-ui/react';
import { t } from 'i18next';
import { ArrowSquareOut, Tag } from 'phosphor-react';
import { useGetGatewayUi } from 'hooks/Network/Endpoints';
import FormattedDate from 'components/FormattedDate';
import { useTranslation } from 'react-i18next';
const propTypes = {
node: PropTypes.instanceOf(Object).isRequired,
@@ -28,6 +29,7 @@ const propTypes = {
};
const DeviceCircle = ({ node, style, handleClicks }) => {
const { t } = useTranslation();
const { data: gwUi } = useGetGatewayUi();
const handleOpenInGateway = useMemo(
@@ -72,6 +74,24 @@ const DeviceCircle = ({ node, style, handleClicks }) => {
<Heading size="sm">
{node.data.details.deviceInfo.health}% {t('analytics.health')}
</Heading>
<Heading size="sm">
{t('analytics.memory_used')}: {Math.floor(node.data.details.deviceInfo.memory)}%
</Heading>
<Heading size="sm">
{node.data.details.deviceInfo.associations_2g} 2G {t('analytics.associations')}
</Heading>
<Heading size="sm">
{node.data.details.deviceInfo.associations_5g} 5G {t('analytics.associations')}
</Heading>
<Heading size="sm">
{node.data.details.deviceInfo.associations_6g} 6G {t('analytics.associations')}
</Heading>
{node.data.details.deviceInfo.lastDisconnection !== 0 && (
<Heading size="sm">
{t('analytics.last_disconnection')}:{' '}
<FormattedDate date={node.data.details.deviceInfo.lastDisconnection} />
</Heading>
)}
</PopoverBody>
</PopoverContent>
</Portal>

View File

@@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import { animated } from '@react-spring/web';
import {
Heading,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Portal,
Text,
} from '@chakra-ui/react';
import { Radio } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
const propTypes = {
node: PropTypes.instanceOf(Object).isRequired,
handleClicks: PropTypes.shape({
onClick: PropTypes.func.isRequired,
}).isRequired,
style: PropTypes.instanceOf(Object).isRequired,
};
const RadioCircle = ({ node, style, handleClicks }) => {
const { t } = useTranslation();
return (
<Popover isLazy trigger="hover" placement="top">
<PopoverTrigger>
<animated.circle
key={node.id}
cx={style.x}
cy={style.y}
r={style.radius}
fill={node.data.details.color}
stroke="black"
strokeWidth="1px"
opacity={style.opacity}
onClick={handleClicks.onClick}
/>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton alignContent="center" mt={1} />
<PopoverHeader display="flex">
<Radio size={24} weight="fill" />
<Text ml={2}>
{t('analytics.band')} {node.data.details.band}
</Text>
</PopoverHeader>
<PopoverBody>
<Heading size="sm">
{t('analytics.channel')}: {node.data.details.channel}
</Heading>
<Heading size="sm">
{t('analytics.airtime')}: {Math.floor(node.data.details.transmitPct)}%
</Heading>
<Heading size="sm">
{t('analytics.noise')}: {node.data.details.noise} db
</Heading>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
RadioCircle.propTypes = propTypes;
export default React.memo(RadioCircle);

View File

@@ -13,8 +13,8 @@ import {
Portal,
Text,
} from '@chakra-ui/react';
import { t } from 'i18next';
import { Broadcast } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
const propTypes = {
node: PropTypes.instanceOf(Object).isRequired,
@@ -24,42 +24,45 @@ const propTypes = {
style: PropTypes.instanceOf(Object).isRequired,
};
const SsidCircle = ({ node, style, handleClicks }) => (
<Popover isLazy trigger="hover" placement="top">
<PopoverTrigger>
<animated.circle
key={node.id}
cx={style.x}
cy={style.y}
r={style.radius}
fill={node.data.details.color}
stroke="black"
strokeWidth="1px"
opacity={style.opacity}
onClick={handleClicks.onClick}
/>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton alignContent="center" mt={1} />
<PopoverHeader display="flex">
<Broadcast size={24} weight="fill" />
<Text ml={2}>
{node.data.details.band}G - {node?.data?.name.split('/')[0]}
</Text>
</PopoverHeader>
<PopoverBody>
<Heading size="sm">BSSID {node.data.details.bssid}</Heading>
<Heading size="sm">
{node.data.children.length} {t('analytics.associations')}
</Heading>
<Heading size="sm">RSSI {node.data.details.avgRssi}</Heading>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
const SsidCircle = ({ node, style, handleClicks }) => {
const { t } = useTranslation();
return (
<Popover isLazy trigger="hover" placement="top">
<PopoverTrigger>
<animated.circle
key={node.id}
cx={style.x}
cy={style.y}
r={style.radius}
fill={node.data.details.color}
stroke="black"
strokeWidth="1px"
opacity={style.opacity}
onClick={handleClicks.onClick}
/>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton alignContent="center" mt={1} />
<PopoverHeader display="flex">
<Broadcast size={24} weight="fill" />
<Text ml={2}>
{node.data.details.band}G - {node?.data?.name.split('/')[0]}
</Text>
</PopoverHeader>
<PopoverBody>
<Heading size="sm">BSSID: {node.data.details.bssid}</Heading>
<Heading size="sm">
{t('analytics.associations')}: {node.data.children.length}
</Heading>
<Heading size="sm">RSSI: {node.data.details.avgRssi} db</Heading>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
SsidCircle.propTypes = propTypes;
export default React.memo(SsidCircle);

View File

@@ -13,8 +13,8 @@ import {
Portal,
Text,
} from '@chakra-ui/react';
import { t } from 'i18next';
import { Buildings } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
const propTypes = {
node: PropTypes.instanceOf(Object).isRequired,
@@ -24,41 +24,44 @@ const propTypes = {
style: PropTypes.instanceOf(Object).isRequired,
};
const VenueCircle = ({ node, style, handleClicks }) => (
<Popover isLazy trigger="hover" placement="top">
<PopoverTrigger>
<animated.circle
key={node.id}
cx={style.x}
cy={style.y}
r={style.radius}
fill={node.data.details.color}
stroke="black"
strokeWidth="1px"
opacity={style.opacity}
onClick={handleClicks.onClick}
/>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton alignContent="center" mt={1} />
<PopoverHeader display="flex">
<Buildings weight="fill" size={24} />
<Text ml={2}>{node?.data?.name.split('/')[0]}</Text>
</PopoverHeader>
<PopoverBody>
<Heading size="sm">
{node.data.children.length} {t('devices.title')}
</Heading>
<Heading size="sm">
{node.data.details.avgHealth}% {t('analytics.average_health')}
</Heading>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
const VenueCircle = ({ node, style, handleClicks }) => {
const { t } = useTranslation();
return (
<Popover isLazy trigger="hover" placement="top">
<PopoverTrigger>
<animated.circle
key={node.id}
cx={style.x}
cy={style.y}
r={style.radius}
fill={node.data.details.color}
stroke="black"
strokeWidth="1px"
opacity={style.opacity}
onClick={handleClicks.onClick}
/>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton alignContent="center" mt={1} />
<PopoverHeader display="flex">
<Buildings weight="fill" size={24} />
<Text ml={2}>{node?.data?.name.split('/')[0]}</Text>
</PopoverHeader>
<PopoverBody>
<Heading size="sm">
{node.data.children.length} {t('devices.title')}
</Heading>
<Heading size="sm">
{node.data.details.avgHealth}% {t('analytics.average_health')}
</Heading>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
VenueCircle.propTypes = propTypes;
export default React.memo(VenueCircle);

View File

@@ -4,6 +4,7 @@ import AssociationCircle from './AssociationCircle';
import DeviceCircle from './DeviceCircle';
import SsidCircle from './SsidCircle';
import VenueCircle from './VenueCircle';
import RadioCircle from './RadioCircle';
const propTypes = {
node: PropTypes.instanceOf(Object).isRequired,
@@ -23,6 +24,7 @@ const CircleComponent = ({ node, style, onClick }) => {
if (node.data.type === 'association')
return <AssociationCircle node={node} style={style} handleClicks={handleClicks} />;
if (node.data.type === 'ssid') return <SsidCircle node={node} style={style} handleClicks={handleClicks} />;
if (node.data.type === 'radio') return <RadioCircle node={node} style={style} handleClicks={handleClicks} />;
if (node.data.type === 'device') return <DeviceCircle node={node} style={style} handleClicks={handleClicks} />;
if (node.data.type === 'venue') return <VenueCircle node={node} style={style} handleClicks={handleClicks} />;
return null;

View File

@@ -43,7 +43,8 @@ const CirclePack = ({ timepoints, fullscreen }) => {
};
let totalHealth = 0;
for (const { device_info: deviceInfo, ssid_data: ssidData } of timepoints[pointIndex]) {
for (const { device_info: deviceInfo, ssid_data: ssidData, radio_data: radioData } of timepoints[pointIndex]) {
totalHealth += deviceInfo.health;
const finalDevice = {
@@ -60,6 +61,24 @@ const CirclePack = ({ timepoints, fullscreen }) => {
else if (deviceInfo.health >= 70) finalDevice.details.color = warningColor(colorMode);
else finalDevice.details.color = errorColor(colorMode);
const radioChannelIndex = {};
for (const [i, { band, transmit_pct: transmitPct, ...radioDetails }] of radioData.entries()) {
const finalRadio = {
name: `${band}/radio/${uuid()}`,
type: 'radio',
details: {
band,
transmitPct,
...radioDetails,
color: transmitPct > 60 ? 'var(--chakra-colors-danger-400)' : 'var(--chakra-colors-success-600)',
},
children: [],
};
radioChannelIndex[band] = i;
finalDevice.children.push(finalRadio);
}
for (const { ssid, associations, ...ssidDetails } of ssidData) {
const finalSsid = {
name: `${ssid}/ssid/${uuid()}`,
@@ -70,7 +89,7 @@ const CirclePack = ({ timepoints, fullscreen }) => {
...ssidDetails,
},
children: [],
scale: associations.length === 0 ? 1 : associations.length * 4,
scale: 1,
};
let totalRssi = 0;
@@ -83,7 +102,7 @@ const CirclePack = ({ timepoints, fullscreen }) => {
rssi: parseDbm(rssi),
...associationDetails,
},
scale: 1,
scale: Math.max(1, Math.floor((associationDetails.tx_bytes_bw + associationDetails.rx_bytes_bw) / 1000)),
};
if (rssi >= -45) finalAssociation.details.color = successColor(colorMode);
@@ -97,10 +116,8 @@ const CirclePack = ({ timepoints, fullscreen }) => {
if (finalSsid.details.avgRssi >= -45) finalSsid.details.color = successColor(colorMode);
else if (finalSsid.details.avgRssi >= -60) finalSsid.details.color = warningColor(colorMode);
else finalSsid.details.color = errorColor(colorMode);
finalDevice.children.push(finalSsid);
finalDevice.children[radioChannelIndex[ssidDetails.band]].children.push(finalSsid);
}
root.children.push(finalDevice);
}
@@ -132,7 +149,7 @@ const CirclePack = ({ timepoints, fullscreen }) => {
value="scale"
data={data}
enableLabels
labelsSkipRadius={32}
labelsSkipRadius={42}
labelsFilter={(label) => label.node.height === 0}
labelTextColor={{
from: 'color',

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import DateTimePicker from 'components/DatePickers/DateTimePicker';
import { Flex, Heading } from '@chakra-ui/react';
import { t } from 'i18next';
import { useTranslation } from 'react-i18next';
const propTypes = {
start: PropTypes.instanceOf(Date).isRequired,
@@ -12,22 +12,26 @@ const propTypes = {
isDisabled: PropTypes.bool.isRequired,
};
const CirclePackTimePickers = ({ start, end, setStart, setEnd, isDisabled }) => (
<Flex>
const CirclePackTimePickers = ({ start, end, setStart, setEnd, isDisabled }) => {
const { t } = useTranslation();
return (
<Flex>
<Heading size="sm" mt="10px" mr={2}>
{t('common.start')}:
</Heading>
<DateTimePicker date={start} isStart onChange={setStart} isDisabled={isDisabled} />
<Flex>
<Heading size="sm" mt="10px" mr={2}>
{t('common.start')}:
</Heading>
<DateTimePicker date={start} isStart onChange={setStart} isDisabled={isDisabled} />
</Flex>
<Flex ml={2}>
<Heading size="sm" mt="10px" mr={2}>
{t('common.end')}:
</Heading>
<DateTimePicker date={end} isEnd onChange={setEnd} startDate={start} endDate={end} isDisabled={isDisabled} />
</Flex>
</Flex>
<Flex ml={2}>
<Heading size="sm" mt="10px" mr={2}>
{t('common.end')}:
</Heading>
<DateTimePicker date={end} isEnd onChange={setEnd} startDate={start} endDate={end} isDisabled={isDisabled} />
</Flex>
</Flex>
);
);
};
CirclePackTimePickers.propTypes = propTypes;
export default React.memo(CirclePackTimePickers);

View File

@@ -5,6 +5,42 @@ export default {
gray: {
700: '#1f2733',
},
success: {
50: '#deffef',
100: '#b4f8d6',
200: '#89f3bd',
300: '#5deda3',
400: '#31e88a',
500: '#17ce71',
600: '#0ba057',
700: '#02733e',
800: '#004624',
900: '#001908',
},
warning: {
50: '#feffdc',
100: '#fbffaf',
200: '#f9ff7e',
300: '#f6ff4d',
400: '#f4ff1f',
500: '#dae609',
600: '#aab300',
700: '#798000',
800: '#494d00',
900: '#181b00',
},
danger: {
50: '#FFE6E9',
100: '#FEB8C1',
200: '#FE8B99',
300: '#FD5D71',
400: '#FD3049',
500: '#FD0221',
600: '#CA021A',
700: '#980114',
800: '#65010D',
900: '#330007',
},
},
styles: {
global: (props) => ({

View File

@@ -1,6 +1,6 @@
export const successColor = (colorMode = 'light') =>
colorMode === 'light' ? 'var(--chakra-colors-green-200)' : 'var(--chakra-colors-green-400)';
colorMode === 'light' ? 'var(--chakra-colors-success-600)' : 'var(--chakra-colors-success-600)';
export const warningColor = (colorMode = 'light') =>
colorMode === 'light' ? 'var(--chakra-colors-yellow-200)' : 'var(--chakra-colors-yellow-400)';
colorMode === 'light' ? 'var(--chakra-colors-warning-400)' : 'var(--chakra-colors-warning-400)';
export const errorColor = (colorMode = 'light') =>
colorMode === 'light' ? 'var(--chakra-colors-red-200)' : 'var(--chakra-colors-red-400)';
colorMode === 'light' ? 'var(--chakra-colors-danger-400)' : 'var(--chakra-colors-danger-400)';