UI fixes, standardization

This commit is contained in:
Charles
2021-09-21 11:39:59 -04:00
parent e61475e9ac
commit c978301d23
40 changed files with 687 additions and 927 deletions

View File

@@ -4,6 +4,7 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpack = require('webpack');
const path = require('path');
const paths = require('./paths');
@@ -19,6 +20,9 @@ module.exports = {
preferRelative: true,
},
plugins: [
new webpack.DefinePlugin({
'process.env.VERSION': JSON.stringify(process.env.npm_package_version),
}),
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.1.10",
"version": "2.1.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.1.10",
"version": "2.1.12",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -32,7 +32,7 @@
"react-tooltip": "^4.2.21",
"react-widgets": "^5.1.1",
"sass": "^1.35.1",
"ucentral-libs": "^0.9.17",
"ucentral-libs": "^0.9.18",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -14812,9 +14812,9 @@
}
},
"node_modules/ucentral-libs": {
"version": "0.9.17",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-0.9.17.tgz",
"integrity": "sha512-GQtEaOhdFcBmNeu4NhYiq4yVsycZ7i8DeotubcKN8lt9v/TjAPecle0FsDdiXgXphOblToym6mk2tCAbH/958g==",
"version": "0.9.18",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-0.9.18.tgz",
"integrity": "sha512-znLLUdrRdBCYyufpFk/LWMVx0iajbuMTqayxWJek8JJeiaFlgH58+52s1OKjsLEb6Q4kPOIeUHguN3pJ/EfB1w==",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -27653,9 +27653,9 @@
}
},
"ucentral-libs": {
"version": "0.9.17",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-0.9.17.tgz",
"integrity": "sha512-GQtEaOhdFcBmNeu4NhYiq4yVsycZ7i8DeotubcKN8lt9v/TjAPecle0FsDdiXgXphOblToym6mk2tCAbH/958g==",
"version": "0.9.18",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-0.9.18.tgz",
"integrity": "sha512-znLLUdrRdBCYyufpFk/LWMVx0iajbuMTqayxWJek8JJeiaFlgH58+52s1OKjsLEb6Q4kPOIeUHguN3pJ/EfB1w==",
"requires": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.1.10",
"version": "2.1.12",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -26,7 +26,7 @@
"react-tooltip": "^4.2.21",
"react-widgets": "^5.1.1",
"sass": "^1.35.1",
"ucentral-libs": "^0.9.17",
"ucentral-libs": "^0.9.18",
"uuid": "^8.3.2"
},
"main": "index.js",

View File

