Compare commits

..

15 Commits

Author SHA1 Message Date
Dmitry Dunaev
1dfd8d8fb1 Add: docker related files 2021-06-28 16:13:07 +03:00
Charles
93c57ecfb6 Merge pull request #6 from stephb9959/main
Option to delete logs and healthchecks, modified how the default config is evaluated
2021-06-22 15:09:10 -04:00
bourquecharles
666f1a0483 Sidebar img size fix 2021-06-22 14:54:14 -04:00
bourquecharles
1b8abac84b Cleaning the README of env variables 2021-06-22 14:37:48 -04:00
bourquecharles
3de2ffb1a1 Changed defaults for config.json file 2021-06-22 14:34:05 -04:00
bourquecharles
419b8c84cb Now ignoring .env, using config.json in public/ 2021-06-22 14:17:54 -04:00
Charles
c9ef0b9046 Merge pull request #1 from stephb9959/feature/4-delete-logs-healtchecks
Added options to delete logs and healthchecks
2021-06-22 11:21:54 -04:00
bourquecharles
7dc2ba9264 Added options to delete logs and healthchecks 2021-06-22 11:20:54 -04:00
cbuschfb
a2fc111d8c readme update
Signed-off-by: cbuschfb <chrisbusch@fb.com>
2021-06-22 10:04:32 -04:00
Stephane Bourque
1b47daced3 Merge pull request #5 from stephb9959/main
Fixes for Interface Stats Component
2021-06-21 14:02:02 -07:00
bourquecharles
ad2be40718 Deleting console.log 2021-06-21 16:46:52 -04:00
bourquecharles
0f1d251c3a Fixed double rendering of the statistics charts 2021-06-21 16:32:50 -04:00
bourquecharles
1bdb30ef01 The rx_bytes were not converted to KB 2021-06-21 15:47:15 -04:00
bourquecharles
dcc37a113f Added back sass package 2021-06-21 15:34:10 -04:00
bourquecharles
8d4d90197d Changed version number 2021-06-21 15:28:47 -04:00
27 changed files with 472 additions and 135 deletions

27
.dockerignore Normal file
View File

@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.git
.github
Dockerfile

2
.env
View File

@@ -1,2 +0,0 @@
REACT_APP_DEFAULT_GATEWAY_URL=https://ucentral.dpaas.arilia.com:16001
REACT_APP_ALLOW_GATEWAY_CHANGE=false

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM node:16-alpine3.11 AS build
COPY package.json package-lock.json /
RUN npm install
COPY . .
RUN echo '{"DEFAULT_GATEWAY_URL": "https://ucentral.dpaas.arilia.com:16001","ALLOW_GATEWAY_CHANGE": true}' > public/config.json \
&& npm run build
FROM nginx:1.20.1-alpine AS runtime
COPY --from=build /build/ /usr/share/nginx/html/

View File

