Dashboards, firmware history, device list fixes

This commit is contained in:
BourqueCharles
2021-08-04 15:15:03 -04:00
parent 08b021fd34
commit 7e74679ea2
13 changed files with 780 additions and 199 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-libs",
"version": "0.8.34",
"version": "0.8.43",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-libs",
"version": "0.8.34",
"version": "0.8.43",
"devDependencies": {
"@babel/core": "^7.14.6",
"@babel/plugin-proposal-class-properties": "^7.14.5",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-libs",
"version": "0.8.34",
"version": "0.8.43",
"main": "dist/index.js",
"source": "src/index.js",
"engines": {

View File

@@ -0,0 +1,68 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CPopover } from '@coreui/react';
import styles from './index.module.scss';
const DeviceBadge = ({ t, device, deviceIcons }) => {
const [src, setSrc] = useState('');
const getSrc = () => {
switch (device.deviceType) {
case 'AP':
setSrc(deviceIcons.apIcon);
break;
case 'MESH':
setSrc(deviceIcons.meshIcon);
break;
case 'SWITCH':
setSrc(deviceIcons.internetSwitch);
break;
case 'IOT':
setSrc(deviceIcons.iotIcon);
break;
default:
break;
}
};
const getCertColor = () => {
switch (device.verifiedCertificate) {
case 'VALID_CERTIFICATE':
case 'NO_CERTIFICATE':
return 'bg-danger';
case 'MISMATCH_SERIAL':
return 'bg-warning';
case 'VERIFIED':
return 'bg-success';
default:
return 'bg-danger';
}
};
useEffect(() => {
getSrc();
}, []);
return (
<CPopover content={device.verifiedCertificate} placement="top">
<div className={`c-avatar c-avatar-lg ${getCertColor()}`}>
<img src={src} className={styles.icon} alt={device.deviceType} />
<CPopover content={device.connected ? t('common.connected') : t('common.not_connected')}>
<span
className={
device.connected ? 'c-avatar-status bg-success' : 'c-avatar-status bg-danger'
}
/>
</CPopover>
</div>
</CPopover>
);
};
DeviceBadge.propTypes = {
t: PropTypes.func.isRequired,
device: PropTypes.instanceOf(Object).isRequired,
deviceIcons: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(DeviceBadge);

View File

@@ -0,0 +1,4 @@
.icon {
height: 36px;
width: 36px;
}

View File

@@ -1,16 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CCard, CCardBody, CCardHeader, CCol, CRow } from '@coreui/react';
import { CCard, CCardBody, CCardHeader, CCol, CRow, CWidgetIcon } from '@coreui/react';
import { CChartBar, CChartPie } from '@coreui/react-chartjs';
import { cilClock, cilMedicalCross, cilThumbUp, cilWarning } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { prettyDate } from '../../utils/formatting';
const getColor = (health) => {
const numberHealth = health ? Number(health.replace('%', '')) : 0;
if (numberHealth >= 90) return 'success';
if (numberHealth >= 60) return 'warning';
return 'danger';
};
const getIcon = (health) => {
const numberHealth = health ? Number(health.replace('%', '')) : 0;
if (numberHealth >= 90) return <CIcon width={36} name="cil-thumbs-up" content={cilThumbUp} />;
if (numberHealth >= 60) return <CIcon width={36} name="cil-warning" content={cilWarning} />;
return <CIcon width={36} name="cil-medical-cross" content={cilMedicalCross} />;
};
const DeviceDashboard = ({ t, data }) => (
<div>
<CRow>
<CCol>
<CWidgetIcon
text={t('common.last_dashboard_refresh')}
header={<h2>{data.snapshot ? prettyDate(data.snapshot) : ''}</h2>}
color="info"
iconPadding={false}
>
<CIcon width={36} name="cil-clock" content={cilClock} />
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={t('common.overall_health')}
header={<h2>{data.overallHealth}</h2>}
color={getColor(data.overallHealth)}
iconPadding={false}
>
{getIcon(data.overallHealth)}
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={t('common.devices')}
header={<h2>{data.numberOfDevices}</h2>}
color="primary"
iconPadding={false}
>
<CIcon width={36} name="cil-router" />
</CWidgetIcon>
</CCol>
</CRow>
<CRow>
<CCol>
<CCard>
<CCardHeader>{t('common.device_status')}</CCardHeader>
<CCardBody>
<CChartPie datasets={data.status.datasets} labels={data.status.labels} />
<CChartPie
datasets={data.status.datasets}
labels={data.status.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) => `${ds.datasets[0].data[item.index]}%`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
@@ -18,15 +82,49 @@ const DeviceDashboard = ({ t, data }) => (
<CCard>
<CCardHeader>{t('common.device_health')}</CCardHeader>
<CCardBody>
<CChartPie datasets={data.healths.datasets} labels={data.healths.labels} />
<CChartPie
datasets={data.healths.datasets}
labels={data.healths.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]}${t('common.of_connected')}`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader>{t('common.uptimes')}</CCardHeader>
<CCardHeader>{t('wifi_analysis.associations')}</CCardHeader>
<CCardBody>
<CChartBar datasets={data.upTimes.datasets} labels={data.upTimes.labels} />
<CChartPie
datasets={data.associations.datasets}
labels={data.associations.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]}% of ${
data.totalAssociations
} associations`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
@@ -36,7 +134,24 @@ const DeviceDashboard = ({ t, data }) => (
<CCard>
<CCardHeader>{t('common.vendors')}</CCardHeader>
<CCardBody>
<CChartBar datasets={data.vendors.datasets} labels={data.vendors.labels} />
<CChartBar
datasets={data.vendors.datasets}
labels={data.vendors.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
@@ -44,7 +159,48 @@ const DeviceDashboard = ({ t, data }) => (
<CCard>
<CCardHeader>{t('firmware.device_types')}</CCardHeader>
<CCardBody>
<CChartPie datasets={data.deviceType.datasets} labels={data.deviceType.labels} />
<CChartPie
datasets={data.deviceType.datasets}
labels={data.deviceType.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader>{t('common.uptimes')}</CCardHeader>
<CCardBody>
<CChartBar
datasets={data.upTimes.datasets}
labels={data.upTimes.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
@@ -54,7 +210,22 @@ const DeviceDashboard = ({ t, data }) => (
<CCard>
<CCardHeader>{t('common.certificates')}</CCardHeader>
<CCardBody>
<CChartPie datasets={data.certificates.datasets} labels={data.certificates.labels} />
<CChartPie
datasets={data.certificates.datasets}
labels={data.certificates.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) => `${ds.datasets[0].data[item.index]}%`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
@@ -62,7 +233,24 @@ const DeviceDashboard = ({ t, data }) => (
<CCard>
<CCardHeader>{t('common.commands')}</CCardHeader>
<CCardBody>
<CChartBar datasets={data.commands.datasets} labels={data.commands.labels} />
<CChartBar
datasets={data.commands.datasets}
labels={data.commands.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
@@ -70,7 +258,24 @@ const DeviceDashboard = ({ t, data }) => (
<CCard>
<CCardHeader>{t('common.memory_used')}</CCardHeader>
<CCardBody>
<CChartBar datasets={data.memoryUsed.datasets} labels={data.memoryUsed.labels} />
<CChartBar
datasets={data.memoryUsed.datasets}
labels={data.memoryUsed.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
CButton,
CDataTable,
CModal,
CModalHeader,
@@ -9,6 +10,7 @@ import {
CRow,
CCol,
CInput,
CModalFooter,
} from '@coreui/react';
import { cleanBytesString, prettyDate } from '../../utils/formatting';
import LoadingButton from '../LoadingButton';
@@ -38,7 +40,7 @@ const DeviceFirmwareModal = ({
return (
<CModal show={show} onClose={toggle} size="xl">
<CModalHeader>
<CModalHeader closeButton>
<CModalTitle>#{device?.serialNumber}</CModalTitle>
</CModalHeader>
<CModalBody>
@@ -96,6 +98,11 @@ const DeviceFirmwareModal = ({
<div />
)}
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal>
);
};

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import {
CBadge,
CCardBody,
CDataTable,
CButton,
@@ -17,14 +16,25 @@ import {
CDropdownToggle,
CDropdownMenu,
CDropdownDivider,
CDropdownItem,
CButtonGroup,
} from '@coreui/react';
import { cilSync, cilInfo, cilBadge, cilBan, cilNotes, cilSave } from '@coreui/icons';
import {
cilSync,
cilNotes,
cilArrowCircleTop,
cilCheckCircle,
cilWifiSignal2,
cilTerminal,
cilTrash,
} from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import styles from './index.module.scss';
import { cleanBytesString } from '../../utils/formatting';
import DeviceBadge from '../DeviceBadge';
import LoadingButton from '../LoadingButton';
const DeviceListTable = ({
currentPage,
devices,
devicesPerPage,
loading,
@@ -34,118 +44,37 @@ const DeviceListTable = ({
refreshDevice,
t,
toggleFirmwareModal,
toggleHistoryModal,
upgradeToLatest,
upgradeStatus,
meshIcon,
apIcon,
internetSwitch,
iotIcon,
deviceIcons,
connectRtty,
deleteDevice,
deleteStatus,
}) => {
const columns = [
{ key: 'deviceType', label: '', filter: false, sorter: false, _style: { width: '3%' } },
{ key: 'verifiedCertificate', label: t('common.certificate'), _style: { width: '1%' } },
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
{ key: 'UUID', label: t('common.config_id'), _style: { width: '6%' } },
{ key: 'firmware', label: t('firmware.revision'), filter: false, _style: { width: '28%' } },
{ key: 'firmware', label: t('firmware.revision') },
{ key: 'firmware_button', label: '', filter: false, _style: { width: '4%' } },
{ key: 'compatible', label: t('firmware.device_type'), filter: false, _style: { width: '6%' } },
{ key: 'txBytes', label: 'Tx', filter: false, _style: { width: '11%' } },
{ key: 'rxBytes', label: 'Rx', filter: false, _style: { width: '11%' } },
{ key: 'ipAddress', label: t('common.ip_address'), _style: { width: '8%' } },
{ key: 'wifi_analysis', label: t(''), _style: { width: '4%' } },
{ key: 'show_details', label: t(''), _style: { width: '4%' } },
{ key: 'refresh_device', label: t(''), _style: { width: '4%' } },
{ key: 'compatible', label: t('common.type'), filter: false, _style: { width: '6%' } },
{ key: 'txBytes', label: 'Tx', filter: false, _style: { width: '8%' } },
{ key: 'rxBytes', label: 'Rx', filter: false, _style: { width: '8%' } },
{ key: 'ipAddress', label: t('IP'), _style: { width: '8%' } },
{ key: 'actions', label: '', _style: { width: '1%' } },
];
const getDeviceIcon = (deviceType) => {
if (deviceType === 'AP_Default' || deviceType === 'AP') {
return <img src={apIcon} className={styles.icon} alt="AP" />;
}
if (deviceType === 'MESH') {
return <img src={meshIcon} className={styles.icon} alt="MESH" />;
}
if (deviceType === 'SWITCH') {
return <img src={internetSwitch} className={styles.icon} alt="SWITCH" />;
}
if (deviceType === 'IOT') {
return <img src={iotIcon} className={styles.icon} alt="SWITCH" />;
}
return null;
};
const getCertBadge = (cert) => {
if (cert === 'NO_CERTIFICATE') {
return (
<div className={styles.certificateWrapper}>
<CIcon className={styles.badge} name="cil-badge" content={cilBadge} size="2xl" alt="AP" />
<CIcon
className={styles.badCertificate}
name="cil-ban"
content={cilBan}
size="3xl"
alt="AP"
/>
</div>
);
}
let color = 'transparent';
switch (cert) {
case 'VALID_CERTIFICATE':
color = 'danger';
break;
case 'MISMATCH_SERIAL':
return (
<CBadge color={color} className={styles.mismatchBackground}>
<CIcon name="cil-badge" content={cilBadge} size="2xl" alt="AP" />
</CBadge>
);
case 'VERIFIED':
color = 'success';
break;
default:
return (
<div className={styles.certificateWrapper}>
<CIcon
className={styles.badge}
name="cil-badge"
content={cilBadge}
size="2xl"
alt="AP"
/>
<CIcon
className={styles.badCertificate}
name="cil-ban"
content={cilBan}
size="3xl"
alt="AP"
/>
</div>
);
}
return (
<CBadge color={color}>
<CIcon name="cil-badge" content={cilBadge} size="2xl" alt="AP" />
</CBadge>
);
};
const getStatusBadge = (status) => {
if (status) {
return 'success';
}
return 'danger';
};
const getFirmwareButton = (latest, device) => {
let text = t('firmware.unknown_firmware_status');
let upgradeText = t('firmware.upgrade_to_latest');
let icon = <CIcon size="lg" name="cil-arrow-circle-top" content={cilArrowCircleTop} />;
let color = 'secondary';
if (latest !== undefined) {
text = t('firmware.newer_firmware_available');
color = 'warning';
if (latest) {
icon = <CIcon size="lg" name="cil-check-circle" content={cilCheckCircle} />;
text = t('firmware.latest_version_installed');
upgradeText = t('firmware.reinstall_latest');
color = 'success';
@@ -154,14 +83,14 @@ const DeviceListTable = ({
return (
<CDropdown>
<CDropdownToggle caret={false} color={color}>
<CIcon size="sm" content={cilSave} />
{icon}
</CDropdownToggle>
<CDropdownMenu style={{ width: '250px' }} className="mt-2 mb-2 mx-5" placement="bottom">
<CRow className="pl-3">
<CRow color="secondary" className="pl-3">
<CCol>{text}</CCol>
</CRow>
<CDropdownDivider />
<CRow className="pl-3 mt-1">
<CRow className="pl-3 mt-3">
<CCol>
<LoadingButton
label={upgradeText}
@@ -185,11 +114,55 @@ const DeviceListTable = ({
</CButton>
</CCol>
</CRow>
<CRow className="pl-3 mt-3">
<CCol>
<CButton
color="primary"
onClick={() => {
toggleHistoryModal(device);
}}
>
{t('firmware.history_title')}
</CButton>
</CCol>
</CRow>
</CDropdownMenu>
</CDropdown>
);
};
const deleteButton = (serialNumber) => (
<CPopover content={t('common.delete_device')}>
<CDropdown>
<CDropdownToggle className="btn-outline-primary btn-sm btn-square" caret={false}>
<CIcon name="cil-trash" content={cilTrash} size="sm" />
</CDropdownToggle>
<CDropdownMenu
style={{ width: '250px' }}
className="mt-2 mb-2 mx-5"
placement="bottom-start"
>
<CRow className="pl-3">
<CCol>{t('common.device_delete', { serialNumber })}</CCol>
</CRow>
<CDropdownDivider />
<CDropdownItem>
<LoadingButton
data-toggle="dropdown"
color="danger"
label={t('common.confirm')}
isLoadingLabel={t('user.deleting')}
isLoading={deleteStatus.loading}
action={() => deleteDevice(serialNumber)}
block
disabled={deleteStatus.loading}
/>
</CDropdownItem>
</CDropdownMenu>
</CDropdown>
</CPopover>
);
return (
<>
<CCard>
@@ -219,8 +192,13 @@ const DeviceListTable = ({
border
loading={loading}
scopedSlots={{
deviceType: (item) => (
<td className="pt-3 text-center">
<DeviceBadge t={t} device={item} deviceIcons={deviceIcons} />
</td>
),
serialNumber: (item) => (
<td className="text-center">
<td className="text-center align-middle">
<CLink
className="c-subheader-nav-link"
aria-current="page"
@@ -230,115 +208,99 @@ const DeviceListTable = ({
</CLink>
</td>
),
deviceType: (item) => (
<td className="pt-3 text-center">
<CPopover
content={item.connected ? t('common.connected') : t('common.not_connected')}
placement="top"
>
<CBadge size="sm" color={getStatusBadge(item.connected)}>
{getDeviceIcon(item.deviceType) ?? item.deviceType}
</CBadge>
</CPopover>
</td>
),
verifiedCertificate: (item) => (
<td className="text-center">
<CPopover
content={item.verifiedCertificate ?? t('common.unknown')}
placement="top"
>
{getCertBadge(item.verifiedCertificate)}
</CPopover>
</td>
),
firmware: (item) => (
<td>
<td className="align-middle">
<CPopover
content={item.firmware ? item.firmware : t('common.na')}
placement="top"
>
<p style={{ width: 'calc(20vw)' }} className="text-truncate">
<div style={{ width: 'calc(22vw)' }} className="text-truncate align-middle">
{item.firmware}
</p>
</div>
</CPopover>
</td>
),
firmware_button: (item) => (
<td className="text-center">
<td className="text-center align-middle">
{item.firmwareInfo
? getFirmwareButton(item.firmwareInfo.latest, item)
: getFirmwareButton(undefined, item)}
</td>
),
compatible: (item) => (
<td>
<td className="align-middle">
<CPopover
content={item.compatible ? item.compatible : t('common.na')}
placement="top"
>
<p style={{ width: 'calc(6vw)' }} className="text-truncate">
<div style={{ width: 'calc(8vw)' }} className="text-truncate align-middle">
{item.compatible}
</p>
</div>
</CPopover>
</td>
),
txBytes: (item) => <td>{cleanBytesString(item.txBytes)}</td>,
rxBytes: (item) => <td>{cleanBytesString(item.rxBytes)}</td>,
txBytes: (item) => <td className="align-middle">{cleanBytesString(item.txBytes)}</td>,
rxBytes: (item) => <td className="align-middle">{cleanBytesString(item.rxBytes)}</td>,
ipAddress: (item) => (
<td>
<td className="align-middle">
<CPopover
content={item.ipAddress ? item.ipAddress : t('common.na')}
placement="top"
>
<p style={{ width: 'calc(8vw)' }} className="text-truncate">
<div style={{ width: 'calc(7vw)' }} className="text-truncate align-middle">
{item.ipAddress}
</p>
</div>
</CPopover>
</td>
),
wifi_analysis: (item) => (
<td className="text-center">
<CPopover content={t('configuration.details')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
<CButton color="primary" variant="outline" shape="square" size="sm">
<CIcon name="cil-info" content={cilInfo} size="sm" />
actions: (item) => (
<td className="text-center align-middle">
<CButtonGroup role="group">
<CPopover content={t('wifi_analysis.title')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}/wifianalysis`}
>
<CButton color="primary" variant="outline" shape="square" size="sm">
<CIcon name="cil-wifi-signal-2" content={cilWifiSignal2} size="sm" />
</CButton>
</CLink>
</CPopover>
<CPopover content={t('actions.connect')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
onClick={() => connectRtty(item.serialNumber)}
>
<CIcon name="cil-terminal" content={cilTerminal} size="sm" />
</CButton>
</CLink>
</CPopover>
</td>
),
show_details: (item) => (
<td className="text-center">
<CPopover content={t('wifi_analysis.title')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}/wifianalysis`}
>
<CButton color="primary" variant="outline" shape="square" size="sm">
<CIcon name="cil-notes" content={cilNotes} size="sm" />
</CPopover>
{deleteButton(item.serialNumber)}
<CPopover content={t('configuration.details')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
<CButton color="primary" variant="outline" shape="square" size="sm">
<CIcon name="cil-notes" content={cilNotes} size="sm" />
</CButton>
</CLink>
</CPopover>
<CPopover content={t('common.refresh_device')}>
<CButton
onClick={() => refreshDevice(item.serialNumber)}
color="primary"
variant="outline"
size="sm"
>
<CIcon name="cil-sync" content={cilSync} size="sm" />
</CButton>
</CLink>
</CPopover>
</td>
),
refresh_device: (item) => (
<td className="text-center">
<CPopover content={t('common.refresh_device')}>
<CButton
onClick={() => refreshDevice(item.serialNumber)}
color="primary"
variant="outline"
size="sm"
>
<CIcon name="cil-sync" content={cilSync} size="sm" />
</CButton>
</CPopover>
</CPopover>
</CButtonGroup>
</td>
),
}}
@@ -348,6 +310,7 @@ const DeviceListTable = ({
nextLabel="Next →"
pageCount={pageCount}
onPageChange={updatePage}
forcePage={Number(currentPage)}
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
@@ -366,6 +329,7 @@ const DeviceListTable = ({
};
DeviceListTable.propTypes = {
currentPage: PropTypes.string,
devices: PropTypes.instanceOf(Array).isRequired,
updateDevicesPerPage: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
@@ -375,12 +339,17 @@ DeviceListTable.propTypes = {
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
toggleFirmwareModal: PropTypes.func.isRequired,
toggleHistoryModal: PropTypes.func.isRequired,
upgradeToLatest: PropTypes.func.isRequired,
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
meshIcon: PropTypes.string.isRequired,
apIcon: PropTypes.string.isRequired,
internetSwitch: PropTypes.string.isRequired,
iotIcon: PropTypes.string.isRequired,
deviceIcons: PropTypes.instanceOf(Object).isRequired,
connectRtty: PropTypes.func.isRequired,
deleteDevice: PropTypes.func.isRequired,
deleteStatus: PropTypes.instanceOf(Object).isRequired,
};
DeviceListTable.defaultProps = {
currentPage: '0',
};
export default React.memo(DeviceListTable);

View File

@@ -0,0 +1,228 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CCard, CCardBody, CCardHeader, CCol, CDataTable, CRow, CWidgetIcon } from '@coreui/react';
import { CChartBar, CChartPie } from '@coreui/react-chartjs';
import { cilClock, cilHappy, cilMeh, cilFrown } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { prettyDate } from '../../utils/formatting';
const getLatestColor = (percent = 0) => {
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
if (numberPercent >= 90) return 'success';
if (numberPercent > 60) return 'warning';
return 'danger';
};
const getLatestIcon = (percent = 0) => {
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
if (numberPercent >= 90) return <CIcon width={36} name="cil-happy" content={cilHappy} />;
if (numberPercent > 60) return <CIcon width={36} name="cil-meh" content={cilMeh} />;
return <CIcon width={36} name="cil-frown" content={cilFrown} />;
};
const FirmwareDashboard = ({ t, data }) => {
const columns = [
{ key: 'endpoint', label: t('common.endpoint'), filter: false, sorter: false },
{ key: 'devices', label: t('common.devices') },
{ key: 'percent', label: '' },
];
return (
<div>
<CRow>
<CCol>
<CWidgetIcon
text={t('common.last_dashboard_refresh')}
header={<h2>{data.snapshot ? prettyDate(data.snapshot) : ''}</h2>}
color="info"
iconPadding={false}
>
<CIcon width={36} name="cil-clock" content={cilClock} />
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={t('common.up_to_date')}
header={<h2>{data.latestSoftwareRate}</h2>}
color={getLatestColor(data.latestSoftwareRate)}
iconPadding={false}
>
{getLatestIcon(data.latestSoftwareRate)}
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={t('common.devices')}
header={<h2>{data.numberOfDevices}</h2>}
color="primary"
iconPadding={false}
>
<CIcon width={36} name="cil-router" />
</CWidgetIcon>
</CCol>
</CRow>
<CRow>
<CCol>
<CCard>
<CCardHeader>{t('common.firmware_installed')}</CCardHeader>
<CCardBody>
<CChartPie
datasets={data.firmwareDistribution.datasets}
labels={data.firmwareDistribution.labels}
options={{
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader>{t('common.devices_using_latest')}</CCardHeader>
<CCardBody>
<CChartBar
datasets={data.latest.datasets}
labels={data.latest.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader>Unknown Firmware</CCardHeader>
<CCardBody>
<CChartBar
datasets={data.unknownFirmwares.datasets}
labels={data.unknownFirmwares.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
</CRow>
<CRow>
<CCol>
<CCard>
<CCardHeader>{t('common.device_status')}</CCardHeader>
<CCardBody>
<CChartPie
datasets={data.status.datasets}
labels={data.status.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) => `${ds.datasets[0].data[item.index]}%`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader>{t('firmware.device_types')}</CCardHeader>
<CCardBody>
<CChartPie
datasets={data.deviceType.datasets}
labels={data.deviceType.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader>OUIs</CCardHeader>
<CCardBody>
<CChartBar
datasets={data.ouis.datasets}
labels={data.ouis.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
</CRow>
<CRow>
<CCol>
<CCard>
<CCardHeader>{t('common.endpoints')}</CCardHeader>
<CCardBody>
<CDataTable items={data.endpoints ?? []} fields={columns} hover border />
</CCardBody>
</CCard>
</CCol>
<CCol />
<CCol />
</CRow>
</div>
);
};
FirmwareDashboard.propTypes = {
t: PropTypes.func.isRequired,
data: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(FirmwareDashboard);

View File

@@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CDataTable } from '@coreui/react';
import { prettyDate } from '../../utils/formatting';
const FirmwareHistoryModal = ({ t, loading, data }) => {
const columns = [
{ key: 'date', label: '#', _style: { width: '20%' } },
{ key: 'fromRelease', label: t('firmware.from_release'), sorter: false },
{ key: 'toRelease', label: t('firmware.to_release'), sorter: false },
];
return (
<CDataTable
addTableClasses="ignore-overflow"
fields={columns}
items={data}
hover
border
loading={loading}
sorter
sorterValue={{ column: 'radio', asc: true }}
scopedSlots={{
date: (item) => <td>{prettyDate(item.upgraded)}</td>,
}}
/>
);
};
FirmwareHistoryModal.propTypes = {
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.instanceOf(Array).isRequired,
};
export default React.memo(FirmwareHistoryModal);

View File

@@ -4,7 +4,15 @@ import { CDataTable, CRow, CCol, CLabel, CInput } from '@coreui/react';
import { prettyDate } from '../../utils/formatting';
import LoadingButton from '../LoadingButton';
const NotesTable = ({ t, notes, addNote, loading, size, extraFunctionParameter }) => {
const NotesTable = ({
t,
notes,
addNote,
loading,
size,
extraFunctionParameter,
descriptionColumn,
}) => {
const [currentNote, setCurrentNote] = useState('');
const columns = [
@@ -21,6 +29,57 @@ const NotesTable = ({ t, notes, addNote, loading, size, extraFunctionParameter }
setCurrentNote('');
}, [notes]);
if (!descriptionColumn) {
return (
<div>
<CRow>
<CCol>
<CInput
id="notes-input"
name="text-input"
value={currentNote}
onChange={(e) => setCurrentNote(e.target.value)}
/>
</CCol>
<CCol sm={size === 'm' ? '3' : '2'}>
<LoadingButton
label={t('common.add')}
isLoadingLabel={t('common.adding_ellipsis')}
isLoading={loading}
action={saveNote}
disabled={loading || currentNote === ''}
/>
</CCol>
</CRow>
<CRow className="pt-3">
<CCol>
<div className="overflow-auto" style={{ height: '200px' }}>
<CDataTable
striped
responsive
border
loading={loading}
fields={columns}
items={notes || []}
noItemsView={{ noItems: t('common.no_items') }}
sorterValue={{ column: 'created', desc: 'true' }}
scopedSlots={{
created: (item) => (
<td>
{item.created && item.created !== 0
? prettyDate(item.created)
: t('common.na')}
</td>
),
}}
/>
</div>
</CCol>
</CRow>
</div>
);
}
return (
<div>
<CRow>
@@ -80,11 +139,13 @@ NotesTable.propTypes = {
loading: PropTypes.bool.isRequired,
size: PropTypes.string,
extraFunctionParameter: PropTypes.string,
descriptionColumn: PropTypes.bool,
};
NotesTable.defaultProps = {
size: 'm',
extraFunctionParameter: '',
descriptionColumn: true,
};
export default NotesTable;

View File

@@ -4,7 +4,7 @@ import { CDataTable } from '@coreui/react';
const RadioAnalysisTable = ({ data, loading }) => {
const columns = [
{ key: 'radio', label: 'R', _style: { width: '5%' } },
{ key: 'radio', label: '#', _style: { width: '5%' } },
{ key: 'channel', label: 'Ch', _style: { width: '5%' } },
{ key: 'channelWidth', label: 'C Width', _style: { width: '7%' }, sorter: false },
{ key: 'noise', label: 'Noise', _style: { width: '4%' }, sorter: false },

View File

@@ -4,18 +4,18 @@ import { CButton, CDataTable, CPopover } from '@coreui/react';
const WifiAnalysisTable = ({ t, data, loading }) => {
const columns = [
{ key: 'radio', label: 'R', _style: { width: '5%' } },
{ key: 'radio', label: '#', _style: { width: '5%' } },
{ key: 'bssid', label: 'BSSID', _style: { width: '14%' } },
{ key: 'mode', label: t('wifi_analysis.mode'), _style: { width: '9%' }, sorter: false },
{ key: 'ssid', label: 'SSID', _style: { width: '17%' } },
{ key: 'rssi', label: 'RSSI', _style: { width: '5%' }, sorter: false },
{ key: 'rxRate', label: 'Rx Rate', _style: { width: '7%' }, sorter: false },
{ key: 'rxBytes', label: 'Rx Bytes', _style: { width: '7%' }, sorter: false },
{ key: 'rxBytes', label: 'Rx', _style: { width: '7%' }, sorter: false },
{ key: 'rxMcs', label: 'Rx MCS', _style: { width: '6%' }, sorter: false },
{ key: 'rxNss', label: 'Rx NSS', _style: { width: '6%' }, sorter: false },
{ key: 'txRate', label: 'Tx Rate', _style: { width: '7%' }, sorter: false },
{ key: 'txBytes', label: 'Tx Bytes', _style: { width: '7%' }, sorter: false },
{ key: 'ips', label: 'Ip Addr.', _style: { width: '6%' }, sorter: false },
{ key: 'txBytes', label: 'Tx', _style: { width: '7%' }, sorter: false },
{ key: 'ips', label: 'IP', _style: { width: '6%' }, sorter: false },
];
const centerIfEmpty = (value) => (

View File

@@ -16,14 +16,17 @@ export { default as EditMyProfile } from './components/EditMyProfile';
export { default as Avatar } from './components/Avatar';
export { default as WifiAnalysisTable } from './components/WifiAnalysisTable';
export { default as RadioAnalysisTable } from './components/RadioAnalysisTable';
export { default as FirmwareHistoryTable } from './components/FirmwareHistoryTable';
export { default as ApiStatusCard } from './components/ApiStatusCard';
export { default as FirmwareList } from './components/FirmwareList';
export { default as DeviceFirmwareModal } from './components/DeviceFirmwareModal';
export { default as DeviceListTable } from './components/DeviceListTable';
export { default as NotesTable } from './components/NotesTable';
// Pages
export { default as LoginPage } from './components/LoginPage';
export { default as DeviceDashboard } from './components/DeviceDashboard';
export { default as FirmwareDashboard } from './components/FirmwareDashboard';
// Hooks
export { default as useFormFields } from './hooks/useFormFields';