Version 0.8.29

This commit is contained in:
bourquecharles
2021-07-28 13:22:48 -04:00
parent 7374dec044
commit 37931823b1
10 changed files with 307 additions and 6 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-libs",
"version": "0.8.24",
"version": "0.8.29",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-libs",
"version": "0.8.24",
"version": "0.8.29",
"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.24",
"version": "0.8.29",
"main": "dist/index.js",
"source": "src/index.js",
"engines": {

View File

@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { CCard, CCardBody, CCardHeader, CCol, CCollapse, CInput, CRow } from '@coreui/react';
import { prettyDate, cleanBytesString } from '../../utils/formatting';
import NotesTable from '../NotesTable';
import LoadingButton from '../LoadingButton';
const FirmwareDetails = ({
t,
show,
item,
addNote,
addNoteLoading,
updateDescription,
updateDescriptionLoading,
}) => {
const [description, setDescription] = useState('');
const saveDescription = () => {
updateDescription(description, item.id);
};
return (
<CCollapse show={show}>
<CCard className="mt-3 mx-3">
<CCardHeader>{t('firmware.details_title', { image: item.image })}</CCardHeader>
<CCardBody>
<CRow>
<CCol sm="1">Created</CCol>
<CCol sm="5">{prettyDate(item.created)}</CCol>
<CCol sm="1">Release</CCol>
<CCol sm="5">{item.release}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="1">Image</CCol>
<CCol sm="5">{item.image}</CCol>
<CCol sm="1">Image Date</CCol>
<CCol sm="5">{prettyDate(item.imageDate)}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="1">Revision</CCol>
<CCol sm="5">{item.revision}</CCol>
<CCol sm="1">Size</CCol>
<CCol sm="5">{cleanBytesString(item.size)}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="1">URI</CCol>
<CCol sm="5">{item.uri}</CCol>
<CCol sm="1">Owner</CCol>
<CCol sm="5">{item.owner === '' ? t('common.unknown') : item.owner}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="1" className="mt-2">
Description
</CCol>
<CCol sm="4">
<CInput
id="description"
defaultValue={item.description}
maxLength="50"
onChange={(e) => setDescription(e.target.value)}
/>
</CCol>
<CCol sm="1">
<LoadingButton
label={t('common.save')}
isLoadingLabel={t('common.saving')}
isLoading={updateDescriptionLoading}
action={saveDescription}
disabled={updateDescriptionLoading}
/>
</CCol>
<CCol>
<NotesTable
t={t}
notes={item.notes}
addNote={addNote}
loading={addNoteLoading}
extraFunctionParameter={item.id}
/>
</CCol>
</CRow>
</CCardBody>
</CCard>
</CCollapse>
);
};
FirmwareDetails.propTypes = {
t: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
item: PropTypes.instanceOf(Object).isRequired,
addNoteLoading: PropTypes.bool.isRequired,
addNote: PropTypes.func.isRequired,
updateDescription: PropTypes.func.isRequired,
updateDescriptionLoading: PropTypes.bool.isRequired,
};
export default React.memo(FirmwareDetails);

View File

@@ -0,0 +1,179 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import { v4 as createUuid } from 'uuid';
import {
CButton,
CCard,
CCardBody,
CCardHeader,
CCol,
CDataTable,
CRow,
CSelect,
} from '@coreui/react';
import { prettyDate, cleanBytesString } from '../../utils/formatting';
import FirmwareDetails from '../FirmwareDetails';
const FirmwareList = ({
t,
loading,
page,
pageCount,
setPage,
data,
firmwarePerPage,
setFirmwarePerPage,
selectedDeviceType,
deviceTypes,
setSelectedDeviceType,
addNote,
addNoteLoading,
updateDescription,
updateDescriptionLoading,
}) => {
const [detailsShown, setDetailsShown] = useState([]);
const fields = [
{ key: 'created', label: t('common.created'), _style: { width: '12%' } },
{ key: 'size', label: t('firmware.size'), _style: { width: '8%' } },
{ key: 'revision', label: t('firmware.revision'), _style: { width: '30%' } },
{ key: 'uri', label: 'URI' },
{ key: 'show_details', label: '', _style: { width: '5%' } },
];
const toggleDetails = (index) => {
const position = detailsShown.indexOf(index);
let newDetails = detailsShown.slice();
if (position !== -1) {
newDetails.splice(position, 1);
} else {
newDetails = [...newDetails, index];
}
setDetailsShown(newDetails);
};
const changePage = (newValue) => {
setDetailsShown([]);
setPage(newValue);
};
useEffect(() => {
setDetailsShown([]);
}, [selectedDeviceType]);
return (
<CCard>
<CCardHeader>
<CRow>
<CCol />
<CCol sm="1" className="pt-2 text-right">
{t('firmware.device_type')}
</CCol>
<CCol sm="2" className="text-right">
<div>
<CSelect
custom
value={selectedDeviceType}
onChange={(e) => setSelectedDeviceType(e.target.value)}
disabled={loading}
>
{deviceTypes.map((deviceType) => (
<option key={createUuid()} value={deviceType}>
{deviceType}
</option>
))}
</CSelect>
</div>
</CCol>
<CCol sm="1">
<div className="text-right">
<CSelect
custom
defaultValue={firmwarePerPage}
onChange={(e) => setFirmwarePerPage(e.target.value)}
disabled={loading}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</CSelect>
</div>
</CCol>
</CRow>
</CCardHeader>
<CCardBody>
<CDataTable
items={data}
fields={fields}
loading={loading}
hover
border
scopedSlots={{
created: (item) => <td>{prettyDate(item.created)}</td>,
size: (item) => <td>{cleanBytesString(item.size)}</td>,
show_details: (item, index) => (
<td className="text-center">
<CButton
color="primary"
variant={detailsShown.includes(index) ? '' : 'outline'}
onClick={() => toggleDetails(index)}
>
{detailsShown.includes(index) ? t('common.hide') : t('common.details')}
</CButton>
</td>
),
details: (item, index) => (
<FirmwareDetails
t={t}
show={detailsShown.includes(index)}
item={item}
addNote={addNote}
addNoteLoading={addNoteLoading}
updateDescription={updateDescription}
updateDescriptionLoading={updateDescriptionLoading}
/>
),
}}
/>
<ReactPaginate
previousLabel="← Previous"
nextLabel="Next →"
pageCount={pageCount}
onPageChange={changePage}
forcePage={page.selected}
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
nextClassName="page-item"
nextLinkClassName="page-link"
activeClassName="active"
/>
</CCardBody>
</CCard>
);
};
FirmwareList.propTypes = {
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
pageCount: PropTypes.number.isRequired,
page: PropTypes.instanceOf(Object).isRequired,
setPage: PropTypes.func.isRequired,
data: PropTypes.instanceOf(Array).isRequired,
firmwarePerPage: PropTypes.string.isRequired,
setFirmwarePerPage: PropTypes.func.isRequired,
selectedDeviceType: PropTypes.string.isRequired,
deviceTypes: PropTypes.instanceOf(Array).isRequired,
setSelectedDeviceType: PropTypes.func.isRequired,
addNote: PropTypes.func.isRequired,
addNoteLoading: PropTypes.bool.isRequired,
updateDescription: PropTypes.func.isRequired,
updateDescriptionLoading: PropTypes.bool.isRequired,
};
export default React.memo(FirmwareList);

View File

@@ -4,7 +4,7 @@ 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 }) => {
const NotesTable = ({ t, notes, addNote, loading, size, extraFunctionParameter }) => {
const [currentNote, setCurrentNote] = useState('');
const columns = [
@@ -14,7 +14,7 @@ const NotesTable = ({ t, notes, addNote, loading, size }) => {
];
const saveNote = () => {
addNote(currentNote);
addNote(currentNote, extraFunctionParameter);
};
useEffect(() => {
@@ -79,10 +79,12 @@ NotesTable.propTypes = {
addNote: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
size: PropTypes.string,
extraFunctionParameter: PropTypes.string,
};
NotesTable.defaultProps = {
size: 'm',
extraFunctionParameter: '',
};
export default NotesTable;

View File

@@ -16,6 +16,7 @@ import { cilBan, cilCheckCircle, cilPencil, cilSync, cilTrash } from '@coreui/ic
import CIcon from '@coreui/icons-react';
import { capitalizeFirstLetter, prettyDate } from '../../utils/formatting';
import DeleteModal from '../DeleteModal';
import Avatar from '../Avatar';
const UserListTable = ({
t,
@@ -49,6 +50,7 @@ const UserListTable = ({
};
const fields = [
{ key: 'avatar', label: t(''), _style: { width: '5%' } },
{ key: 'email', label: t('user.login_id'), _style: { width: '10%' } },
{ key: 'name', label: t('user.name'), _style: { width: '10%' } },
{ key: 'userRole', label: t('user.user_role'), _style: { width: '5%' } },
@@ -130,6 +132,11 @@ const UserListTable = ({
hover
border
scopedSlots={{
avatar: (item) => (
<td className="text-center">
<Avatar src={item.avatar} fallback={item.email} />
</td>
),
name: (item) => (
<td>
<p style={{ width: 'calc(10vw)' }} className="text-truncate">

View File

@@ -72,6 +72,7 @@ const WifiAnalysisTable = ({ t, data, loading }) => {
radio: (item) => <td className="text-center">{item.radio.radio}</td>,
rxMcs: (item) => centerIfEmpty(item.rxMcs),
rxNss: (item) => centerIfEmpty(item.rxNss),
rssi: (item) => centerIfEmpty(item.rssi),
ips: (item) => displayIp(item.ipV4, item.ipV6),
}}
/>

View File

@@ -17,6 +17,7 @@ export { default as Avatar } from './components/Avatar';
export { default as WifiAnalysisTable } from './components/WifiAnalysisTable';
export { default as RadioAnalysisTable } from './components/RadioAnalysisTable';
export { default as ApiStatusCard } from './components/ApiStatusCard';
export { default as FirmwareList } from './components/FirmwareList';
// Pages
export { default as LoginPage } from './components/LoginPage';

View File

@@ -24,6 +24,7 @@ const Header = ({
t,
i18n,
logout,
logo,
authToken,
endpoints,
user,
@@ -50,7 +51,7 @@ const Header = ({
<CToggler inHeader className="ml-md-3 d-lg-none" onClick={toggleSidebarMobile} />
<CToggler inHeader className="ml-3 d-md-down-none" onClick={toggleSidebar} />
<CHeaderBrand className="mx-auto d-lg-none" to="/">
<CIcon name="logo" height="48" alt="Logo" />
<img src={logo} alt="OpenWifi" />
</CHeaderBrand>
<CHeaderNav className="d-md-down-none mr-auto" />
@@ -94,6 +95,7 @@ Header.propTypes = {
i18n: PropTypes.instanceOf(Object).isRequired,
logout: PropTypes.func.isRequired,
authToken: PropTypes.string.isRequired,
logo: PropTypes.string.isRequired,
endpoints: PropTypes.instanceOf(Object).isRequired,
user: PropTypes.instanceOf(Object).isRequired,
avatar: PropTypes.string.isRequired,

View File

@@ -29,3 +29,14 @@ export const emailToName = (email) => {
}
return 'N/A';
};
export const cleanBytesString = (bytes, decimals = 2) => {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (!bytes || bytes === 0) {
return '0 B';
}
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10);
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
};