@@ -4,6 +4,8 @@
The uCentralGW Client is a user interface that lets you monitor and manage devices connected to the [uCentral gateway](https://github.com/Telecominfraproject/wlan-cloud-ucentralgw). To use the interface,
you either need to run it on your machine for [development](#development) or build it for [production](#production).
NOTE: This UI will be evolving as micro services are added to the uCentral program most notably with provisioning, base dashboard, firmware, device management
## Running the solution
### Development
@@ -21,16 +23,4 @@ git clone https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui
cd wlan-cloud-ucentralgw-ui
npm run build
```
Once the build is done, you can move the `build` folder on your server.
### Environment variables
There are two environment variables currently used to control the gateway URL and also controlling if the users can modify the gateway URL. You can modify these values in the `.env` file located in the root of the project.
During development, you will need to stop and start the project again to see those changes come into effect.
```asm
REACT_APP_DEFAULT_GATEWAY_URL=https://ucentral.dpaas.arilia.com:16001
REACT_APP_ALLOW_GATEWAY_CHANGE=false
```
- `REACT_APP_DEFAULT_GATEWAY_URL` points to the actual uCentral gateway, including the port.
- `REACT_APP_ALLOW_GATEWAY_CHANGE` : when set to `true` will allow a user to change the gateway name she wants to use. When set to `false`, will not show a text field for the gateway and will only allow users to go to the gateway speficied in `REACT_APP_DEFAULT_GATEWAY_URL`.
Once the build is done, you can move the `build` folder on your server.

20
package-lock.json generated
View File

@@ -1,11 +1,12 @@
{
"name": "ucentral-client",
"version": "0.9.0",
"version": "0.9.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.9.0",
"name": "ucentral-client",
"version": "0.9.2",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -37,6 +38,7 @@
"react-select": "^4.3.1",
"react-widgets": "^5.1.1",
"redux": "^4.1.0",
"sass": "^1.35.1",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -5248,7 +5250,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -5737,7 +5738,6 @@
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"optional": true,
"dependencies": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
@@ -10569,7 +10569,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"optional": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -17453,7 +17452,6 @@
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"optional": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -18343,8 +18341,6 @@
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.35.1.tgz",
"integrity": "sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ==",
"optional": true,
"peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0"
},
@@ -26490,8 +26486,7 @@
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"optional": true
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
},
"bindings": {
"version": "1.5.0",
@@ -26896,7 +26891,6 @@
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"optional": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
@@ -30706,7 +30700,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"optional": true,
"requires": {
"binary-extensions": "^2.0.0"
}
@@ -36047,7 +36040,6 @@
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"optional": true,
"requires": {
"picomatch": "^2.2.1"
}
@@ -36756,8 +36748,6 @@
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.35.1.tgz",
"integrity": "sha512-oCisuQJstxMcacOPmxLNiLlj4cUyN2+8xJnG7VanRoh2GOLr9RqkvI4AxA4a6LHVg/rsu+PmxXeGhrdSF9jCiQ==",
"optional": true,
"peer": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "0.9.0",
"version": "0.9.3",
"private": true,
"dependencies": {
"@coreui/coreui": "^3.4.0",
@@ -33,6 +33,7 @@
"react-select": "^4.3.1",
"react-widgets": "^5.1.1",
"redux": "^4.1.0",
"sass": "^1.35.1",
"uuid": "^8.3.2"
},
"scripts": {

4
public/config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"DEFAULT_GATEWAY_URL": "https://ucentral.dpaas.arilia.com:16001",
"ALLOW_GATEWAY_CHANGE": false
}

View File

@@ -103,6 +103,12 @@
"explanation": "Möchten Sie diesen Befehl wirklich löschen? Diese Aktion ist nicht umkehrbar.",
"title": "Befehl löschen"
},
"delete_logs": {
"date": "Wählen Sie das Datum des ältesten Protokolls aus, das Sie behalten möchten",
"device_logs_title": "Geräteprotokolle löschen",
"explanation": "Dadurch werden alle {{object}} vor dem von Ihnen gewählten Datum gelöscht. Seien Sie vorsichtig, diese Aktion ist nicht umkehrbar.",
"healthchecks_title": "Healthchecks löschen"
},
"device_logs": {
"log": "Log",
"severity": "Schwere",

View File

@@ -103,6 +103,12 @@
"explanation": "Are you sure you want to delete this command? This action is not reversible.",
"title": "Delete Command"
},
"delete_logs": {
"date": "Select the date of the oldest log you would like to keep",
"device_logs_title": "Delete Device Logs",
"explanation": "This will delete all of the {{object}} before the date you choose. Be careful, this action is not reversible.",
"healthchecks_title": "Delete Healthchecks"
},
"device_logs": {
"log": "Log",
"severity": "Severity",

View File

@@ -103,6 +103,12 @@
"explanation": "¿Está seguro de que desea eliminar este comando? Esta acción no es reversible.",
"title": "Eliminar comando"
},
"delete_logs": {
"date": "Seleccione la fecha del registro más antiguo que le gustaría conservar",
"device_logs_title": "Eliminar registros de dispositivos",
"explanation": "Esto eliminará todos los {{object}} antes de la fecha que elija. Tenga cuidado, esta acción no es reversible.",
"healthchecks_title": "Eliminar comprobaciones de estado"
},
"device_logs": {
"log": "Iniciar sesión",
"severity": "Gravedad",

View File

@@ -103,6 +103,12 @@
"explanation": "Êtes-vous sûr de vouloir supprimer cette commande ? Cette action n'est pas réversible.",
"title": "Supprimer la commande"
},
"delete_logs": {
"date": "Sélectionnez la date du plus ancien journal que vous souhaitez conserver",
"device_logs_title": "Supprimer les journaux de l'appareil",
"explanation": "Cela supprimera tous les {{object}} avant la date que vous choisissez. Attention, cette action n'est pas réversible.",
"healthchecks_title": "Supprimer les vérifications d'état"
},
"device_logs": {
"log": "Bûche",
"severity": "Gravité",

View File

@@ -103,6 +103,12 @@
"explanation": "Tem certeza de que deseja excluir este comando? esta ação não é reversível.",
"title": "Apagar Comando"
},
"delete_logs": {
"date": "Selecione a data do registro mais antigo que você gostaria de manter",
"device_logs_title": "Excluir registros do dispositivo",
"explanation": "Isso excluirá todos os {{object}} antes da data que você escolheu. Cuidado, esta ação não é reversível.",
"healthchecks_title": "Excluir verificações de saúde"
},
"device_logs": {
"log": "Registro",
"severity": "Gravidade",

View File

@@ -0,0 +1,64 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { CButton, CSpinner, CModalFooter } from '@coreui/react';
const ConfirmFooter = ({ isShown, isLoading, action, color, variant, block, toggleParent }) => {
const { t } = useTranslation();
const [askingIfSure, setAskingIfSure] = useState(false);
const confirmingIfSure = () => {
setAskingIfSure(true);
};
useEffect(() => {
setAskingIfSure(false);
}, [isShown]);
return (
<CModalFooter>
<div hidden={!askingIfSure}>{t('common.are_you_sure')}</div>
<CButton
disabled={isLoading}
hidden={askingIfSure}
color={color}
variant={variant}
onClick={() => confirmingIfSure()}
block={block}
>
{t('common.submit')}
</CButton>
<CButton
disabled={isLoading}
hidden={!askingIfSure}
color={color}
onClick={() => action()}
block={block}
>
{isLoading ? t('common.loading_ellipsis') : t('common.yes')}
<CSpinner color="light" hidden={!isLoading} component="span" size="sm" />
</CButton>
<CButton color="secondary" onClick={toggleParent}>
{t('common.cancel')}
</CButton>
</CModalFooter>
);
};
ConfirmFooter.propTypes = {
isLoading: PropTypes.bool.isRequired,
block: PropTypes.bool,
action: PropTypes.func.isRequired,
color: PropTypes.string,
variant: PropTypes.string,
toggleParent: PropTypes.func.isRequired,
isShown: PropTypes.bool.isRequired,
};
ConfirmFooter.defaultProps = {
color: 'primary',
variant: '',
block: false,
};
export default ConfirmFooter;

View File

@@ -0,0 +1,100 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CModal, CModalHeader, CModalTitle, CModalBody, CCol, CRow } from '@coreui/react';
import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import ConfirmFooter from 'components/ConfirmFooter';
import { dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import { getToken } from 'utils/authHelper';
import eventBus from 'utils/eventBus';
import styles from './index.module.scss';
const DeleteLogModal = ({ serialNumber, show, toggle, object }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [maxDate, setMaxDate] = useState(new Date().toString());
const setDate = (date) => {
if (date) {
setMaxDate(date.toString());
}
};
const deleteLog = async () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${getToken()}`,
},
params: {
endDate: dateToUnix(maxDate),
},
};
return axiosInstance
.delete(`/device/${serialNumber}/${object}`, options)
.then(() => {})
.catch(() => {})
.finally(() => {
if (object === 'healthchecks')
eventBus.dispatch('deletedHealth', { message: 'Healthcheck was deleted' });
else if (object === 'logs')
eventBus.dispatch('deletedLogs', { message: 'Deleted device logs' });
setLoading(false);
toggle();
});
};
useEffect(() => {
setLoading(false);
setMaxDate(new Date().toString());
}, [show]);
return (
<CModal className={styles.modal} show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle>
{object === 'healthchecks'
? t('delete_logs.healthchecks_title')
: t('delete_logs.device_logs_title')}
</CModalTitle>
</CModalHeader>
<CModalBody>
<h6>{t('delete_logs.explanation', { object })}</h6>
<CRow className={styles.spacedRow}>
<CCol md="4" className={styles.spacedDate}>
<p>{t('common.date')}:</p>
</CCol>
<CCol xs="12" md="8">
<DatePicker
selected={new Date(maxDate)}
includeTime
value={new Date(maxDate)}
placeholder="Select custom date"
disabled={loading}
onChange={(date) => setDate(date)}
/>
</CCol>
</CRow>
</CModalBody>
<ConfirmFooter
isShown={show}
isLoading={loading}
action={deleteLog}
color="primary"
toggleParent={toggle}
/>
</CModal>
);
};
DeleteLogModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
object: PropTypes.string.isRequired,
serialNumber: PropTypes.string.isRequired,
};
export default DeleteLogModal;

