2.5.27: standardized command history, health and logs, wifi analysis table fixes

This commit is contained in:
Charles
2022-01-17 19:16:36 +01:00
parent 40ed1dd612
commit 09a10d7838
6 changed files with 429 additions and 341 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.5.26",
"version": "2.5.27",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.5.26",
"version": "2.5.27",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.5.26",
"version": "2.5.27",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",

View File

@@ -2,9 +2,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
CWidgetDropdown,
CRow,
CCol,
CCardHeader,
CCardBody,
CButton,
CDataTable,
CCard,
@@ -262,155 +261,162 @@ const DeviceCommands = () => {
return (
<div>
<CWidgetDropdown
className="m-0"
inverse="true"
color="gradient-primary"
header={t('commands.title')}
footerSlot={
<div className="pb-1 px-3">
<CRow className="mb-2">
<CCol>
From:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</CCol>
<CCol>
To:
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</CCol>
</CRow>
<CCard>
<div className="overflow-auto" style={{ height: '200px' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
loading={loading}
items={commands ?? []}
fields={columns}
className="text-white"
sorterValue={{ column: 'created', desc: 'true' }}
scopedSlots={{
command: (item) => <td className="align-middle">{item.command}</td>,
completed: (item) => (
<td className="align-middle">
{item.completed && item.completed !== 0 ? (
<FormattedDate date={item.completed} />
) : (
'Pending'
)}
</td>
),
executed: (item) => (
<td className="align-middle">
{item.executed && item.executed !== 0 ? (
<FormattedDate date={item.executed} />
) : (
'Pending'
)}
</td>
),
submitted: (item) => (
<td className="align-middle">
{item.submitted && item.submitted !== '' ? (
<FormattedDate date={item.submitted} />
) : (
'Pending'
)}
</td>
),
errorCode: (item) => <td className="align-middle">{item.errorCode}</td>,
show_buttons: (item, index) => (
<td className="align-middle">
<CButtonToolbar
role="group"
className="justify-content-flex-end"
style={{ width: '160px' }}
>
<CPopover
content={
item.command === 'trace' ? t('common.download') : t('common.result')
}
>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleDetails(item);
}}
>
{item.command === 'trace' ? (
<CIcon name="cil-cloud-download" content={cilCloudDownload} />
) : (
<CIcon name="cil-calendar-check" content={cilCalendarCheck} />
)}
</CButton>
</CPopover>
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleResponse(item);
}}
>
<CIcon name="cilList" />
</CButton>
</CPopover>
<CPopover content={t('common.delete')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleConfirmModal(item.UUID, index);
}}
>
<CIcon name="cilTrash" />
</CButton>
</CPopover>
</CButtonToolbar>
</td>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreCommands}
variant="outline"
/>
</div>
)}
</div>
</CCard>
<CCard className="m-0">
<CCardHeader className="dark-header">
<div className="d-flex flex-row-reverse align-items-center">
<div className="pl-2">
<CPopover content={t('common.refresh')}>
<CButton
size="sm"
color="info"
onClick={getCommands}
disabled={startError || endError}
>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
</div>
}
>
<div className="text-right float-right">
<CButton onClick={refreshCommands} size="sm">
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
</CButton>
</div>
</CWidgetDropdown>
</CCardHeader>
<CCardBody className="p-1">
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
loading={loading}
items={commands ?? []}
fields={columns}
className="text-white"
sorterValue={{ column: 'created', desc: 'true' }}
scopedSlots={{
command: (item) => <td className="align-middle">{item.command}</td>,
completed: (item) => (
<td className="align-middle">
{item.completed && item.completed !== 0 ? (
<FormattedDate date={item.completed} />
) : (
'Pending'
)}
</td>
),
executed: (item) => (
<td className="align-middle">
{item.executed && item.executed !== 0 ? (
<FormattedDate date={item.executed} />
) : (
'Pending'
)}
</td>
),
submitted: (item) => (
<td className="align-middle">
{item.submitted && item.submitted !== '' ? (
<FormattedDate date={item.submitted} />
) : (
'Pending'
)}
</td>
),
errorCode: (item) => <td className="align-middle">{item.errorCode}</td>,
show_buttons: (item, index) => (
<td className="align-middle">
<CButtonToolbar
role="group"
className="justify-content-flex-end"
style={{ width: '160px' }}
>
<CPopover
content={
item.command === 'trace' ? t('common.download') : t('common.result')
}
>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleDetails(item);
}}
>
{item.command === 'trace' ? (
<CIcon name="cil-cloud-download" content={cilCloudDownload} />
) : (
<CIcon name="cil-calendar-check" content={cilCalendarCheck} />
)}
</CButton>
</CPopover>
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleResponse(item);
}}
>
<CIcon name="cilList" />
</CButton>
</CPopover>
<CPopover content={t('common.delete')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleConfirmModal(item.UUID, index);
}}
>
<CIcon name="cilTrash" />
</CButton>
</CPopover>
</CButtonToolbar>
</td>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreCommands}
variant="outline"
/>
</div>
)}
</div>
</CCardBody>
</CCard>
<WifiScanResultModalWidget
show={showScanModal}
toggle={toggleScanModal}

