Merge pull request #122 from stephb9959/dev

Version 2.5.24
This commit is contained in:
Charles
2022-01-14 19:57:12 +01:00
committed by GitHub
13 changed files with 414 additions and 73 deletions

4
package-lock.json generated
View File

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

View File

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

View File

@@ -47,6 +47,7 @@
"cancel": "Abbrechen",
"certificate": "Zertifikat",
"certificates": "Zertifikate",
"claim": "Anspruch",
"clear": "Löschen",
"close": "Schließen",
"code": "Code",
@@ -117,6 +118,7 @@
"hours": "std",
"id": "ID",
"invalid_credentials": "Ungültiger Benutzername und / oder Passwort",
"invalid_date_explanation": "Ungültiges Datum, bitte verwenden Sie den Kalender, auf den Sie über die Schaltfläche rechts zugreifen können",
"invalid_file": "Die ausgewählte Datei war ungültig, bitte lesen Sie die Anweisungen und passen Sie Ihre Datei entsprechend an",
"invalid_password": "Dieses Passwort entspricht nicht den grundlegenden Passwortregeln. Bitte besuchen Sie unsere Seite Passwortrichtlinien, um mehr zu erfahren",
"invalid_pem": "Ihre PEM-Datei ist ungültig. Es sollte mit '-----BEGIN CERTIFICATE-----' ODER '-----BEGIN PRIVATE KEY-----' beginnen und mit '-----END CERTIFICATE--- enden. --' ODER '-----END PRIVATSCHLÜSSEL-----'",
@@ -162,6 +164,7 @@
"recorded": "Verzeichnet",
"refresh": "Aktualisierung",
"refresh_device": "Gerät aktualisieren",
"remove_claim": "Anspruch entfernen",
"required": "Erforderlich",
"result": "Ergebnis",
"save": "Sparen",
@@ -655,11 +658,14 @@
},
"statistics": {
"data": "Daten (KB)",
"data_mb": "Daten (MB)",
"latest_statistics": "Neueste Statistiken",
"lifetime_stats": "Lifetime-Statistik",
"memory": "Erinnerung",
"no_interfaces": "Keine Statistiken zur Schnittstellenlebensdauer verfügbar",
"show_latest": "Letzte Statistik",
"title": "Statistiken"
"title": "Statistiken",
"used": "Verwendeter Speicher %"
},
"status": {
"connection_status": "Status",
@@ -671,17 +677,22 @@
"percentage_free": "{{percentage}}% von {{total}} kostenlos",
"percentage_used": "{{percentage}}% von {{total}} verwendet",
"title": "#{{serialNumber}} Status",
"total_memory": "Gesamtspeicher",
"uptime": "Betriebszeit",
"used_total_memory": "{{used}} verwendet / {{total}} insgesamt"
},
"subscriber": {
"add_device_subscriber_explanation": "Um andere Geräte zu reklamieren, kannst du unsere Suchleiste verwenden oder direkt aus der Tabelle reklamieren. Wenn ein Gerät bereits von einem Benutzer beansprucht wurde, müssen Sie zu dessen Details gehen und die Zuweisung aufheben, bevor Sie es beanspruchen.",
"create": "Abonnenten erstellen",
"devices_one": "{{count}} Gerät",
"devices_other": "{{count}} Geräte",
"edit": "Abonnent bearbeiten",
"error_create": "Fehler beim Erstellen des Abonnenten: {{error}}",
"error_delete": "Fehler beim Löschen des Abonnenten: {{error}}",
"error_fetching": "Fehler beim Abrufen von Abonnenten: {{error}}",
"error_fetching_single": "Fehler beim Abrufen des Abonnenten: {{error}}",
"error_update": "Fehler beim Aktualisieren des Abonnenten: {{error}}",
"is_already_claimed": "wird bereits beansprucht von",
"subscribers": "Abonnenten",
"success_create": "Abonnent erfolgreich erstellt!",
"success_delete": "Abonnent erfolgreich gelöscht!",

View File

@@ -47,6 +47,7 @@
"cancel": "Cancel",
"certificate": "Certificate",
"certificates": "Certificates",
"claim": "Claim",
"clear": "Clear",
"close": "Close",
"code": "Code",
@@ -117,6 +118,7 @@
"hours": "hours",
"id": "Id",
"invalid_credentials": "Invalid username and/or password",
"invalid_date_explanation": "Invalid Date, please use the calendar accessible with the button on the right ",
"invalid_file": "The chosen file was invalid, please read the instructions and adjust your file accordingly",
"invalid_password": "This password does not confirm to basic password rules. Please visit our Password Policy page to learn more",
"invalid_pem": "Your .pem file is invalid. It should start with '-----BEGIN CERTIFICATE-----' OR '-----BEGIN PRIVATE KEY-----' and it should end with '-----END CERTIFICATE-----' OR '-----END PRIVATE KEY-----'",
@@ -162,6 +164,7 @@
"recorded": "Recorded",
"refresh": "Refresh",
"refresh_device": "Refresh Device",
"remove_claim": "Remove Claim",
"required": "Required",
"result": "Result",
"save": "Save",
@@ -655,11 +658,14 @@
},
"statistics": {
"data": "Data (KB)",
"data_mb": "Data (MB)",
"latest_statistics": "Latest Statistics",
"lifetime_stats": "Lifetime Statistics",
"memory": "Memory",
"no_interfaces": "No interface lifetime statistics available",
"show_latest": "Last Statistics",
"title": "Statistics"
"title": "Statistics",
"used": "Used Memory %"
},
"status": {
"connection_status": "Status",
@@ -671,17 +677,22 @@
"percentage_free": "{{percentage}}% of {{total}} free",
"percentage_used": "{{percentage}}% of {{total}} used",
"title": "#{{serialNumber}} Status",
"total_memory": "Total Memory",
"uptime": "Uptime",
"used_total_memory": "{{used}} used / {{total}} total "
},
"subscriber": {
"add_device_subscriber_explanation": "To claim devices, you can use our search bar or claim directly from the table. If a device was already claimed by a user, you will need to go to to their details and unassign it before claiming it.",
"create": "Create Subscriber",
"devices_one": "{{count}} Device",
"devices_other": "{{count}} Devices",
"edit": "Edit Subscriber",
"error_create": "Error creating subscriber: {{error}}",
"error_delete": "Error deleting subscriber: {{error}}",
"error_fetching": "Error fetching subscribers: {{error}}",
"error_fetching_single": "Error fetching subscriber: {{error}}",
"error_update": "Error updating subscriber: {{error}}",
"is_already_claimed": "is already claimed by ",
"subscribers": "Subscribers",
"success_create": "Subscriber successfully created!",
"success_delete": "Subscriber successfully deleted!",

View File

@@ -47,6 +47,7 @@
"cancel": "Cancelar",
"certificate": "Certificado",
"certificates": "Certificados",
"claim": "Reclamación",
"clear": "Claro",
"close": "Cerrar",
"code": "Código",
@@ -117,6 +118,7 @@
"hours": "horas",
"id": "Carné de identidad",
"invalid_credentials": "Nombre de usuario y / o contraseña inválido",
"invalid_date_explanation": "Fecha no válida, utilice el calendario accesible con el botón de la derecha",
"invalid_file": "El archivo elegido no es válido, lea las instrucciones y ajuste su archivo en consecuencia",
"invalid_password": "Esta contraseña no confirma las reglas básicas de contraseña. Visite nuestra página de Política de contraseñas para obtener más información.",
"invalid_pem": "Su archivo .pem no es válido. Debe comenzar con '----- BEGIN CERTIFICATE -----' O '----- BEGIN PRIVATE KEY -----' y debe terminar con '----- END CERTIFICATE --- - 'O' ----- FIN DE CLAVE PRIVADA ----- '",
@@ -162,6 +164,7 @@
"recorded": "Grabado",
"refresh": "Refrescar",
"refresh_device": "Actualizar dispositivo",
"remove_claim": "Quitar reclamo",
"required": "Necesario",
"result": "Resultado",
"save": "Salvar",
@@ -655,11 +658,14 @@
},
"statistics": {
"data": "Datos (KB)",
"data_mb": "Datos (MB)",
"latest_statistics": "Últimas estadísticas",
"lifetime_stats": "Estadísticas de por vida",
"memory": "Memoria",
"no_interfaces": "No hay estadísticas de vida útil de la interfaz disponibles",
"show_latest": "Últimas estadísticas",
"title": "estadística"
"title": "estadística",
"used": "Memoria usada %"
},
"status": {
"connection_status": "Estado",
@@ -671,17 +677,22 @@
"percentage_free": "{{percentage}}% de {{total}} gratis",
"percentage_used": "{{percentage}}% de {{total}} utilizado",
"title": "#{{serialNumber}} Estado",
"total_memory": "Memoria total",
"uptime": "Tiempo de actividad",
"used_total_memory": "{{used}} usado / {{total}} total"
},
"subscriber": {
"add_device_subscriber_explanation": "Para reclamar otros dispositivos, puede usar nuestra barra de búsqueda o reclamar directamente desde la tabla. Si un dispositivo ya fue reclamado por un usuario, deberá ir a sus detalles y anular la asignación antes de reclamarlo.",
"create": "Crear suscriptor",
"devices_one": "{{count}} dispositivo",
"devices_other": "{{count}} dispositivos",
"edit": "Editar suscriptor",
"error_create": "Error al crear el suscriptor: {{error}}",
"error_delete": "Error al eliminar el suscriptor: {{error}}",
"error_fetching": "Error al obtener suscriptores: {{error}}",
"error_fetching_single": "Error al obtener el suscriptor: {{error}}",
"error_update": "Error al actualizar el suscriptor: {{error}}",
"is_already_claimed": "ya es reclamado por",
"subscribers": "Suscriptores",
"success_create": "¡Suscriptor creado correctamente!",
"success_delete": "¡Suscriptor eliminado correctamente!",

View File

@@ -47,6 +47,7 @@
"cancel": "annuler",
"certificate": "Certificat",
"certificates": "Certificats",
"claim": "Prétendre",
"clear": "Clair",
"close": "Fermer",
"code": "Code",
@@ -117,6 +118,7 @@
"hours": "heures",
"id": "Id",
"invalid_credentials": "Nom d'utilisateur et / ou mot de passe incorrect",
"invalid_date_explanation": "Date invalide, merci d'utiliser le calendrier accessible avec le bouton à droite",
"invalid_file": "Le fichier choisi n'était pas valide, veuillez lire les instructions et ajuster votre fichier en conséquence",
"invalid_password": "Ce mot de passe ne confirme pas les règles de base des mots de passe. Veuillez visiter notre page Politique de mot de passe pour en savoir plus",
"invalid_pem": "Votre fichier .pem n'est pas valide. Il doit commencer par '-----BEGIN CERTIFICATE-----' OU '-----BEGIN PRIVATE KEY-----' et il doit se terminer par '-----END CERTIFICATE--- --' OU '-----FIN CLÉ PRIVÉE-----'",
@@ -162,6 +164,7 @@
"recorded": "Enregistré",
"refresh": "Rafraîchir",
"refresh_device": "Actualiser l'appareil",
"remove_claim": "Supprimer la réclamation",
"required": "Champs obligatoires",
"result": "Résultat",
"save": "Sauvegarder",
@@ -655,11 +658,14 @@
},
"statistics": {
"data": "Données (Ko)",
"data_mb": "Données (Mo)",
"latest_statistics": "Dernières statistiques",
"lifetime_stats": "Statistiques à vie",
"memory": "mémoire",
"no_interfaces": "Aucune statistique de durée de vie de l'interface disponible",
"show_latest": "Dernières statistiques",
"title": "statistiques"
"title": "statistiques",
"used": "Mémoire utilisée %"
},
"status": {
"connection_status": "Statut",
@@ -671,17 +677,22 @@
"percentage_free": "{{percentage}}% de {{total}} gratuit",
"percentage_used": "{{percentage}}% de {{total}} utilisé",
"title": "#{{serialNumber}} état",
"total_memory": "Mémoire totale",
"uptime": "La disponibilité",
"used_total_memory": "{{used}} utilisé / {{total}} total"
},
"subscriber": {
"add_device_subscriber_explanation": "Pour réclamer d'autres appareils, vous pouvez utiliser notre barre de recherche ou réclamer directement à partir du tableau. Si un appareil a déjà été réclamé par un utilisateur, vous devrez accéder à ses détails et le désaffecter avant de le réclamer.",
"create": "Créer un abonné",
"devices_one": "{{count}} Appareil",
"devices_other": "{{count}} appareils",
"edit": "Modifier l'abonné",
"error_create": "Erreur lors de la création de l'abonné : {{error}}",
"error_delete": "Erreur lors de la suppression de l'abonné : {{error}}",
"error_fetching": "Erreur lors de la récupération des abonnés : {{error}}",
"error_fetching_single": "Erreur lors de la récupération de l'abonné : {{error}}",
"error_update": "Erreur lors de la mise à jour de l'abonné : {{error}}",
"is_already_claimed": "est déjà réclamé par",
"subscribers": "Les abonnés",
"success_create": "Abonné créé avec succès !",
"success_delete": "Abonné supprimé avec succès !",

View File

@@ -47,6 +47,7 @@
"cancel": "Cancelar",
"certificate": "Certificado",
"certificates": "Certificados",
"claim": "Afirmação",
"clear": "Claro",
"close": "Perto",
"code": "Código",
@@ -117,6 +118,7 @@
"hours": "horas",
"id": "identidade",
"invalid_credentials": "Nome de usuário e / ou senha inválidos",
"invalid_date_explanation": "Data inválida, use o calendário acessível com o botão à direita",
"invalid_file": "O arquivo escolhido era inválido, por favor, leia as instruções e ajuste seu arquivo de acordo",
"invalid_password": "Esta senha não está de acordo com as regras básicas de senha. Visite nossa página de Política de Senha para saber mais",
"invalid_pem": "Seu arquivo .pem é inválido. Deve começar com '----- BEGIN CERTIFICATE -----' OU '----- BEGIN PRIVATE KEY -----' e deve terminar com '----- END CERTIFICATE --- - 'OU' ----- END PRIVATE KEY ----- '",
@@ -162,6 +164,7 @@
"recorded": "Gravado",
"refresh": "REFRESH",
"refresh_device": "Atualizar dispositivo",
"remove_claim": "Remover reivindicação",
"required": "Requeridos",
"result": "Resultado",
"save": "Salve",
@@ -655,11 +658,14 @@
},
"statistics": {
"data": "Dados (KB)",
"data_mb": "Dados (MB)",
"latest_statistics": "Estatísticas mais recentes",
"lifetime_stats": "Estatísticas de vida",
"memory": "Memória",
"no_interfaces": "Nenhuma estatística de tempo de vida da interface disponível",
"show_latest": "Últimas estatísticas",
"title": "Estatisticas"
"title": "Estatisticas",
"used": "Memoria usada %"
},
"status": {
"connection_status": "Status",
@@ -671,17 +677,22 @@
"percentage_free": "{{percentage}}% de {{total}} grátis",
"percentage_used": "{{percentage}}% de {{total}} usado",
"title": "#{{serialNumber}} status",
"total_memory": "Memória total",
"uptime": "Tempo de atividade",
"used_total_memory": "{{used}} usado / {{total}} total"
},
"subscriber": {
"add_device_subscriber_explanation": "Para reivindicar outros dispositivos, você pode usar nossa barra de pesquisa ou reivindicar diretamente na tabela. Se um dispositivo já foi reivindicado por um usuário, você precisará acessar os detalhes dele e cancelar a atribuição antes de reivindicá-lo.",
"create": "Criar assinante",
"devices_one": "{{count}} Dispositivo",
"devices_other": "{{count}} dispositivos",
"edit": "Editar Assinante",
"error_create": "Erro ao criar assinante: {{error}}",
"error_delete": "Erro ao excluir assinante: {{error}}",
"error_fetching": "Erro ao buscar assinantes: {{error}}",
"error_fetching_single": "Erro ao buscar assinante: {{error}}",
"error_update": "Erro ao atualizar assinante: {{error}}",
"is_already_claimed": "já é reivindicado por",
"subscribers": "Inscritos",
"success_create": "Assinante criado com sucesso!",
"success_delete": "Assinante excluído com sucesso!",

View File

@@ -10,6 +10,7 @@ import {
CCard,
CPopover,
CButtonToolbar,
CFormText,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import DatePicker from 'react-widgets/DatePicker';
@@ -41,7 +42,9 @@ const DeviceCommands = () => {
const [commands, setCommands] = useState([]);
const [loading, setLoading] = useState(false);
const [start, setStart] = useState('');
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState('');
const [endError, setEndError] = useState(false);
const [commandLimit, setCommandLimit] = useState(25);
// Load more button related
const [loadingMore, setLoadingMore] = useState(false);
@@ -65,11 +68,25 @@ const DeviceCommands = () => {
};
const modifyStart = (value) => {
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
} catch (e) {
setEnd('');
setEndError(true);
}
};
const deleteCommandFromList = (commandUuid) => {
@@ -256,10 +273,16 @@ const DeviceCommands = () => {
<CCol>
From:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</CCol>
<CCol>
To:
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</CCol>
</CRow>
<CCard>

View File

@@ -12,4 +12,4 @@ DeviceStatisticsChart.propTypes = {
chart: PropTypes.instanceOf(Object).isRequired,
};
export default DeviceStatisticsChart;
export default React.memo(DeviceStatisticsChart);

View File

@@ -1,37 +1,94 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { CSpinner } from '@coreui/react';
import { useTranslation } from 'react-i18next';
import { v4 as createUuid } from 'uuid';
import axiosInstance from 'utils/axiosInstance';
import { useAuth, useDevice } from 'ucentral-libs';
import { unixToTime, capitalizeFirstLetter } from 'utils/helper';
import eventBus from 'utils/eventBus';
import {
capitalizeFirstLetter,
datesSameDay,
dateToUnix,
prettyDate,
unixToTime,
} from 'utils/helper';
import DeviceStatisticsChart from './DeviceStatisticsChart';
const StatisticsChartList = () => {
const StatisticsChartList = ({ setOptions, section, setStart, setEnd, time }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const [statOptions, setStatOptions] = useState({
interfaceList: [],
memory: [],
settings: {},
});
const transformIntoDataset = (data) => {
const sortedData = data.sort((a, b) => {
let sortedData = data.sort((a, b) => {
if (a.recorded > b.recorded) return 1;
if (b.recorded > a.recorded) return -1;
return 0;
});
const dataLength = sortedData.length;
if (dataLength > 1000 && dataLength < 3000) {
sortedData = sortedData.filter((dat, index) => index % 4 === 0);
} else if (dataLength >= 3000 && dataLength < 5000) {
sortedData = sortedData.filter((dat, index) => index % 8 === 0);
} else if (dataLength >= 5000 && dataLength < 7000) {
sortedData = sortedData.filter((dat, index) => index % 12 === 0);
} else if (dataLength > 7000) {
sortedData = sortedData.filter((dat, index) => index % 20 === 0);
}
// Looping through data to build our memory graph data
const memoryUsed = [
{
titleName: t('statistics.memory'),
name: 'Used',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
{
titleName: t('statistics.memory'),
name: 'Buffered',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
{
titleName: t('statistics.memory'),
name: 'Cached',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
];
for (const log of sortedData) {
memoryUsed[0].data.push(
Math.floor((log.data.unit.memory.total - log.data.unit.memory.free) / 1024 / 1024),
);
memoryUsed[1].data.push(Math.floor(log.data.unit.memory.buffered / 1024 / 1024));
memoryUsed[2].data.push(Math.floor(log.data.unit.memory.cached / 1024 / 1024));
}
// This dictionary will have a key that is the interface name and a value of it's index in the final array
const interfaceTypes = {};
const interfaceList = [];
const categories = [];
let i = 0;
const areSameDay = datesSameDay(
new Date(sortedData[0].recorded * 1000),
new Date(sortedData[sortedData.length - 1].recorded * 1000),
);
// Just building the array for all the interfaces
for (const log of sortedData) {
categories.push(unixToTime(log.recorded));
categories.push(areSameDay ? unixToTime(log.recorded) : prettyDate(log.recorded));
for (const logInterface of log.data.interfaces) {
if (interfaceTypes[logInterface.name] === undefined) {
interfaceTypes[logInterface.name] = i;
@@ -69,10 +126,9 @@ const StatisticsChartList = () => {
}
}
const options = {
const interfaceOptions = {
chart: {
id: 'chart',
group: 'txrx',
},
stroke: {
curve: 'smooth',
@@ -85,7 +141,7 @@ const StatisticsChartList = () => {
},
},
categories,
tickAmount: 20,
tickAmount: areSameDay ? 15 : 10,
},
yaxis: {
labels: {
@@ -105,61 +161,76 @@ const StatisticsChartList = () => {
},
};
const memoryOptions = {
chart: {
id: 'chart',
},
stroke: {
curve: 'smooth',
},
xaxis: {
tickAmount: areSameDay ? 15 : 10,
title: {
text: 'Time',
style: {
fontSize: '15px',
},
},
categories,
},
yaxis: {
tickAmount: 5,
title: {
text: t('statistics.data_mb'),
style: {
fontSize: '15px',
},
},
},
legend: {
position: 'top',
horizontalAlign: 'right',
float: true,
},
};
const newOptions = {
interfaceList,
settings: options,
memory: [memoryUsed],
interfaceOptions,
memoryOptions,
start: new Date(sortedData[0].recorded * 1000).toISOString(),
end: new Date(sortedData[sortedData.length - 1].recorded * 1000).toISOString(),
};
if (statOptions !== newOptions) {
setStatOptions(newOptions);
const sectionOptions = newOptions.interfaceList.map((opt) => ({
value: opt[0].titleName,
label: opt[0].titleName,
}));
setOptions([...sectionOptions, { value: 'memory', label: t('statistics.memory') }]);
setStatOptions({ ...newOptions });
if (sortedData.length > 0) {
setStart(new Date(sortedData[0].recorded * 1000));
setEnd(new Date(sortedData[sortedData.length - 1].recorded * 1000));
}
}
};
const getStatistics = () => {
const getInterface = useCallback(() => {
if (statOptions.interfaceList.length === 0) return <p>N/A</p>;
const interfaceToShow = statOptions.interfaceList.find(
(inter) => inter[0].titleName === section,
);
if (interfaceToShow) {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
params: {
serialNumber: '24f5a207a130',
},
};
axiosInstance
.get(
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics?newest=true&limit=50`,
options,
)
.then((response) => {
transformIntoDataset(response.data.data);
})
.catch(() => {});
};
useEffect(() => {
if (deviceSerialNumber) {
getStatistics();
}
}, [deviceSerialNumber]);
useEffect(() => {
eventBus.on('refreshInterfaceStatistics', () => getStatistics());
return () => {
eventBus.remove('refreshInterfaceStatistics');
};
}, []);
return (
<div>
{statOptions.interfaceList.map((data) => {
const options = {
data,
data: interfaceToShow,
options: {
...statOptions.settings,
...statOptions.interfaceOptions,
title: {
text: capitalizeFirstLetter(data[0].titleName),
text: capitalizeFirstLetter(interfaceToShow[0].titleName),
align: 'left',
style: {
fontSize: '25px',
@@ -172,9 +243,92 @@ const StatisticsChartList = () => {
<DeviceStatisticsChart chart={options} />
</div>
);
}
return <p>N/A</p>;
}, [statOptions, section]);
const getStatistics = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
params: {},
};
let extraParams = '';
if (time.start !== null && time.end !== null) {
const utcStart = new Date(time.start).toISOString();
const utcEnd = new Date(time.end).toISOString();
options.params.startDate = dateToUnix(utcStart);
options.params.endDate = dateToUnix(utcEnd);
options.params.limit = 10000;
} else {
extraParams = '?newest=true&limit=50';
}
axiosInstance
.get(
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics${extraParams}`,
options,
)
.then((response) => {
transformIntoDataset(response.data.data);
})
.catch(() => {})
.finally(() => setLoading(false));
};
useEffect(() => {
if (deviceSerialNumber) {
getStatistics();
}
}, [deviceSerialNumber, time.refreshId]);
if (loading) {
return (
<div className="text-center">
<CSpinner size="xl" />
</div>
);
}
return (
<div>
{section !== 'memory' && !loading && getInterface()}
{section === 'memory' &&
!loading &&
statOptions.memory.map((data) => {
const options = {
data,
options: {
...statOptions.memoryOptions,
title: {
text: capitalizeFirstLetter(data[0].titleName),
align: 'left',
style: {
fontSize: '25px',
},
},
},
};
return (
<div key={createUuid()}>
<DeviceStatisticsChart chart={options} section={section} />
</div>
);
})}
</div>
);
};
StatisticsChartList.propTypes = {
setOptions: PropTypes.func.isRequired,
section: PropTypes.string.isRequired,
time: PropTypes.instanceOf(Object).isRequired,
setStart: PropTypes.func.isRequired,
setEnd: PropTypes.func.isRequired,
};
export default React.memo(StatisticsChartList);

View File

@@ -1,9 +1,18 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CCard, CCardHeader, CCardBody, CPopover, CButton } from '@coreui/react';
import { v4 as createUuid } from 'uuid';
import {
CCard,
CCardHeader,
CCardBody,
CPopover,
CButton,
CSelect,
CFormText,
} from '@coreui/react';
import DatePicker from 'react-widgets/DatePicker';
import { cilSync } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import eventBus from 'utils/eventBus';
import LifetimeStatsmodal from 'components/LifetimeStatsModal';
import StatisticsChartList from './StatisticsChartList';
import LatestStatisticsmodal from './LatestStatisticsModal';
@@ -12,6 +21,13 @@ const DeviceStatisticsCard = () => {
const { t } = useTranslation();
const [showLatestModal, setShowLatestModal] = useState(false);
const [showLifetimeModal, setShowLifetimeModal] = useState(false);
const [options, setOptions] = useState([]);
const [section, setSection] = useState('');
const [start, setStart] = useState(null);
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState(null);
const [endError, setEndError] = useState(false);
const [time, setTime] = useState({ refreshId: '0', start: null, end: null });
const toggleLatestModal = () => {
setShowLatestModal(!showLatestModal);
@@ -21,10 +37,36 @@ const DeviceStatisticsCard = () => {
setShowLifetimeModal(!showLifetimeModal);
};
const refresh = () => {
eventBus.dispatch('refreshInterfaceStatistics', { message: 'Refresh interface statistics' });
const modifyStart = (value) => {
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
} catch (e) {
setEnd('');
setEndError(true);
}
};
const refresh = () => {
setTime({ refreshId: createUuid(), start, end });
};
useEffect(() => {
if (section === '' && options.length > 0) setSection(options[0].value);
}, [options]);
return (
<div>
<CCard className="m-0">
@@ -32,12 +74,34 @@ const DeviceStatisticsCard = () => {
<div className="d-flex flex-row-reverse align-items-center">
<div className="pl-2">
<CPopover content={t('common.refresh')}>
<CButton size="sm" color="info" onClick={refresh}>
<CButton size="sm" color="info" onClick={refresh} disabled={startError || endError}>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
<div className="px-2">
<CButton size="sm" color="info" onClick={toggleLifetimeModal}>
Lifetime Statistics
</CButton>
@@ -47,10 +111,30 @@ const DeviceStatisticsCard = () => {
{t('statistics.show_latest')}
</CButton>
</div>
<div className="pl-2">
<CSelect
custom
value={section}
disabled={options.length === 0}
onChange={(e) => setSection(e.target.value)}
>
{options.map((opt) => (
<option value={opt.value} key={createUuid()}>
{opt.label}
</option>
))}
</CSelect>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<StatisticsChartList />
<StatisticsChartList
setOptions={setOptions}
section={section}
time={time}
setStart={setStart}
setEnd={setEnd}
/>
</CCardBody>
</CCard>
<LatestStatisticsmodal show={showLatestModal} toggle={toggleLatestModal} />

View File

@@ -2,13 +2,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CAlert,
CCard,
CCardHeader,
CRow,
CCol,
CCardBody,
CBadge,
CAlert,
CPopover,
CButton,
CSpinner,
@@ -16,8 +16,7 @@ import {
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSync } from '@coreui/icons';
import { prettyDate, compactSecondsToDetailed } from 'utils/helper';
import MemoryBar from './MemoryBar';
import { prettyDate, compactSecondsToDetailed, cleanBytesString } from 'utils/helper';
import styles from './index.module.scss';
@@ -27,6 +26,20 @@ const errorField = (t) => (
</CAlert>
);
const getMemoryColor = (memTotal, memFree) => {
let memoryUsed = 0;
if (memTotal > 0) memoryUsed = Math.floor(((memTotal - memFree) / memTotal) * 100);
if (memoryUsed < 60) return 'success';
if (memoryUsed <= 85) return 'warning';
return 'danger';
};
const getMemoryPercentage = (memTotal, memFree) => {
if (memTotal <= 0) return `0%`;
return `${Math.floor(((memTotal - memFree) / memTotal) * 100)}%`;
};
const DeviceStatusCard = ({
t,
loading,
@@ -156,6 +169,12 @@ const DeviceStatusCard = ({
</div>
)}
</CCol>
<CCol className="mb-1" md="4" xl="4">
{t('status.total_memory')}:
</CCol>
<CCol className="mb-1" md="8" xl="8" style={{ paddingTop: '5px' }}>
{error ? errorField(t) : cleanBytesString(lastStats?.unit?.memory?.total)}
</CCol>
<CCol className="mb-1" md="4" xl="4">
{t('status.memory')}:
</CCol>
@@ -163,15 +182,19 @@ const DeviceStatusCard = ({
{error ? (
errorField(t)
) : (
<MemoryBar
t={t}
usedBytes={
lastStats?.unit?.memory?.total && lastStats?.unit?.memory?.free
? lastStats?.unit?.memory?.total - lastStats?.unit?.memory?.free
: 0
}
totalBytes={lastStats?.unit?.memory?.total ?? 0}
/>
<CAlert
style={{ width: '40px' }}
className="p-0 text-center"
color={getMemoryColor(
lastStats?.unit?.memory?.total ?? 0,
lastStats?.unit?.memory?.free ?? 0,
)}
>
{getMemoryPercentage(
lastStats?.unit?.memory?.total ?? 0,
lastStats?.unit?.memory?.free ?? 0,
)}
</CAlert>
)}
</CCol>
</CRow>

View File

@@ -124,3 +124,5 @@ export const testRegex = (value, regexString) => {
const regex = new RegExp(regexString);
return regex.test(value);
};
export const datesSameDay = (first, second) => first.getDate() === second.getDate();