Merge pull request #6 from stephb9959/main

Option to delete logs and healthchecks, modified how the default config is evaluated
This commit is contained in:
Charles
2021-06-22 15:09:10 -04:00
committed by GitHub
22 changed files with 410 additions and 111 deletions

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

View File

@@ -23,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.

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "0.9.1",
"version": "0.9.3",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "0.9.2",
"version": "0.9.3",
"private": true,
"dependencies": {
"@coreui/coreui": "^3.4.0",

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

@@ -12,7 +12,7 @@ const StatisticsChartList = ({ selectedDeviceId, lastRefresh }) => {
const [loading, setLoading] = useState(false);
const [statOptions, setStatOptions] = useState({
interfaceList: [],
settings: {}
settings: {},
});
const transformIntoDataset = (data) => {
@@ -62,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 / 1024));
interfaceList[interfaceTypes[inter.name]][1].data.push(
Math.floor(inter.counters.rx_bytes / 1024),
);
}
}
@@ -101,10 +103,10 @@ const StatisticsChartList = ({ selectedDeviceId, lastRefresh }) => {
const newOptions = {
interfaceList,
settings: options
settings: options,
};
if(statOptions !== newOptions){
if (statOptions !== newOptions) {
setStatOptions(newOptions);
}
};

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.2</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>