mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
synced 2025-10-30 02:12:33 +00:00
Compare commits
15 Commits
dev
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dfd8d8fb1 | ||
|
|
93c57ecfb6 | ||
|
|
666f1a0483 | ||
|
|
1b8abac84b | ||
|
|
3de2ffb1a1 | ||
|
|
419b8c84cb | ||
|
|
c9ef0b9046 | ||
|
|
7dc2ba9264 | ||
|
|
a2fc111d8c | ||
|
|
1b47daced3 | ||
|
|
ad2be40718 | ||
|
|
0f1d251c3a | ||
|
|
1bdb30ef01 | ||
|
|
dcc37a113f | ||
|
|
8d4d90197d |
27
.dockerignore
Normal file
27
.dockerignore
Normal 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
2
.env
@@ -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
14
Dockerfile
Normal 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/
|
||||
16
README.md
16
README.md
@@ -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
20
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
4
public/config.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"DEFAULT_GATEWAY_URL": "https://ucentral.dpaas.arilia.com:16001",
|
||||
"ALLOW_GATEWAY_CHANGE": false
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -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",
|
||||
|
||||
64
src/components/ConfirmFooter/index.js
Normal file
64
src/components/ConfirmFooter/index.js
Normal 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;
|
||||
100
src/components/DeleteLogModal/index.js
Normal file
100
src/components/DeleteLogModal/index.js
Normal 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;
|
||||
11
src/components/DeleteLogModal/index.module.scss
Normal file
11
src/components/DeleteLogModal/index.module.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.modal {
|
||||
color: #3c4b64;
|
||||
}
|
||||
|
||||
.spacedRow {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.spacedColumn {
|
||||
margin-top: 7px;
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -25,3 +25,7 @@
|
||||
.scrollable {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.alignRight {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,3 +17,7 @@
|
||||
.loadMoreRow {
|
||||
margin-bottom: 1%;
|
||||
}
|
||||
|
||||
.alignRight {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.chart {
|
||||
height: 360px;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"REACT_APP_BASE_URL": "https://ucentral.dpaas.arilia.com:16001/api/v1"
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.sidebarImgFull {
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.sidebarImgMinimized {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user