View File

@@ -0,0 +1,11 @@
.modal {
color: #3c4b64;
}
.spacedRow {
margin-top: 20px;
}
.spacedColumn {
margin-top: 7px;
}

View File

@@ -10,6 +10,7 @@ import {
CRow,
CCol,
CProgress,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { useTranslation } from 'react-i18next';
@@ -18,7 +19,9 @@ import PropTypes from 'prop-types';
import { prettyDate, dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import { getToken } from 'utils/authHelper';
import eventBus from 'utils/eventBus';
import LoadingButton from 'components/LoadingButton';
import DeleteLogModal from 'components/DeleteLogModal';
import styles from './index.module.scss';
const DeviceHealth = ({ selectedDeviceId }) => {
@@ -34,6 +37,11 @@ const DeviceHealth = ({ selectedDeviceId }) => {
const [showLoadingMore, setShowLoadingMore] = useState(true);
const [sanityLevel, setSanityLevel] = useState(null);
const [barColor, setBarColor] = useState('gradient-dark');
const [showDeleteModal, setShowDeleteModal] = useState(false);
const toggleDeleteModal = () => {
setShowDeleteModal(!showDeleteModal);
};
const toggle = (e) => {
setCollapse(!collapse);
@@ -167,6 +175,14 @@ const DeviceHealth = ({ selectedDeviceId }) => {
}
}, [start, end, selectedDeviceId]);
useEffect(() => {
eventBus.on('deletedHealth', () => getDeviceHealth());
return () => {
eventBus.remove('deletedHealth');
};
}, []);
return (
<CWidgetDropdown
header={sanityLevel ? `${sanityLevel}%` : t('common.unknown')}
@@ -178,6 +194,20 @@ const DeviceHealth = ({ selectedDeviceId }) => {
<div className={styles.footer}>
<CProgress className={styles.progressBar} color="white" value={sanityLevel ?? 0} />
<CCollapse show={collapse}>
<div className={styles.alignRight}>
<CPopover content={t('common.delete')}>
<CButton
color="light"
shape="square"
size="sm"
onClick={() => {
toggleDeleteModal();
}}
>
<CIcon name="cilTrash" size="lg" />
</CButton>
</CPopover>
</div>
<CRow className={styles.spacedRow}>
<CCol>
{t('common.from')}:
@@ -250,6 +280,12 @@ const DeviceHealth = ({ selectedDeviceId }) => {
size="lg"
/>
</CButton>
<DeleteLogModal
serialNumber={selectedDeviceId}
object="healthchecks"
show={showDeleteModal}
toggle={toggleDeleteModal}
/>
</div>
}
/>

View File

@@ -25,3 +25,7 @@
.scrollable {
height: 250px;
}
.alignRight {
float: right;
}

View File

@@ -9,6 +9,7 @@ import {
CDataTable,
CCard,
CCardBody,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { useTranslation } from 'react-i18next';
@@ -17,7 +18,9 @@ import PropTypes from 'prop-types';
import { prettyDate, dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import { getToken } from 'utils/authHelper';
import eventBus from 'utils/eventBus';
import LoadingButton from 'components/LoadingButton';
import DeleteLogModal from 'components/DeleteLogModal';
import styles from './index.module.scss';
const DeviceLogs = ({ selectedDeviceId }) => {
@@ -31,6 +34,11 @@ const DeviceLogs = ({ selectedDeviceId }) => {
const [logLimit, setLogLimit] = useState(25);
const [loadingMore, setLoadingMore] = useState(false);
const [showLoadingMore, setShowLoadingMore] = useState(true);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const toggleDeleteModal = () => {
setShowDeleteModal(!showDeleteModal);
};
const toggle = (e) => {
setCollapse(!collapse);
@@ -149,85 +157,115 @@ const DeviceLogs = ({ selectedDeviceId }) => {
}
}, [start, end, selectedDeviceId]);
useEffect(() => {
eventBus.on('deletedLogs', () => getLogs());
return () => {
eventBus.remove('deletedLogs');
};
}, []);
return (
<CWidgetDropdown
inverse="true"
color="gradient-info"
header={t('device_logs.title')}
footerSlot={
<div className={styles.footer}>
<CCollapse show={collapse}>
<CRow className={styles.datepickerRow}>
<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={[styles.scrollableCard, 'overflow-auto'].join(' ')}>
<CDataTable
items={logs ?? []}
fields={columns}
loading={loading}
className={styles.whiteIcon}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
recorded: (item) => <td>{prettyDate(item.recorded)}</td>,
show_details: (item, index) => (
<td className="py-2">
<CButton
color="primary"
variant={details.includes(index) ? '' : 'outline'}
shape="square"
size="sm"
onClick={() => {
toggleDetails(index);
}}
>
<CIcon name="cilList" size="lg" />
</CButton>
</td>
),
details: (item, index) => (
<CCollapse show={details.includes(index)}>
<CCardBody>
<h5>{t('common.details')}</h5>
<div>{getDetails(index, item)}</div>
</CCardBody>
</CCollapse>
),
}}
/>
<CRow className={styles.loadMoreRow}>
{showLoadingMore && (
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
)}
</CRow>
<div>
<CWidgetDropdown
inverse="true"
color="gradient-info"
header={t('device_logs.title')}
footerSlot={
<div className={styles.footer}>
<CCollapse show={collapse}>
<div className={styles.alignRight}>
<CPopover content={t('common.delete')}>
<CButton
color="light"
shape="square"
size="sm"
onClick={() => {
toggleDeleteModal();
}}
>
<CIcon name="cilTrash" size="lg" />
</CButton>
</CPopover>
</div>
</CCard>
</CCollapse>
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className={styles.whiteIcon}
size="lg"
/>
</CButton>
</div>
}
>
<CIcon name="cilList" className={styles.whiteIcon} size="lg" />
</CWidgetDropdown>
<CRow className={styles.datepickerRow}>
<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={[styles.scrollableCard, 'overflow-auto'].join(' ')}>
<CDataTable
items={logs ?? []}
fields={columns}
loading={loading}
className={styles.whiteIcon}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
recorded: (item) => <td>{prettyDate(item.recorded)}</td>,
show_details: (item, index) => (
<td className="py-2">
<CButton
color="primary"
variant={details.includes(index) ? '' : 'outline'}
shape="square"
size="sm"
onClick={() => {
toggleDetails(index);
}}
>
<CIcon name="cilList" size="lg" />
</CButton>
</td>
),
details: (item, index) => (
<CCollapse show={details.includes(index)}>
<CCardBody>
<h5>{t('common.details')}</h5>
<div>{getDetails(index, item)}</div>
</CCardBody>
</CCollapse>
),
}}
/>
<CRow className={styles.loadMoreRow}>
{showLoadingMore && (
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
)}
</CRow>
</div>
</CCard>
</CCollapse>
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className={styles.whiteIcon}
size="lg"
/>
</CButton>
</div>
}
>
<CIcon name="cilList" className={styles.whiteIcon} size="lg" />
</CWidgetDropdown>
<DeleteLogModal
serialNumber={selectedDeviceId}
object="logs"
show={showDeleteModal}
toggle={toggleDeleteModal}
/>
</div>
);
};

View File

@@ -17,3 +17,7 @@
.loadMoreRow {
margin-bottom: 1%;
}
.alignRight {
float: right;
}

View File

@@ -1,10 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import Chart from 'react-apexcharts';
import styles from './index.module.scss';
const DeviceStatisticsChart = ({ data, options }) => (
<div className={styles.chart}>
<div style={{ height: '360px' }}>
<Chart series={data} options={options} type="line" height="100%" />
</div>
);

View File

@@ -1,3 +0,0 @@
.chart {
height: 360px;
}

View File

@@ -10,8 +10,10 @@ import DeviceStatisticsChart from '../DeviceStatisticsChart';
const StatisticsChartList = ({ selectedDeviceId, lastRefresh }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [deviceStats, setStats] = useState([]);
const [statOptions, setStatOptions] = useState({});
const [statOptions, setStatOptions] = useState({
interfaceList: [],
settings: {},
});
const transformIntoDataset = (data) => {
const sortedData = data.sort((a, b) => {
@@ -60,7 +62,9 @@ const StatisticsChartList = ({ selectedDeviceId, lastRefresh }) => {
interfaceList[interfaceTypes[inter.name]][0].data.push(
Math.floor(inter.counters.tx_bytes / 1024),
);
interfaceList[interfaceTypes[inter.name]][1].data.push(Math.floor(inter.counters.rx_bytes));
interfaceList[interfaceTypes[inter.name]][1].data.push(
Math.floor(inter.counters.rx_bytes / 1024),
);
}
}
@@ -97,8 +101,14 @@ const StatisticsChartList = ({ selectedDeviceId, lastRefresh }) => {
},
};
setStatOptions(options);
setStats(interfaceList);
const newOptions = {
interfaceList,
settings: options,
};
if (statOptions !== newOptions) {
setStatOptions(newOptions);
}
};
const getStatistics = () => {
@@ -134,20 +144,20 @@ const StatisticsChartList = ({ selectedDeviceId, lastRefresh }) => {
}, [selectedDeviceId]);
useEffect(() => {
if (lastRefresh !== '' && selectedDeviceId) {
if (!loading && lastRefresh !== '' && selectedDeviceId) {
getStatistics();
}
}, [lastRefresh]);
return (
<div>
{deviceStats.map((data) => (
{statOptions.interfaceList.map((data) => (
<div key={createUuid()}>
<DeviceStatisticsChart
key={createUuid()}
data={data}
options={{
...statOptions,
...statOptions.settings,
title: {
text: capitalizeFirstLetter(data[0].titleName),
align: 'left',

View File

@@ -9,7 +9,7 @@ import styles from './index.module.scss';
const DeviceStatisticsCard = ({ selectedDeviceId }) => {
const { t } = useTranslation();
const [lastRefresh, setLastRefresh] = useState('');
const [lastRefresh, setLastRefresh] = useState(new Date().toString());
const refresh = () => {
setLastRefresh(new Date().toString());

View File

@@ -1,3 +0,0 @@
{
"REACT_APP_BASE_URL": "https://ucentral.dpaas.arilia.com:16001/api/v1"
}

View File

@@ -6,7 +6,7 @@ const TheFooter = () => (
<Translation>
{(t) => (
<CFooter fixed={false}>
<div>{t('footer.version')} 0.9.0</div>
<div>{t('footer.version')} 0.9.3</div>
<div className="mfs-auto">
<span className="mr-1">{t('footer.powered_by')}</span>
<a href="https://coreui.io/react" target="_blank" rel="noopener noreferrer">

View File

@@ -1,5 +1,6 @@
.sidebarImgFull {
height: 75px;
width: 75px;
}
.sidebarImgMinimized {

View File

@@ -30,15 +30,30 @@ const Login = () => {
const dispatch = useDispatch();
const [userId, setUsername] = useState('');
const [password, setPassword] = useState('');
const [gatewayUrl, setGatewayUrl] = useState(process.env.REACT_APP_DEFAULT_GATEWAY_URL);
const [gatewayUrl, setGatewayUrl] = useState('');
const [hadError, setHadError] = useState(false);
const [emptyUsername, setEmptyUsername] = useState(false);
const [emptyPassword, setEmptyPassword] = useState(false);
const [emptyGateway, setEmptyGateway] = useState(false);
const placeholderUrl = 'Gateway URL (ex: https://ucentral.dpaas.arilia.com:16001)';
const defaultGatewayUrl = process.env.REACT_APP_DEFAULT_GATEWAY_URL;
const allowUrlChange = process.env.REACT_APP_ALLOW_GATEWAY_CHANGE === 'true';
const loginErrorText = t('login.login_error');
const [defaultConfig, setDefaultConfig] = useState({
DEFAULT_GATEWAY_URL: '',
ALLOW_GATEWAY_CHANGE: true,
});
const placeholderUrl = 'Gateway URL (ex: https://your-url:port)';
const getDefaultConfig = async () => {
fetch('./config.json', {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
})
.then((response) => response.json())
.then((json) => {
setDefaultConfig(json);
})
.catch();
};
const formValidation = () => {
setHadError(false);
@@ -59,12 +74,13 @@ const Login = () => {
setEmptyGateway(true);
isSuccessful = false;
}
return isSuccessful;
};
const SignIn = (credentials) => {
const gatewayUrlToUse = allowUrlChange ? gatewayUrl : defaultGatewayUrl;
const gatewayUrlToUse = defaultConfig.ALLOW_GATEWAY_CHANGE
? gatewayUrl
: defaultConfig.DEFAULT_GATEWAY_URL;
axiosInstance
.post(`${gatewayUrlToUse}/api/v1/oauth2`, credentials)
@@ -93,6 +109,12 @@ const Login = () => {
useEffect(() => {
if (emptyGateway) setEmptyGateway(false);
}, [gatewayUrl]);
useEffect(() => {
getDefaultConfig();
}, []);
useEffect(() => {
setGatewayUrl(defaultConfig.DEFAULT_GATEWAY_URL);
}, [defaultConfig]);
return (
<div className="c-app c-default-layout flex-row align-items-center">
@@ -151,7 +173,7 @@ const Login = () => {
{t('login.please_enter_password')}
</CInvalidFeedback>
</CInputGroup>
<CInputGroup className="mb-4" hidden={!allowUrlChange}>
<CInputGroup className="mb-4" hidden={!defaultConfig.ALLOW_GATEWAY_CHANGE}>
<CPopover content="Gateway URL">
<CInputGroupPrepend>
<CInputGroupText>
@@ -175,7 +197,7 @@ const Login = () => {
<CRow>
<CCol>
<CAlert show={hadError} color="danger">
{loginErrorText}
{t('login.login_error')}
</CAlert>
</CCol>
</CRow>