View File

@@ -1,14 +1,14 @@
/* eslint-disable-rule prefer-destructuring */
import React, { useState, useEffect } from 'react';
import {
CWidgetDropdown,
CCardBody,
CButton,
CDataTable,
CCard,
CRow,
CCol,
CProgress,
CCardHeader,
CPopover,
CCard,
CFormText,
CBadge,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSync, cilTrash } from '@coreui/icons';
@@ -27,7 +27,9 @@ const DeviceHealth = () => {
const [loading, setLoading] = useState(false);
const [healthChecks, setHealthChecks] = useState([]);
const [start, setStart] = useState('');
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState('');
const [endError, setEndError] = useState(false);
const [logLimit, setLogLimit] = useState(25);
const [loadingMore, setLoadingMore] = useState(false);
const [showLoadingMore, setShowLoadingMore] = useState(true);
@@ -40,13 +42,26 @@ const DeviceHealth = () => {
};
const modifyStart = (value) => {
setStart(value);
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
setEnd(value);
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
} catch (e) {
setEnd('');
setEndError(true);
}
};
const showMoreLogs = () => {
setLogLimit(logLimit + 50);
};
@@ -128,14 +143,14 @@ const DeviceHealth = () => {
const tempSanityLevel = sortedHealthchecks[healthChecks.length - 1].sanity;
setSanityLevel(tempSanityLevel);
if (tempSanityLevel === 100) {
setBarColor('gradient-success');
setBarColor('success');
} else if (tempSanityLevel >= 90) {
setBarColor('gradient-warning');
setBarColor('warning');
} else {
setBarColor('gradient-danger');
setBarColor('danger');
}
} else {
setBarColor('gradient-dark');
setBarColor('dark');
}
}, [healthChecks]);
@@ -156,88 +171,104 @@ const DeviceHealth = () => {
}, []);
return (
<CWidgetDropdown
className="m-0"
header={t('health.title')}
text={sanityLevel ? `${sanityLevel}%` : t('common.unknown')}
value={sanityLevel ?? 100}
color={barColor}
inverse="true"
footerSlot={
<div className="pb-1 px-3">
<CProgress className="mb-3" color="white" value={sanityLevel ?? 0} />
<CRow className="mb-3">
<CCol>
{t('common.from')}
:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
</CCol>
<CCol>
{t('common.to')}
:
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard className="p-0">
<div className="overflow-auto" style={{ height: '200px' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
items={healthChecks ?? []}
fields={columns}
className="text-white"
loading={loading}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
UUID: (item) => <td className="align-middle">{item.UUID}</td>,
recorded: (item) => (
<td className="align-middle">
<FormattedDate date={item.recorded} />
</td>
),
sanity: (item) => <td className="align-middle">{`${item.sanity}%`}</td>,
checkDetails: (item) => (
<td>
<pre className="my-0">{JSON.stringify(item.values)}</pre>
</td>
),
}}
<CCard className="m-0">
<CCardHeader className="dark-header">
<div className="float-left align-middle pt-1">
<h4>
<CBadge color={barColor} className="my-0">
{sanityLevel ? `${sanityLevel}%` : `${t('common.unknown')} Sanity Level`}
</CBadge>
</h4>
</div>
<div className="d-flex flex-row-reverse align-items-center">
<div className="pl-2">
<CPopover content={t('common.refresh')}>
<CButton
size="sm"
color="info"
onClick={getDeviceHealth}
disabled={startError || endError}
>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
<div className="px-2">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm" color="danger">
<CIcon name="cil-trash" content={cilTrash} />
</CButton>
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
items={healthChecks ?? []}
fields={columns}
className="text-white"
loading={loading}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
UUID: (item) => <td className="align-middle">{item.UUID}</td>,
recorded: (item) => (
<td className="align-middle">
<FormattedDate date={item.recorded} />
</td>
),
sanity: (item) => <td className="align-middle">{`${item.sanity}%`}</td>,
checkDetails: (item) => (
<td>
<pre className="my-0">{JSON.stringify(item.values)}</pre>
</td>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</div>
</CCard>
)}
<DeleteLogModal
serialNumber={deviceSerialNumber}
object="healthchecks"
object="logs"
show={showDeleteModal}
toggle={toggleDeleteModal}
/>
</div>
}
>
<div className="text-right float-right">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm">
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
</CButton>
</CPopover>
<CPopover content={t('common.refresh')}>
<CButton onClick={getDeviceHealth} size="sm">
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
</CButton>
</CPopover>
</div>
</CWidgetDropdown>
</CCardBody>
</CCard>
);
};