@@ -23,10 +23,11 @@
},
"commands": {
"error": "Fehler beim Senden des Befehls!",
"error_delete_log": "Fehler beim Versuch zu löschen: {{error}}",
"event_queue": "Ereigniswarteschlange",
"success": "Befehl wurde erfolgreich übermittelt",
"title": "Gerätebefehle",
"unable_queue": "Anfrage für Ereigniswarteschlange kann nicht abgeschlossen werden"
"unable_queue": "Anfrage für Ereigniswarteschlange kann nicht abgeschlossen werden: {{error}}"
},
"common": {
"access_policy": "Zugangsrichtlinien",
@@ -205,6 +206,9 @@
"title": "Gerät konfigurieren",
"valid_json": "Sie müssen ein gültiges JSON eingeben"
},
"connect": {
"error_trying_to_connect": "Fehler beim Versuch, eine Verbindung zum Gerät herzustellen: {{error}}"
},
"delete_command": {
"explanation": "Möchten Sie diesen Befehl wirklich löschen? Diese Aktion ist nicht umkehrbar.",
"title": "Befehl löschen"
@@ -215,6 +219,10 @@
"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": {
"error_fetching_device": "Fehler beim Abrufen der Geräteinformationen: {{error}}",
"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}"
},
"device_logs": {
"log": "Protokoll",
"severity": "Wichtigkeit",
@@ -486,7 +494,7 @@
"create_success": "Benutzer erfolgreich erstellt",
"creating": "Benutzer erstellen ...",
"delete_avatar": "Avatar löschen",
"delete_failure": "Fehler beim Versuch, den Benutzer zu löschen",
"delete_failure": "Fehler beim Versuch, den Benutzer zu löschen: {{error}}",
"delete_success": "Benutzer erfolgreich gelöscht!",
"delete_title": "Benutzer löschen",
"delete_warning": "Warnung: Sobald Sie einen Benutzer gelöscht haben, können Sie ihn nicht wiederherstellen",
@@ -494,6 +502,7 @@
"description": "Beschreibung",
"edit": "Benutzer bearbeiten",
"email_address": "E-Mail-Addresse",
"error_fetching_users": "Fehler beim Abrufen der Nutzer: {{error}}",
"force_password_change": "Passwortänderung bei der Anmeldung erzwingen",
"id": "Benutzeridentifikation.",
"last_login": "Letzte Anmeldung",
@@ -509,7 +518,7 @@
"provide_password": "Bitte geben Sie ein gültiges Passwort ein",
"save_avatar": "Avatar speichern",
"show_hide_password": "Passwort anzeigen/verbergen",
"update_failure": "Stellen Sie sicher, dass alle Ihre Daten gültig sind. Wenn Sie das Kennwort ändern, stellen Sie sicher, dass es sich nicht um ein altes handelt.",
"update_failure": "Fehler beim Aktualisieren: {{error}}",
"update_failure_title": "Update fehlgeschlagen",
"update_success": "Benutzer erfolgreich aktualisiert",
"update_success_title": "Erfolg",

View File

@@ -23,10 +23,11 @@
},
"commands": {
"error": "Error while submitting command!",
"error_delete_log": "Error while trying to delete: {{error}}",
"event_queue": "Event Queue",
"success": "Command submitted successfully, you can look at the Commands log for the result",
"title": "Command History",
"unable_queue": "Unable to complete event queue request"
"unable_queue": "Unable to complete event queue request: {{error}}"
},
"common": {
"access_policy": "Access Policy",
@@ -205,6 +206,9 @@
"title": "Configure",
"valid_json": "You need to enter valid JSON"
},
"connect": {
"error_trying_to_connect": "Error while trying to connect to device: {{error}}"
},
"delete_command": {
"explanation": "Are you sure you want to delete this command? This action is not reversible.",
"title": "Delete Command"
@@ -215,6 +219,10 @@
"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": {
"error_fetching_device": "Error fetching device information: {{error}}",
"error_fetching_devices": "Error while fetching devices: {{error}}"
},
"device_logs": {
"log": "Log",
"severity": "Severity",
@@ -486,7 +494,7 @@
"create_success": "User Created Successfully",
"creating": "Creating User...",
"delete_avatar": "Delete Avatar",
"delete_failure": "Error while trying to delete user",
"delete_failure": "Error while trying to delete user: {{error}}",
"delete_success": "User successfully deleted!",
"delete_title": "Delete User",
"delete_warning": "Warning: Once you delete a user you cannot revert",
@@ -494,6 +502,7 @@
"description": "Description",
"edit": "Edit User",
"email_address": "Email Address",
"error_fetching_users": "Error fetching users: {{error}}",
"force_password_change": "Force Password Change on Login",
"id": "User Id.",
"last_login": "Last Login",
@@ -509,7 +518,7 @@
"provide_password": "Please provide a valid password",
"save_avatar": "Save Avatar",
"show_hide_password": "Show/Hide Password",
"update_failure": "Make sure all of your data is valid. If you are modifying the password, make sure it is not an old one.",
"update_failure": "Error while trying to update: {{error}}",
"update_failure_title": "Update Failed",
"update_success": "User Updated Successfully",
"update_success_title": "Success",

View File

@@ -23,10 +23,11 @@
},
"commands": {
"error": "¡Error al enviar el comando!",
"error_delete_log": "Error al intentar eliminar: {{error}}",
"event_queue": "Cola de eventos",
"success": "Comando enviado con éxito, puede consultar el registro de Comandos para ver el resultado",
"title": "Historial de Comandos",
"unable_queue": "No se pudo completar la solicitud de cola de eventos"
"unable_queue": "No se pudo completar la solicitud de cola de eventos: {{error}}"
},
"common": {
"access_policy": "Política de acceso",
@@ -205,6 +206,9 @@
"title": "Configurar",
"valid_json": "Debes ingresar un JSON válido"
},
"connect": {
"error_trying_to_connect": "Error al intentar conectarse al dispositivo: {{error}}"
},
"delete_command": {
"explanation": "¿Está seguro de que desea eliminar este comando? Esta acción no es reversible.",
"title": "Eliminar comando"
@@ -215,6 +219,10 @@
"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": {
"error_fetching_device": "Error al obtener la información del dispositivo: {{error}}",
"error_fetching_devices": "Error al recuperar dispositivos: {{error}}"
},
"device_logs": {
"log": "Iniciar sesión",
"severity": "Gravedad",
@@ -486,7 +494,7 @@
"create_success": "Usuario creado con éxito",
"creating": "Creando usuario ...",
"delete_avatar": "Eliminar avatar",
"delete_failure": "Error al intentar eliminar al usuario",
"delete_failure": "Error al intentar eliminar al usuario: {{error}}",
"delete_success": "¡Usuario eliminado correctamente!",
"delete_title": "Borrar usuario",
"delete_warning": "Advertencia: una vez que elimina un usuario, no puede revertir",
@@ -494,6 +502,7 @@
"description": "Descripción",
"edit": "editar usuario",
"email_address": "Dirección de correo electrónico",
"error_fetching_users": "Error al obtener usuarios: {{error}}",
"force_password_change": "Forzar cambio de contraseña al iniciar sesión",
"id": "Id. De usuario",
"last_login": "Último acceso",
@@ -509,7 +518,7 @@
"provide_password": "Proporcione una contraseña válida",
"save_avatar": "Guardar avatar",
"show_hide_password": "Mostrar / Ocultar contraseña",
"update_failure": "Asegúrese de que todos sus datos sean válidos. Si está modificando la contraseña, asegúrese de que no sea antigua.",
"update_failure": "Error al intentar actualizar: {{error}}",
"update_failure_title": "Actualización fallida",
"update_success": "Usuario actualizado con éxito",
"update_success_title": "Éxito",

View File

@@ -23,10 +23,11 @@
},
"commands": {
"error": "Erreur lors de la soumission de la commande !",
"error_delete_log": "Erreur lors de la tentative de suppression : {{error}}",
"event_queue": "File d'attente d'événements",
"success": "Commande soumise avec succès, vous pouvez consulter le journal des commandes pour le résultat",
"title": "Historique des commandes",
"unable_queue": "Impossible de terminer la demande de file d'attente d'événements"
"unable_queue": "Impossible de terminer la demande de file d'attente d'événements: {{error}}"
},
"common": {
"access_policy": "Politique d'accès",
@@ -205,6 +206,9 @@
"title": "Configurer",
"valid_json": "Vous devez entrer un JSON valide"
},
"connect": {
"error_trying_to_connect": "Erreur lors de la tentative de connexion à l'appareil : {{error}}"
},
"delete_command": {
"explanation": "Êtes-vous sûr de vouloir supprimer cette commande ? Cette action n'est pas réversible.",
"title": "Supprimer la commande"
@@ -215,6 +219,10 @@
"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": {
"error_fetching_device": "Erreur lors de la récupération des informations sur l'appareil : {{error}}",
"error_fetching_devices": "Erreur lors de la récupération des appareils : {{error}}"
},
"device_logs": {
"log": "Bûche",
"severity": "Gravité",
@@ -486,7 +494,7 @@
"create_success": "L'utilisateur a été créé avec succès",
"creating": "Création de l'utilisateur...",
"delete_avatar": "Supprimer l'avatar",
"delete_failure": "Erreur lors de la tentative de suppression de l'utilisateur",
"delete_failure": "Erreur lors de la tentative de suppression de l'utilisateur: {{error}}",
"delete_success": "Utilisateur supprimé avec succès !",
"delete_title": "Supprimer l'utilisateur",
"delete_warning": "Avertissement : Une fois que vous avez supprimé un utilisateur, vous ne pouvez plus revenir en arrière",
@@ -494,6 +502,7 @@
"description": "La description",
"edit": "Modifier l'utilisateur",
"email_address": "Adresse électronique",
"error_fetching_users": "Erreur lors de la récupération des utilisateurs : {{error}}",
"force_password_change": "Forcer le changement de mot de passe lors de la connexion",
"id": "Identifiant d'utilisateur.",
"last_login": "Dernière connexion",
@@ -509,7 +518,7 @@
"provide_password": "Veuillez fournir un mot de passe valide",
"save_avatar": "Enregistrer l'avatar",
"show_hide_password": "Afficher/Masquer le mot de passe",
"update_failure": "Assurez-vous que toutes vos données sont valides. Si vous modifiez le mot de passe, assurez-vous qu'il ne s'agit pas d'un ancien.",
"update_failure": "Erreur lors de la tentative de mise à jour : {{error}}",
"update_failure_title": "mise à jour a échoué",
"update_success": "L'utilisateur a bien été mis à jour",
"update_success_title": "Succès",

View File

@@ -23,10 +23,11 @@
},
"commands": {
"error": "Erro ao enviar comando!",
"error_delete_log": "Erro ao tentar excluir: {{error}}",
"event_queue": "Fila de Eventos",
"success": "Comando enviado com sucesso, você pode consultar o log de Comandos para ver o resultado",
"title": "Histórico de Comandos",
"unable_queue": "Incapaz de completar o pedido de fila de eventos"
"unable_queue": "Incapaz de completar o pedido de fila de eventos: {{error}}"
},
"common": {
"access_policy": "Política de Acesso",
@@ -205,6 +206,9 @@
"title": "Configurar",
"valid_json": "Você precisa inserir um JSON válido"
},
"connect": {
"error_trying_to_connect": "Erro ao tentar conectar ao dispositivo: {{error}}"
},
"delete_command": {
"explanation": "Tem certeza de que deseja excluir este comando? esta ação não é reversível.",
"title": "Apagar Comando"
@@ -215,6 +219,10 @@
"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": {
"error_fetching_device": "Erro ao buscar informações do dispositivo: {{error}}",
"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}"
},
"device_logs": {
"log": "Registro",
"severity": "Gravidade",
@@ -486,7 +494,7 @@
"create_success": "Usuário criado com sucesso",
"creating": "Criando usuário ...",
"delete_avatar": "Apagar Avatar",
"delete_failure": "Erro ao tentar excluir usuário",
"delete_failure": "Erro ao tentar excluir usuário: {{error}}",
"delete_success": "Usuário excluído com sucesso!",
"delete_title": "Deletar usuário",
"delete_warning": "Aviso: depois de excluir um usuário, você não pode reverter",
@@ -494,6 +502,7 @@
"description": "Descrição",
"edit": "Editar usuário",
"email_address": "Endereço de e-mail",
"error_fetching_users": "Erro ao buscar usuários: {{error}}",
"force_password_change": "Forçar mudança de senha no login",
"id": "ID do usuário.",
"last_login": "Último login",
@@ -509,7 +518,7 @@
"provide_password": "Forneça uma senha válida",
"save_avatar": "Salvar Avatar",
"show_hide_password": "Mostrar / ocultar senha",
"update_failure": "Certifique-se de que todos os seus dados são válidos. Se você estiver modificando a senha, certifique-se de que não seja uma senha antiga.",
"update_failure": "Erro ao tentar atualizar: {{error}}",
"update_failure_title": "Atualização falhou",
"update_success": "Usuário atualizado com sucesso",
"update_success_title": "Sucesso",

View File

@@ -11,7 +11,10 @@ import {
CFormGroup,
CInputRadio,
CLabel,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
@@ -88,8 +91,15 @@ const BlinkModal = ({ show, toggleModal }) => {
return (
<CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('blink.device_leds')}</CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('blink.device_leds')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
{result === 'success' ? (
<SuccessfulActionModalBody toggleModal={toggleModal} />

View File

@@ -1,27 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CButton,
CModal,
CModalHeader,
CModalBody,
CModalTitle,
CModalFooter,
} from '@coreui/react';
import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
const DetailsModal = ({ t, show, toggle, details, commandUuid }) => (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalHeader className="p-1">
<CModalTitle className="text-dark">{commandUuid}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<pre className="ignore">{JSON.stringify(details, null, 4)}</pre>
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal>
);

View File

@@ -5,11 +5,11 @@ import {
CWidgetDropdown,
CRow,
CCol,
CCollapse,
CButton,
CDataTable,
CCard,
CPopover,
CButtonToolbar,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import DatePicker from 'react-widgets/DatePicker';
@@ -21,7 +21,6 @@ import ConfirmModal from 'components/ConfirmModal';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import WifiScanResultModalWidget from 'components/WifiScanResultModal';
import DetailsModal from './DetailsModal';
import styles from './index.module.scss';
const DeviceCommands = () => {
const { t } = useTranslation();
@@ -38,8 +37,6 @@ const DeviceCommands = () => {
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [detailsUuid, setDetailsUuid] = useState('');
const [modalDetails, setModalDetails] = useState({});
// Main collapsible
const [collapse, setCollapse] = useState(false);
// General states
const [commands, setCommands] = useState([]);
const [loading, setLoading] = useState(false);
@@ -50,11 +47,6 @@ const DeviceCommands = () => {
const [loadingMore, setLoadingMore] = useState(false);
const [showLoadingMore, setShowLoadingMore] = useState(true);
const toggle = (e) => {
setCollapse(!collapse);
e.preventDefault();
};
const toggleScanModal = () => {
setShowScanModal(!showScanModal);
};
@@ -196,17 +188,15 @@ const DeviceCommands = () => {
};
const columns = [
{ key: 'UUID', label: t('common.id'), _style: { width: '28%' } },
{ key: 'command', label: t('common.command'), _style: { width: '10%' } },
{ key: 'completed', label: t('common.completed'), filter: false, _style: { width: '16%' } },
{ key: 'submitted', label: t('common.submitted'), filter: false, _style: { width: '16%' } },
{ key: 'executed', label: t('common.executed'), filter: false, _style: { width: '16%' } },
{ key: 'command', label: t('common.command'), _style: { width: '15%' } },
{ key: 'completed', label: t('common.completed'), filter: false, _style: { width: '20%' } },
{ key: 'submitted', label: t('common.submitted'), filter: false, _style: { width: '20%' } },
{
key: 'show_buttons',
label: '',
sorter: false,
filter: false,
_style: { width: '14%' },
_style: { width: '1%' },
},
];
@@ -252,23 +242,13 @@ const DeviceCommands = () => {
}, [commands]);
return (
<CWidgetDropdown
inverse="true"
color="gradient-primary"
header={t('commands.title')}
footerSlot={
<div className={styles.footer}>
<CCollapse show={collapse}>
<CRow>
<CCol />
<CCol className="text-right">
<div>
<CButton onClick={refreshCommands} size="sm">
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
</CButton>
</div>
</CCol>
</CRow>
<div>
<CWidgetDropdown
inverse="true"
color="gradient-primary"
header={t('commands.title')}
footerSlot={
<div className="pb-1 px-3">
<CRow className="mb-2">
<CCol>
From:
@@ -280,8 +260,9 @@ const DeviceCommands = () => {
</CCol>
</CRow>
<CCard>
<div className={['overflow-auto', styles.scrollableBox].join(' ')}>
<div className="overflow-auto" style={{ height: '200px' }}>
<CDataTable
border
loading={loading}
items={commands ?? []}
fields={columns}
@@ -302,98 +283,100 @@ const DeviceCommands = () => {
: 'Pending'}
</td>
),
executed: (item) => (
<td>
{item.executed && item.executed !== ''
? prettyDate(item.executed)
: 'Pending'}
</td>
),
show_buttons: (item, index) => (
<td>
<CRow>
<CCol>
<CPopover
content={
item.command === 'trace' ? t('common.download') : t('common.result')
}
<CButtonToolbar
role="group"
className="justify-content-flex-end"
style={{ width: '170px' }}
>
<CPopover
content={
item.command === 'trace' ? t('common.download') : t('common.result')
}
>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleDetails(item);
}}
>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
onClick={() => {
toggleDetails(item);
}}
>
{item.command === 'trace' ? (
<CIcon content={cilCloudDownload} size="lg" />
) : (
<CIcon content={cilCalendarCheck} size="lg" />
)}
</CButton>
</CPopover>
</CCol>
<CCol>
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
onClick={() => {
toggleResponse(item);
}}
>
<CIcon name="cilList" size="lg" />
</CButton>
</CPopover>
</CCol>
<CCol>
<CPopover content={t('common.delete')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
onClick={() => {
toggleConfirmModal(item.UUID, index);
}}
>
<CIcon name="cilTrash" size="lg" />
</CButton>
</CPopover>
</CCol>
</CRow>
{item.command === 'trace' ? (
<CIcon
name="cil-cloud-download"
content={cilCloudDownload}
size="lg"
/>
) : (
<CIcon
name="cil-calendar-check"
content={cilCalendarCheck}
size="lg"
/>
)}
</CButton>
</CPopover>
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleResponse(item);
}}
>
<CIcon name="cilList" size="lg" />
</CButton>
</CPopover>
<CPopover content={t('common.delete')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => {
toggleConfirmModal(item.UUID, index);
}}
>
<CIcon name="cilTrash" size="lg" />
</CButton>
</CPopover>
</CButtonToolbar>
</td>
),
}}
/>
<CRow className={styles.loadMoreSpacing}>
{showLoadingMore && (
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label="View More"
isLoadingLabel="Loading More..."
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreCommands}
variant="outline"
/>
)}
</CRow>
</div>
)}
</div>
</CCard>
</CCollapse>
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className="text-white"
size="lg"
/>
</div>
}
>
<div className="text-right float-right">
<CButton onClick={refreshCommands} size="sm">
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
</CButton>
</div>
}
>
</CWidgetDropdown>
<WifiScanResultModalWidget
show={showScanModal}
toggle={toggleScanModal}
@@ -408,7 +391,7 @@ const DeviceCommands = () => {
details={modalDetails}
commandUuid={detailsUuid}
/>
</CWidgetDropdown>
</div>
);
};

View File

@@ -1,15 +0,0 @@
.footer {
padding: 20px;
}
.scrollableBox {
height: 200px;
}
.customIconHeight {
height: 19px;
}
.loadMoreSpacing {
margin-bottom: 1%;
}

View File

@@ -12,7 +12,10 @@ import {
CTextarea,
CInvalidFeedback,
CInputFile,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
@@ -125,8 +128,15 @@ const ConfigureModal = ({ show, toggleModal }) => {
return (
<CModal show={show} onClose={toggleModal} size="lg">
<CModalHeader closeButton>
<CModalTitle>{t('configure.title')}</CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('configure.title')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
{hadSuccess ? (
<SuccessfulActionModalBody toggleModal={toggleModal} />

View File

@@ -9,7 +9,10 @@ import {
CModalFooter,
CSpinner,
CBadge,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import PropTypes from 'prop-types';
const ConfirmModal = ({ show, toggle, action }) => {
@@ -63,8 +66,15 @@ const ConfirmModal = ({ show, toggle, action }) => {
return (
<CModal className="text-dark" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle>{t('delete_command.title')}</CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('delete_command.title')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<h6>{t('delete_command.explanation')}</h6>
@@ -73,9 +83,6 @@ const ConfirmModal = ({ show, toggle, action }) => {
<CButton disabled={loading} color="primary" onClick={() => doAction()}>
{getButtonContent()}
</CButton>
<CButton color="secondary" onClick={toggle}>
{t('common.cancel')}
</CButton>
</CModalFooter>
</CModal>
);

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { CModal, CModalHeader, CModalBody } from '@coreui/react';
import { CModal, CModalHeader, CModalBody, CModalTitle, CPopover, CButton } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSave, cilX } from '@coreui/icons';
import { CreateUserForm, useFormFields, useAuth, useToast } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { testRegex, validateEmail } from 'utils/helper';
@@ -143,14 +145,26 @@ const CreateUserModal = ({ show, toggle, getUsers }) => {
return (
<CModal show={show} onClose={toggle} size="xl">
<CModalHeader>{t('user.create')}</CModalHeader>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('user.create')}</CModalTitle>
<div className="text-right">
<CPopover content={t('user.create')}>
<CButton color="primary" variant="outline" onClick={createUser} disabled={loading}>
<CIcon content={cilSave} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<CreateUserForm
t={t}
fields={formFields}
updateField={updateFieldWithId}
createUser={createUser}
loading={loading}
policies={policies}
toggleChange={toggleChange}
/>

View File

@@ -1,9 +1,20 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CModal, CModalHeader, CModalTitle, CModalBody, CCol, CRow } from '@coreui/react';
import {
CModal,
CModalHeader,
CModalTitle,
CModalBody,
CCol,
CRow,
CPopover,
CButton,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import { ConfirmFooter, useAuth, useDevice } from 'ucentral-libs';
import { ConfirmFooter, useAuth, useDevice, useToast } from 'ucentral-libs';
import { dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
@@ -11,6 +22,7 @@ import eventBus from 'utils/eventBus';
const DeleteLogModal = ({ show, toggle, object }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const { deviceSerialNumber } = useDevice();
const [loading, setLoading] = useState(false);
const [maxDate, setMaxDate] = useState(new Date().toString());
@@ -36,7 +48,14 @@ const DeleteLogModal = ({ show, toggle, object }) => {
return axiosInstance
.delete(`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/${object}`, options)
.then(() => {})
.catch(() => {})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('commands.error_delete_log', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
if (object === 'healthchecks')
eventBus.dispatch('deletedHealth', { message: 'Healthcheck was deleted' });
@@ -54,12 +73,19 @@ const DeleteLogModal = ({ show, toggle, object }) => {
return (
<CModal className="text-dark" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">
{object === 'healthchecks'
? t('delete_logs.healthchecks_title')
: t('delete_logs.device_logs_title')}
</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<h6>{t('delete_logs.explanation', { object })}</h6>

View File

@@ -70,7 +70,14 @@ const DeviceActions = () => {
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (newWindow) newWindow.opener = null;
})
.catch(() => {})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('connect.error_trying_to_connect', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setConnectLoading(false);
});
@@ -131,7 +138,7 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className="mt-4">
<CCol>
<CButton block color="primary" onClick={toggleUpgradeModal}>
{t('actions.firmware_upgrade')}
@@ -143,7 +150,7 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className="mt-4">
<CCol>
<CButton block color="primary" onClick={toggleScanModal}>
{t('actions.wifi_scan')}
@@ -155,7 +162,7 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className="mt-4">
<CCol>
<LoadingButton
isLoading={connectLoading}
@@ -170,7 +177,7 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className="mt-4">
<CCol>
<CButton block color="primary" onClick={toggleQueueModal}>
{t('commands.event_queue')}

View File

@@ -22,6 +22,7 @@ import {
NotesTable,
useAuth,
useDevice,
useToast,
} from 'ucentral-libs';
import DeviceConfigurationModal from './DeviceConfigurationModal';
@@ -29,6 +30,7 @@ const DeviceConfiguration = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [collapse, setCollapse] = useState(false);
@@ -61,7 +63,14 @@ const DeviceConfiguration = () => {
.then((response) => {
setDevice(response.data);
})
.catch(() => {});
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
});
};
const saveNote = (currentNote) => {
@@ -153,46 +162,46 @@ const DeviceConfiguration = () => {
{device.firmware}
</CCol>
</CRow>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('configuration.last_configuration_change')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{prettyDate(device.lastConfigurationChange)}
</CCol>
</CRow>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('common.mac')} :</CLabel>
</CCol>
<CCol xs="12" md="9">
{device.macAddress}
</CCol>
</CRow>
<CRow className="mt-2 mb-4">
<CCol md="3">
<CLabel className="align-middle">{t('configuration.device_password')} : </CLabel>
</CCol>
<CCol xs="12" md="2">
{getPassword()}
</CCol>
<CCol md="7">
<HideTextButton t={t} toggle={toggleShowPassword} show={showPassword} />
<CopyToClipboardButton
t={t}
size="sm"
content={device?.devicePassword === '' ? 'openwifi' : device.devicePassword}
/>
</CCol>
</CRow>
<NotesTable
t={t}
notes={device.notes}
loading={loading}
addNote={saveNote}
descriptionColumn={false}
/>
<CCollapse show={collapse}>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('configuration.last_configuration_change')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{prettyDate(device.lastConfigurationChange)}
</CCol>
</CRow>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('common.mac')} :</CLabel>
</CCol>
<CCol xs="12" md="9">
{device.macAddress}
</CCol>
</CRow>
<CRow className="mt-2 mb-4">
<CCol md="3">
<CLabel className="align-middle">{t('configuration.device_password')} : </CLabel>
</CCol>
<CCol xs="12" md="2">
{getPassword()}
</CCol>
<CCol md="7">
<HideTextButton t={t} toggle={toggleShowPassword} show={showPassword} />
<CopyToClipboardButton
t={t}
size="sm"
content={device?.devicePassword === '' ? 'openwifi' : device.devicePassword}
/>
</CCol>
</CRow>
<NotesTable
t={t}
notes={device.notes}
loading={loading}
addNote={saveNote}
descriptionColumn={false}
/>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('configuration.last_configuration_download')} : </CLabel>

View File

@@ -13,6 +13,7 @@ import {
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilTrash } from '@coreui/icons';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import { prettyDate, dateToUnix } from 'utils/helper';
@@ -25,7 +26,6 @@ const DeviceHealth = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const [collapse, setCollapse] = useState(false);
const [details, setDetails] = useState([]);
const [loading, setLoading] = useState(false);
const [healthChecks, setHealthChecks] = useState([]);
@@ -42,11 +42,6 @@ const DeviceHealth = () => {
setShowDeleteModal(!showDeleteModal);
};
const toggle = (e) => {
setCollapse(!collapse);
e.preventDefault();
};
const modifyStart = (value) => {
setStart(value);
};
@@ -195,96 +190,71 @@ const DeviceHealth = () => {
color={barColor}
inverse="true"
footerSlot={
<div className="p-4">
<div className="pb-1 px-3">
<CProgress className="mb-3" color="white" value={sanityLevel ?? 0} />
<CCollapse show={collapse}>
<div className="text-right">
<CPopover content={t('common.delete')}>
<CButton
color="light"
shape="square"
size="sm"
onClick={() => {
toggleDeleteModal();
}}
>
<CIcon name="cilTrash" size="lg" />
</CButton>
</CPopover>
<CRow className="mb-3">
<CCol>
{t('common.from')}
:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
</CCol>
<CCol>
{t('common.to')}
:
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard className="p-0">
<div className="overflow-auto" style={{ height: '200px' }}>
<CDataTable
border
items={healthChecks ?? []}
fields={columns}
className="text-white"
loading={loading}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
UUID: (item) => <td className="align-middle">{item.UUID}</td>,
recorded: (item) => <td className="align-middle">{prettyDate(item.recorded)}</td>,
sanity: (item) => <td className="align-middle">{`${item.sanity}%`}</td>,
show_details: (item, index) => (
<td className="align-middle">
<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.values)}</div>
</CCardBody>
</CCollapse>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</div>
<CRow className="mb-3">
<CCol>
{t('common.from')}
:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
</CCol>
<CCol>
{t('common.to')}
:
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard className="p-0">
<div className="overflow-auto" style={{ height: '250px' }}>
<CDataTable
border
items={healthChecks ?? []}
fields={columns}
className="text-white"
loading={loading}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
UUID: (item) => <td className="align-middle">{item.UUID}</td>,
recorded: (item) => (
<td className="align-middle">{prettyDate(item.recorded)}</td>
),
sanity: (item) => <td className="align-middle">{`${item.sanity}%`}</td>,
show_details: (item, index) => (
<td className="align-middle">
<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.values)}</div>
</CCardBody>
</CCollapse>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</div>
</CCard>
</CCollapse>
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className="text-white"
size="lg"
/>
</CButton>
</CCard>
<DeleteLogModal
serialNumber={deviceSerialNumber}
object="healthchecks"
@@ -293,7 +263,15 @@ const DeviceHealth = () => {
/>
</div>
}
/>
>
<div className="text-right float-right">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm">
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
</CButton>
</CPopover>
</div>
</CWidgetDropdown>
);
};

View File

@@ -99,7 +99,13 @@ const DeviceList = () => {
setDevices(fullDevices);
setLoading(false);
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
@@ -130,7 +136,13 @@ const DeviceList = () => {
}
getDeviceInformation(selectedPage);
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
@@ -179,7 +191,13 @@ const DeviceList = () => {
setDevices(newList);
setLoading(false);
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
@@ -282,10 +300,10 @@ const DeviceList = () => {
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (newWindow) newWindow.opener = null;
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('common.unable_to_connect'),
body: t('connect.error_trying_to_connect', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});

View File

@@ -12,6 +12,7 @@ import {
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilTrash } from '@coreui/icons';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import { prettyDate, dateToUnix } from 'utils/helper';
@@ -24,7 +25,6 @@ const DeviceLogs = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const [collapse, setCollapse] = useState(false);
const [details, setDetails] = useState([]);
const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState([]);
@@ -39,11 +39,6 @@ const DeviceLogs = () => {
setShowDeleteModal(!showDeleteModal);
};
const toggle = (e) => {
setCollapse(!collapse);
e.preventDefault();
};
const modifyStart = (value) => {
setStart(value);
};
@@ -176,91 +171,76 @@ const DeviceLogs = () => {
color="gradient-info"
header={t('device_logs.title')}
footerSlot={
<div className="p-4">
<CCollapse show={collapse}>
<div className="text-right">
<CPopover content={t('common.delete')}>
<CButton
color="light"
shape="square"
size="sm"
onClick={() => {
toggleDeleteModal();
}}
>
<CIcon name="cilTrash" size="lg" />
</CButton>
</CPopover>
<div className="pb-1 px-3">
<CRow className="mb-3">
<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="overflow-auto" style={{ height: '250px' }}>
<CDataTable
items={logs ?? []}
fields={columns}
loading={loading}
className="text-white"
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>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</div>
<CRow className="mb-3">
<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="overflow-auto" style={{ height: '250px' }}>
<CDataTable
items={logs ?? []}
fields={columns}
loading={loading}
className="text-white"
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>
),
}}
/>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
isLoading={loadingMore}
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</div>
</CCard>
</CCollapse>
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className="text-white"
size="lg"
/>
</CButton>
</CCard>
</div>
}
/>
>
<div className="text-right float-right">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm">
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
</CButton>
</CPopover>
</div>
</CWidgetDropdown>
<DeleteLogModal
serialNumber={deviceSerialNumber}
object="logs"

View File

@@ -1,17 +1,42 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { DeviceStatusCard as Card, useDevice, useAuth } from 'ucentral-libs';
import { DeviceStatusCard as Card, useDevice, useAuth, useToast } from 'ucentral-libs';
const DeviceStatusCard = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [lastStats, setLastStats] = useState(null);
const [status, setStatus] = useState(null);
const [deviceConfig, setDeviceConfig] = useState(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const getDevice = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}`, options)
.then((response) => {
setDeviceConfig(response.data);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
});
};
const getData = () => {
setLoading(true);
const options = {
@@ -45,9 +70,17 @@ const DeviceStatusCard = () => {
});
};
const refresh = () => {
getData();
getDevice();
};
useEffect(() => {
setError(false);
if (deviceSerialNumber) getData();
if (deviceSerialNumber) {
getDevice();
getData();
}
}, [deviceSerialNumber]);
return (
@@ -56,7 +89,8 @@ const DeviceStatusCard = () => {
loading={loading}
error={error}
deviceSerialNumber={deviceSerialNumber}
getData={getData}
getData={refresh}
deviceConfig={deviceConfig}
status={status}
lastStats={lastStats}
/>

View File

@@ -145,10 +145,10 @@ const EditUserModal = ({ show, toggle, userId, getUsers }) => {
getUsers();
toggle();
})
.catch(() => {
.catch((e) => {
addToast({
title: t('user.update_failure_title'),
body: t('user.update_failure'),
body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});

View File

@@ -32,10 +32,10 @@ const EventQueueModal = ({ show, toggle }) => {
.then((response) => {
setResult(response.data);
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('commands.unable_queue'),
body: t('commands.unable_queue', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});

View File

@@ -10,7 +10,10 @@ import {
CForm,
CSwitch,
CAlert,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
@@ -84,8 +87,15 @@ const ConfigureModal = ({ show, toggleModal }) => {
return (
<CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('factory_reset.title')}</CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('factory_reset.title')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
{hadSuccess ? (
<SuccessfulActionModalBody toggleModal={toggleModal} />

View File

@@ -46,7 +46,7 @@ const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
return (
<CModal size="xl" show={show} onClose={toggle} scrollable>
<CModalHeader closeButton>
<CModalTitle>
<CModalTitle className="pl-1 pt-1">
#{serialNumber} {t('firmware.history_title')}
</CModalTitle>
</CModalHeader>

View File

@@ -1,74 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { CButton, CSpinner, CModalFooter } from '@coreui/react';
const UpgradeFooter = ({
isNow,
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}
>
{isNow ? t('upgrade.upgrade') : t('common.schedule')}
</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>
);
};
UpgradeFooter.propTypes = {
isNow: PropTypes.bool.isRequired,
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,
};
UpgradeFooter.defaultProps = {
color: 'primary',
variant: '',
block: false,
};
export default UpgradeFooter;

View File

@@ -1,99 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { CModalBody } from '@coreui/react';
import { v4 as createUuid } from 'uuid';
import { useAuth } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
const UpgradeWaitingBody = ({ serialNumber }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [currentStep, setCurrentStep] = useState(0);
const [secondsElapsed, setSecondsElapsed] = useState(0);
const [labelsToShow, setLabelsToShow] = useState(['upgrade.command_submitted']);
const getDeviceConnection = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/device/${encodeURIComponent(serialNumber)}/status`, options)
.then((response) => response.data.connected)
.catch(() => {});
};
const getFirmwareVersion = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/device/${encodeURIComponent(serialNumber)}`, options)
.then((response) => response.data.firmware)
.catch(() => {});
};
const refreshStep = () => {
if (currentStep === 0 && !getDeviceConnection) {
const labelsToAdd = [
t('upgrade.device_disconnected'),
t('upgrade.device_upgrading_firmware'),
t('upgrade.waiting_for_device'),
];
setLabelsToShow([...labelsToShow, ...labelsToAdd]);
setCurrentStep(1);
} else if (currentStep === 1 && getDeviceConnection()) {
const newFirmware = `: ${getFirmwareVersion()}`;
const labelsToAdd = [
t('upgrade.device_reconnected'),
`${t('upgrade.new_version')}: ${newFirmware}`,
];
setLabelsToShow([...labelsToShow, ...labelsToAdd]);
setCurrentStep(2);
}
};
useEffect(() => {
const refreshIntervalId = setInterval(() => {
refreshStep();
}, 5000);
const timerIntervalId = setInterval(() => {
setSecondsElapsed(secondsElapsed + 1);
}, 1000);
return () => {
clearInterval(refreshIntervalId);
clearInterval(timerIntervalId);
};
}, []);
return (
<CModalBody>
<div className="consoleBox">
{labelsToShow.map((label) => (
<p key={createUuid()}>
{new Date().toString()}:{label}
</p>
))}
<p>
{t('common.seconds_elapsed')}:{secondsElapsed}
</p>
</div>
</CModalBody>
);
};
UpgradeWaitingBody.propTypes = {
serialNumber: PropTypes.string.isRequired,
};
export default UpgradeWaitingBody;

View File

@@ -1,241 +0,0 @@
import {
CButton,
CModal,
CModalHeader,
CModalTitle,
CModalBody,
CSwitch,
CCol,
CRow,
CInput,
CInvalidFeedback,
CModalFooter,
} from '@coreui/react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import { dateToUnix } from 'utils/helper';
import 'react-widgets/styles.css';
import { useDevice, useAuth } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import ButtonFooter from './UpgradeFooter';
import UpgradeWaitingBody from './UpgradeWaitingBody';
const FirmwareUpgradeModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber, getDeviceConnection } = useDevice();
const [isNow, setIsNow] = useState(true);
const [waitForUpgrade, setWaitForUpgrade] = useState(false);
const [date, setDate] = useState(new Date().toString());
const [firmware, setFirmware] = useState('');
const [validFirmware, setValidFirmware] = useState(true);
const [validDate, setValidDate] = useState(true);
const [blockFields, setBlockFields] = useState(false);
const [disabledWaiting, setDisableWaiting] = useState(false);
const [waitingForUpgrade, setWaitingForUpgrade] = useState(false);
const [showWaitingConsole, setShowWaitingConsole] = useState(false);
const [deviceConnected, setDeviceConnected] = useState(true);
const toggleNow = () => {
if (isNow) {
setWaitForUpgrade(false);
setDisableWaiting(true);
} else {
setDisableWaiting(false);
}
setIsNow(!isNow);
};
const toggleWaitForUpgrade = () => {
setWaitForUpgrade(waitForUpgrade);
};
const formValidation = () => {
let valid = true;
if (firmware.trim() === '') {
setValidFirmware(false);
valid = false;
}
if (!isNow && date.trim() === '') {
setValidDate(false);
valid = false;
}
return valid;
};
useEffect(() => {
setBlockFields(false);
setShowWaitingConsole(false);
}, [show]);
useEffect(() => {
setValidFirmware(true);
setValidDate(true);
}, [firmware, date]);
useEffect(() => {
if (deviceSerialNumber !== null && show) {
const asyncGet = async () => {
const isConnected = await getDeviceConnection(
deviceSerialNumber,
currentToken,
endpoints.owgw,
);
setDisableWaiting(!isConnected);
setDeviceConnected(isConnected);
};
asyncGet();
}
}, [show]);
const postUpgrade = () => {
if (formValidation()) {
setWaitingForUpgrade(true);
setBlockFields(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
serialNumber: deviceSerialNumber,
};
const parameters = {
serialNumber: deviceSerialNumber,
when: isNow ? 0 : dateToUnix(date),
uri: firmware,
};
axiosInstance
.post(
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}/upgrade`,
parameters,
{ headers },
)
.then(() => {
if (waitForUpgrade) {
setShowWaitingConsole(true);
}
})
.catch(() => {})
.finally(() => {
setBlockFields(false);
setWaitingForUpgrade(false);
eventBus.dispatch('actionCompleted', { message: 'An action has been completed' });
});
}
};
if (showWaitingConsole) {
return (
<CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('upgrade.title')}</CModalTitle>
</CModalHeader>
<CModalBody>
<UpgradeWaitingBody serialNumber={deviceSerialNumber} />
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggleModal}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal>
);
}
return (
<CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('upgrade.title')}</CModalTitle>
</CModalHeader>
<CModalBody>
<h6>{t('upgrade.directions')}</h6>
<CRow className="mt-3">
<CCol md="4" className="mt-2">
<p>{t('upgrade.firmware_uri')}</p>
</CCol>
<CCol md="8">
<CInput
disabled={blockFields}
className={`form-control ${!validFirmware ? 'is-invalid' : ''}`}
type="text"
id="uri"
name="uri-input"
autoComplete="firmware-uri"
onChange={(event) => setFirmware(event.target.value)}
value={firmware}
/>
<CInvalidFeedback>{t('upgrade.need_uri')}</CInvalidFeedback>
</CCol>
</CRow>
<CRow className="mt-3">
<CCol md="8">
<p>{t('common.execute_now')}</p>
</CCol>
<CCol>
<CSwitch
disabled={blockFields}
color="primary"
defaultChecked={isNow}
onClick={toggleNow}
labelOn={t('common.yes')}
labelOff={t('common.no')}
/>
</CCol>
</CRow>
<CRow className="mt-3" hidden={isNow}>
<CCol md="4" className="mt-2">
<p>{t('upgrade.time')}</p>
</CCol>
<CCol xs="12" md="8">
<DatePicker
selected={new Date(date)}
value={new Date(date)}
className={`form-control ${!validDate ? 'is-invalid' : ''}`}
includeTime
disabled={blockFields}
onChange={(newDate) => setDate(newDate.toString())}
/>
<CInvalidFeedback>{t('common.need_date')}</CInvalidFeedback>
</CCol>
</CRow>
<CRow className="mt-3" hidden={true || !isNow || disabledWaiting || !deviceConnected}>
<CCol md="8">
<p>
{t('upgrade.wait_for_upgrade')}
<b hidden={!disabledWaiting}> {t('upgrade.offline_device')}</b>
</p>
</CCol>
<CCol>
<CSwitch
disabled={blockFields || disabledWaiting}
color="primary"
defaultChecked={waitForUpgrade}
onClick={toggleWaitForUpgrade}
labelOn={t('common.yes')}
labelOff={t('common.no')}
/>
</CCol>
</CRow>
</CModalBody>
<ButtonFooter
isNow={isNow}
isShown={show}
isLoading={waitingForUpgrade}
action={postUpgrade}
color="primary"
toggleParent={toggleModal}
/>
</CModal>
);
};
FirmwareUpgradeModal.propTypes = {
show: PropTypes.bool.isRequired,
toggleModal: PropTypes.func.isRequired,
};
export default FirmwareUpgradeModal;

View File

@@ -1,12 +1,7 @@
import React, { useState, useEffect } from 'react';
import {
CButton,
CModal,
CModalHeader,
CModalBody,
CModalTitle,
CModalFooter,
} from '@coreui/react';
import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
@@ -45,17 +40,19 @@ const LatestStatisticsModal = ({ show, toggle }) => {
return (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalHeader className="p-1">
<CModalTitle className="text-dark">{t('statistics.latest_statistics')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<pre className="ignore">{JSON.stringify(latestStats, null, 4)}</pre>
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal>
);
};

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { CCard, CCardHeader, CCardBody, CRow, CCol, CPopover, CButton } from '@coreui/react';
import { CCard, CCardHeader, CCardBody, CPopover, CButton } from '@coreui/react';
import { cilSync } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import eventBus from 'utils/eventBus';
@@ -36,38 +36,31 @@ const DeviceStatisticsCard = () => {
<div>
<CCard>
<CCardHeader>
<CRow>
<CCol>
<div className="text-value-xxl pt-2">{t('statistics.title')}</div>
</CCol>
<CCol sm="6" xxl="6">
<CRow>
<CCol sm="1" xxl="5" />
<CCol sm="4" xxl="2" className="text-right">
<CButton color="secondary" onClick={goToAnalysis}>
{t('wifi_analysis.title')}
</CButton>
</CCol>
<CCol sm="3" xxl="2" className="text-right">
<CButton color="secondary" onClick={toggleLatestModal}>
{t('statistics.show_latest')}
</CButton>
</CCol>
<CCol sm="3" xxl="2" className="text-right">
<CButton color="secondary" onClick={toggleLifetimeModal}>
Lifetime Statistics
</CButton>
</CCol>
<CCol sm="1" xxl="1" className="text-center">
<CPopover content={t('common.refresh')}>
<CButton color="secondary" onClick={refresh} size="sm">
<CIcon content={cilSync} />
</CButton>
</CPopover>
</CCol>
</CRow>
</CCol>
</CRow>
<div className="d-flex flex-row-reverse align-items-center">
<div className="pl-2">
<CPopover content={t('common.refresh')}>
<CButton color="primary" variant="outline" onClick={refresh}>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<CButton color="primary" variant="outline" onClick={toggleLifetimeModal}>
Lifetime Statistics
</CButton>
</div>
<div className="pl-2">
<CButton color="primary" variant="outline" onClick={toggleLatestModal}>
{t('statistics.show_latest')}
</CButton>
</div>
<div>
<CButton color="primary" variant="outline" onClick={goToAnalysis}>
{t('wifi_analysis.title')}
</CButton>
</div>
<div className="text-value-lg mr-auto">{t('statistics.title')}</div>
</div>
</CCardHeader>
<CCardBody className="p-5">
<StatisticsChartList />

View File

@@ -8,7 +8,10 @@ import {
CSwitch,
CCol,
CRow,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
@@ -82,8 +85,15 @@ const ActionModal = ({ show, toggleModal }) => {
return (
<CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('reboot.title')}</CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('reboot.title')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
{result === 'success' ? (
<SuccessfulActionModalBody toggleModal={toggleModal} />

View File

@@ -13,7 +13,10 @@ import {
CInputRadio,
CFormGroup,
CLabel,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
@@ -256,8 +259,15 @@ const TraceModal = ({ show, toggleModal }) => {
return (
<CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('trace.title')}</CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('trace.title')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
{getBody()}
</CModal>

View File

@@ -10,7 +10,10 @@ import {
CSwitch,
CCol,
CSpinner,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
@@ -124,8 +127,15 @@ const WifiScanModal = ({ show, toggleModal }) => {
return (
<CModal size="lg" show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('actions.wifi_scan')}</CModalTitle>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('actions.wifi_scan')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<div hidden={hideOptions || waiting}>

View File

@@ -1,14 +1,9 @@
/* eslint-disable-rule prefer-destructuring */
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
CButton,
CModal,
CModalHeader,
CModalBody,
CModalTitle,
CModalFooter,
} from '@coreui/react';
import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import PropTypes from 'prop-types';
import { prettyDate } from 'utils/helper';
import WifiChannelTable from './WifiChannelTable';
@@ -48,21 +43,23 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
};
return (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalHeader>
<CModalTitle className="text-dark">
{date !== '' ? prettyDate(date) : ''} {t('scan.results')}
</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
{scanResults === null ? null : (
<WifiChannelTable channels={parseThroughList(scanResults)} />
)}
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal>
);
};

View File

@@ -89,7 +89,7 @@ const TheLayout = () => {
<PageContainer t={t} routes={routes} redirectTo="/devices" />
</ToastProvider>
</div>
<Footer t={t} version="2.1.10" />
<Footer t={t} version={process.env.VERSION} />
</div>
</div>
);

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { useParams } from 'react-router-dom';
import { CRow, CCol } from '@coreui/react';
import DeviceHealth from 'components/DeviceHealth';
import DeviceConfiguration from 'components/DeviceConfiguration';
import DeviceStatusCard from 'components/DeviceStatusCard';
import CommandHistory from 'components/CommandHistory';
import DeviceLogs from 'components/DeviceLogs';
@@ -18,20 +17,25 @@ const DevicePage = () => {
<div className="App">
<DeviceProvider axiosInstance={axiosInstance} serialNumber={deviceId}>
<CRow>
<CCol xs="12" lg="6">
<CCol>
<DeviceStatusCard />
<DeviceConfiguration />
</CCol>
<CCol xs="12" lg="6">
<DeviceLogs />
<DeviceHealth />
</CRow>
<CRow>
<CCol>
<CommandHistory />
</CCol>
<CCol>
<DeviceActionCard />
</CCol>
</CRow>
<CRow>
<CCol>
<DeviceStatisticsCard />
<CommandHistory />
</CCol>
<CCol>
<DeviceHealth />
<DeviceLogs />
</CCol>
</CRow>
</DeviceProvider>

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CCard, CCardBody } from '@coreui/react';
import { CCard, CCardBody, CCardHeader, CButton, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSave } from '@coreui/icons';
import axiosInstance from 'utils/axiosInstance';
import { testRegex } from 'utils/helper';
import { useUser, EditMyProfile, useAuth, useToast } from 'ucentral-libs';
@@ -122,10 +124,10 @@ const ProfilePage = () => {
setNewAvatarFile(null);
setFileInputKey(fileInputKey + 1);
})
.catch(() => {
.catch((e) => {
addToast({
title: t('user.update_failure_title'),
body: t('user.update_failure'),
body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
@@ -180,10 +182,10 @@ const ProfilePage = () => {
autohide: true,
});
})
.catch(() => {
.catch((e) => {
addToast({
title: t('user.update_failure_title'),
body: t('user.update_failure'),
body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
@@ -260,12 +262,20 @@ const ProfilePage = () => {
return (
<CCard>
<CCardHeader>
<div className="text-right">
<CPopover content={t('common.save')}>
<CButton onClick={updateUser} color="primary" variant="outline" disabled={loading}>
<CIcon content={cilSave} />
</CButton>
</CPopover>
</div>
</CCardHeader>
<CCardBody>
<EditMyProfile
t={t}
user={userForm}
updateUserWithId={updateWithId}
saveUser={updateUser}
loading={loading}
policies={policies}
addNote={addNote}

View File

@@ -45,7 +45,13 @@ const UserListPage = () => {
.then((response) => {
setUsers(response.data.users);
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('user.error_fetching_users', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
@@ -113,7 +119,13 @@ const UserListPage = () => {
setUsersToDisplay(newUsers);
setLoading(false);
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('user.error_fetching_users', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
@@ -139,10 +151,10 @@ const UserListPage = () => {
});
getUsers();
})
.catch(() => {
.catch((e) => {
addToast({
title: t('common.error'),
body: t('user.delete_failure'),
body: t('user.delete_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
@@ -186,6 +198,7 @@ const UserListPage = () => {
usersPerPage={usersPerPage}
setUsersPerPage={updateUsersPerPage}
pageCount={pageCount}
currentPage={page.selected}
setPage={setPage}
deleteUser={deleteUser}
deleteLoading={deleteLoading}

View File

@@ -14,8 +14,12 @@ import {
CModal,
CModalHeader,
CModalBody,
CModalTitle,
CRow,
CPopover,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
const WifiAnalysisPage = () => {
const { t } = useTranslation();
@@ -198,7 +202,7 @@ const WifiAnalysisPage = () => {
<h5 className="mb-0">{t('common.device', { serialNumber: deviceId })}</h5>
</CCol>
<CCol className="text-right">
<CButton color="secondary" onClick={toggleModal}>
<CButton color="primary" variant="outline" onClick={toggleModal}>
{t('wifi_analysis.network_diagram')}
</CButton>
</CCol>
@@ -235,7 +239,16 @@ const WifiAnalysisPage = () => {
</CCardBody>
</CCard>
<CModal size="xl" show={showModal} onClose={toggleModal}>
<CModalHeader closeButton>{t('wifi_analysis.network_diagram')}</CModalHeader>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('wifi_analysis.network_diagram')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
{showModal ? (
<NetworkDiagram