View File

@@ -1,15 +1,14 @@
/* eslint-disable-rule prefer-destructuring */
import React, { useState, useEffect } from 'react';
import {
CWidgetDropdown,
CRow,
CCol,
CCardHeader,
CCardBody,
CCollapse,
CButton,
CDataTable,
CCard,
CCardBody,
CPopover,
CFormText,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSync, cilTrash } from '@coreui/icons';
@@ -29,7 +28,9 @@ const DeviceLogs = () => {
const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState([]);
const [start, setStart] = useState('');
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState('');
const [endError, setEndError] = useState(false);
const [logLimit, setLogLimit] = useState(25);
const [loadingMore, setLoadingMore] = useState(false);
const [showLoadingMore, setShowLoadingMore] = useState(true);
@@ -40,11 +41,25 @@ const DeviceLogs = () => {
};
const modifyStart = (value) => {
setStart(value);
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
setEnd(value);
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
} catch (e) {
setEnd('');
setEndError(true);
}
};
const showMoreLogs = () => {
@@ -167,96 +182,105 @@ const DeviceLogs = () => {
return (
<div>
<CWidgetDropdown
className="m-0"
inverse="true"
color="gradient-info"
header={t('device_logs.title')}
footerSlot={
<div className="pb-1 px-3">
<CRow className="mb-3">
<CCol>
{t('common.from')}
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
</CCol>
<CCol>
{t('common.to')}
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard>
<div className="overflow-auto" style={{ height: '250px' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
items={logs ?? []}
fields={columns}
loading={loading}
className="text-white"
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
recorded: (item) => (
<td className="align-middle">
<FormattedDate date={item.recorded} />
</td>
),
UUID: (item) => <td className="align-middle">{item.UUID}</td>,
severity: (item) => <td className="align-middle">{item.severity}</td>,
log: (item) => <td className="align-middle">{item.log}</td>,
show_details: (item, index) => (
<td className="align-middle">
<CButton
color="primary"
variant={details.includes(index) ? '' : 'outline'}
shape="square"
size="sm"
onClick={() => {
toggleDetails(index);
}}
>
<CIcon name="cilList" />
</CButton>
</td>
),
details: (item, index) => (
<CCollapse show={details.includes(index)}>
<CCardBody>
<h5>{t('common.details')}</h5>
<div>{getDetails(index, item)}</div>
</CCardBody>
</CCollapse>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</div>
</CCard>
<CCard className="m-0">
<CCardHeader className="dark-header">
<div className="d-flex flex-row-reverse align-items-center">
<div className="pl-2">
<CPopover content={t('common.refresh')}>
<CButton size="sm" color="info" onClick={getLogs} disabled={startError || endError}>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
<div className="px-2">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm" color="danger">
<CIcon name="cil-trash" content={cilTrash} />
</CButton>
</CPopover>
</div>
</div>
}
>
<div className="text-right float-right">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm">
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
</CButton>
</CPopover>
<CPopover content={t('common.refresh')}>
<CButton onClick={getLogs} size="sm">
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
</CButton>
</CPopover>
</div>
</CWidgetDropdown>
</CCardHeader>
<CCardBody className="p-1">
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
items={logs ?? []}
fields={columns}
loading={loading}
className="text-white"
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
recorded: (item) => (
<td className="align-middle">
<FormattedDate date={item.recorded} />
</td>
),
UUID: (item) => <td className="align-middle">{item.UUID}</td>,
severity: (item) => <td className="align-middle">{item.severity}</td>,
log: (item) => <td className="align-middle">{item.log}</td>,
show_details: (item, index) => (
<td className="align-middle">
<CButton
color="primary"
variant={details.includes(index) ? '' : 'outline'}
shape="square"
size="sm"
onClick={() => {
toggleDetails(index);
}}
>
<CIcon name="cilList" />
</CButton>
</td>
),
details: (item, index) => (
<CCollapse show={details.includes(index)}>
<CCardBody>
<h5>{t('common.details')}</h5>
<div>{getDetails(index, item)}</div>
</CCardBody>
</CCollapse>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</div>
</CCardBody>
</CCard>
<DeleteLogModal
serialNumber={deviceSerialNumber}
object="logs"

View File

@@ -34,26 +34,39 @@ const WifiAnalysisTable = ({ t, data, loading }) => {
{ key: 'rxNss', label: 'Rx NSS', _style: { width: '6%' }, sorter: false },
{ key: 'txRate', label: 'Tx Rate', _style: { width: '7%' }, sorter: false },
{ key: 'txBytes', label: 'Tx', _style: { width: '7%' }, sorter: false },
{ key: 'ips', label: 'IP', _style: { width: '6%' }, sorter: false },
{ key: 'ips', label: 'IP', _style: { width: '1%' }, sorter: false },
];
const centerIfEmpty = (value) => (
<td className={!value || value === '' || value === '-' ? 'text-center' : ''}>{value}</td>
<td
className={
!value || value === '' || value === '-'
? 'text-center align-middle'
: 'text-right align-middle'
}
>
{value}
</td>
);
const displayIp = (ssid, v4, v6) => {
const count = v4.length + v6.length;
return (
<td className="ignore-overflow text-center">
<td className="ignore-overflow text-center align-middle">
{count > 0 ? (
<CPopover content="View">
<CButton color="primary" size="sm" onClick={() => toggle(ssid, v4, v6)}>
<CButton
color="primary"
size="sm"
onClick={() => toggle(ssid, v4, v6)}
className="py-1"
>
{count}
</CButton>
</CPopover>
) : (
<p>{count}</p>
count
)}
</td>
);
@@ -71,9 +84,23 @@ const WifiAnalysisTable = ({ t, data, loading }) => {
sorter
sorterValue={{ column: 'radio', asc: true }}
scopedSlots={{
bssid: (item) => <td className="text-center text-monospace">{item.bssid}</td>,
radio: (item) => <td className="text-center">{item.radio.radio}</td>,
bssid: (item) => (
<td
className="text-center align-middle"
style={{ fontFamily: 'monospace', fontSize: '0.96rem' }}
>
{item.bssid}
</td>
),
radio: (item) => <td className="text-center align-middle">{item.radio.radio}</td>,
ssid: (item) => <td className="align-middle">{item.ssid}</td>,
mode: (item) => <td className="align-middle">{item.mode}</td>,
vendor: (item) => <td className="align-middle">{item.vendor}</td>,
rxMcs: (item) => centerIfEmpty(item.rxMcs),
rxRate: (item) => centerIfEmpty(item.rxRate),
rxBytes: (item) => centerIfEmpty(item.rxBytes),
txRate: (item) => centerIfEmpty(item.txRate),
txBytes: (item) => centerIfEmpty(item.txBytes),
rxNss: (item) => centerIfEmpty(item.rxNss),
rssi: (item) => centerIfEmpty(item.rssi),
ips: (item) => displayIp(item.ssid, item.ipV4, item.ipV6),