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

18
package-lock.json generated
View File

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

View File

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

View File

@@ -23,10 +23,11 @@
}, },
"commands": { "commands": {
"error": "Fehler beim Senden des Befehls!", "error": "Fehler beim Senden des Befehls!",
"error_delete_log": "Fehler beim Versuch zu löschen: {{error}}",
"event_queue": "Ereigniswarteschlange", "event_queue": "Ereigniswarteschlange",
"success": "Befehl wurde erfolgreich übermittelt", "success": "Befehl wurde erfolgreich übermittelt",
"title": "Gerätebefehle", "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": { "common": {
"access_policy": "Zugangsrichtlinien", "access_policy": "Zugangsrichtlinien",
@@ -205,6 +206,9 @@
"title": "Gerät konfigurieren", "title": "Gerät konfigurieren",
"valid_json": "Sie müssen ein gültiges JSON eingeben" "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": { "delete_command": {
"explanation": "Möchten Sie diesen Befehl wirklich löschen? Diese Aktion ist nicht umkehrbar.", "explanation": "Möchten Sie diesen Befehl wirklich löschen? Diese Aktion ist nicht umkehrbar.",
"title": "Befehl löschen" "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.", "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" "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": { "device_logs": {
"log": "Protokoll", "log": "Protokoll",
"severity": "Wichtigkeit", "severity": "Wichtigkeit",
@@ -486,7 +494,7 @@
"create_success": "Benutzer erfolgreich erstellt", "create_success": "Benutzer erfolgreich erstellt",
"creating": "Benutzer erstellen ...", "creating": "Benutzer erstellen ...",
"delete_avatar": "Avatar löschen", "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_success": "Benutzer erfolgreich gelöscht!",
"delete_title": "Benutzer löschen", "delete_title": "Benutzer löschen",
"delete_warning": "Warnung: Sobald Sie einen Benutzer gelöscht haben, können Sie ihn nicht wiederherstellen", "delete_warning": "Warnung: Sobald Sie einen Benutzer gelöscht haben, können Sie ihn nicht wiederherstellen",
@@ -494,6 +502,7 @@
"description": "Beschreibung", "description": "Beschreibung",
"edit": "Benutzer bearbeiten", "edit": "Benutzer bearbeiten",
"email_address": "E-Mail-Addresse", "email_address": "E-Mail-Addresse",
"error_fetching_users": "Fehler beim Abrufen der Nutzer: {{error}}",
"force_password_change": "Passwortänderung bei der Anmeldung erzwingen", "force_password_change": "Passwortänderung bei der Anmeldung erzwingen",
"id": "Benutzeridentifikation.", "id": "Benutzeridentifikation.",
"last_login": "Letzte Anmeldung", "last_login": "Letzte Anmeldung",
@@ -509,7 +518,7 @@
"provide_password": "Bitte geben Sie ein gültiges Passwort ein", "provide_password": "Bitte geben Sie ein gültiges Passwort ein",
"save_avatar": "Avatar speichern", "save_avatar": "Avatar speichern",
"show_hide_password": "Passwort anzeigen/verbergen", "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_failure_title": "Update fehlgeschlagen",
"update_success": "Benutzer erfolgreich aktualisiert", "update_success": "Benutzer erfolgreich aktualisiert",
"update_success_title": "Erfolg", "update_success_title": "Erfolg",

View File

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

View File

@@ -23,10 +23,11 @@
}, },
"commands": { "commands": {
"error": "¡Error al enviar el comando!", "error": "¡Error al enviar el comando!",
"error_delete_log": "Error al intentar eliminar: {{error}}",
"event_queue": "Cola de eventos", "event_queue": "Cola de eventos",
"success": "Comando enviado con éxito, puede consultar el registro de Comandos para ver el resultado", "success": "Comando enviado con éxito, puede consultar el registro de Comandos para ver el resultado",
"title": "Historial de Comandos", "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": { "common": {
"access_policy": "Política de acceso", "access_policy": "Política de acceso",
@@ -205,6 +206,9 @@
"title": "Configurar", "title": "Configurar",
"valid_json": "Debes ingresar un JSON válido" "valid_json": "Debes ingresar un JSON válido"
}, },
"connect": {
"error_trying_to_connect": "Error al intentar conectarse al dispositivo: {{error}}"
},
"delete_command": { "delete_command": {
"explanation": "¿Está seguro de que desea eliminar este comando? Esta acción no es reversible.", "explanation": "¿Está seguro de que desea eliminar este comando? Esta acción no es reversible.",
"title": "Eliminar comando" "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.", "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" "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": { "device_logs": {
"log": "Iniciar sesión", "log": "Iniciar sesión",
"severity": "Gravedad", "severity": "Gravedad",
@@ -486,7 +494,7 @@
"create_success": "Usuario creado con éxito", "create_success": "Usuario creado con éxito",
"creating": "Creando usuario ...", "creating": "Creando usuario ...",
"delete_avatar": "Eliminar avatar", "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_success": "¡Usuario eliminado correctamente!",
"delete_title": "Borrar usuario", "delete_title": "Borrar usuario",
"delete_warning": "Advertencia: una vez que elimina un usuario, no puede revertir", "delete_warning": "Advertencia: una vez que elimina un usuario, no puede revertir",
@@ -494,6 +502,7 @@
"description": "Descripción", "description": "Descripción",
"edit": "editar usuario", "edit": "editar usuario",
"email_address": "Dirección de correo electrónico", "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", "force_password_change": "Forzar cambio de contraseña al iniciar sesión",
"id": "Id. De usuario", "id": "Id. De usuario",
"last_login": "Último acceso", "last_login": "Último acceso",
@@ -509,7 +518,7 @@
"provide_password": "Proporcione una contraseña válida", "provide_password": "Proporcione una contraseña válida",
"save_avatar": "Guardar avatar", "save_avatar": "Guardar avatar",
"show_hide_password": "Mostrar / Ocultar contraseña", "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_failure_title": "Actualización fallida",
"update_success": "Usuario actualizado con éxito", "update_success": "Usuario actualizado con éxito",
"update_success_title": "Éxito", "update_success_title": "Éxito",

View File

@@ -23,10 +23,11 @@
}, },
"commands": { "commands": {
"error": "Erreur lors de la soumission de la commande !", "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", "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", "success": "Commande soumise avec succès, vous pouvez consulter le journal des commandes pour le résultat",
"title": "Historique des commandes", "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": { "common": {
"access_policy": "Politique d'accès", "access_policy": "Politique d'accès",
@@ -205,6 +206,9 @@
"title": "Configurer", "title": "Configurer",
"valid_json": "Vous devez entrer un JSON valide" "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": { "delete_command": {
"explanation": "Êtes-vous sûr de vouloir supprimer cette commande ? Cette action n'est pas réversible.", "explanation": "Êtes-vous sûr de vouloir supprimer cette commande ? Cette action n'est pas réversible.",
"title": "Supprimer la commande" "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.", "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" "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": { "device_logs": {
"log": "Bûche", "log": "Bûche",
"severity": "Gravité", "severity": "Gravité",
@@ -486,7 +494,7 @@
"create_success": "L'utilisateur a été créé avec succès", "create_success": "L'utilisateur a été créé avec succès",
"creating": "Création de l'utilisateur...", "creating": "Création de l'utilisateur...",
"delete_avatar": "Supprimer l'avatar", "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_success": "Utilisateur supprimé avec succès !",
"delete_title": "Supprimer l'utilisateur", "delete_title": "Supprimer l'utilisateur",
"delete_warning": "Avertissement : Une fois que vous avez supprimé un utilisateur, vous ne pouvez plus revenir en arrière", "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", "description": "La description",
"edit": "Modifier l'utilisateur", "edit": "Modifier l'utilisateur",
"email_address": "Adresse électronique", "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", "force_password_change": "Forcer le changement de mot de passe lors de la connexion",
"id": "Identifiant d'utilisateur.", "id": "Identifiant d'utilisateur.",
"last_login": "Dernière connexion", "last_login": "Dernière connexion",
@@ -509,7 +518,7 @@
"provide_password": "Veuillez fournir un mot de passe valide", "provide_password": "Veuillez fournir un mot de passe valide",
"save_avatar": "Enregistrer l'avatar", "save_avatar": "Enregistrer l'avatar",
"show_hide_password": "Afficher/Masquer le mot de passe", "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_failure_title": "mise à jour a échoué",
"update_success": "L'utilisateur a bien été mis à jour", "update_success": "L'utilisateur a bien été mis à jour",
"update_success_title": "Succès", "update_success_title": "Succès",

View File

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

View File

@@ -11,7 +11,10 @@ import {
CFormGroup, CFormGroup,
CInputRadio, CInputRadio,
CLabel, CLabel,
CPopover,
} from '@coreui/react'; } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker'; import DatePicker from 'react-widgets/DatePicker';
@@ -88,8 +91,15 @@ const BlinkModal = ({ show, toggleModal }) => {
return ( return (
<CModal show={show} onClose={toggleModal}> <CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton> <CModalHeader className="p-1">
<CModalTitle>{t('blink.device_leds')}</CModalTitle> <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> </CModalHeader>
{result === 'success' ? ( {result === 'success' ? (
<SuccessfulActionModalBody toggleModal={toggleModal} /> <SuccessfulActionModalBody toggleModal={toggleModal} />

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; 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 { CreateUserForm, useFormFields, useAuth, useToast } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance'; import axiosInstance from 'utils/axiosInstance';
import { testRegex, validateEmail } from 'utils/helper'; import { testRegex, validateEmail } from 'utils/helper';
@@ -143,14 +145,26 @@ const CreateUserModal = ({ show, toggle, getUsers }) => {
return ( return (
<CModal show={show} onClose={toggle} size="xl"> <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> <CModalBody>
<CreateUserForm <CreateUserForm
t={t} t={t}
fields={formFields} fields={formFields}
updateField={updateFieldWithId} updateField={updateFieldWithId}
createUser={createUser}
loading={loading}
policies={policies} policies={policies}
toggleChange={toggleChange} toggleChange={toggleChange}
/> />

View File

@@ -1,9 +1,20 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; 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 DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types'; 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 { dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance'; import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus'; import eventBus from 'utils/eventBus';
@@ -11,6 +22,7 @@ import eventBus from 'utils/eventBus';
const DeleteLogModal = ({ show, toggle, object }) => { const DeleteLogModal = ({ show, toggle, object }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentToken, endpoints } = useAuth(); const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const { deviceSerialNumber } = useDevice(); const { deviceSerialNumber } = useDevice();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [maxDate, setMaxDate] = useState(new Date().toString()); const [maxDate, setMaxDate] = useState(new Date().toString());
@@ -36,7 +48,14 @@ const DeleteLogModal = ({ show, toggle, object }) => {
return axiosInstance return axiosInstance
.delete(`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/${object}`, options) .delete(`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/${object}`, options)
.then(() => {}) .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(() => { .finally(() => {
if (object === 'healthchecks') if (object === 'healthchecks')
eventBus.dispatch('deletedHealth', { message: 'Healthcheck was deleted' }); eventBus.dispatch('deletedHealth', { message: 'Healthcheck was deleted' });
@@ -54,12 +73,19 @@ const DeleteLogModal = ({ show, toggle, object }) => {
return ( return (
<CModal className="text-dark" show={show} onClose={toggle}> <CModal className="text-dark" show={show} onClose={toggle}>
<CModalHeader closeButton> <CModalHeader className="p-1">
<CModalTitle> <CModalTitle className="pl-1 pt-1">
{object === 'healthchecks' {object === 'healthchecks'
? t('delete_logs.healthchecks_title') ? t('delete_logs.healthchecks_title')
: t('delete_logs.device_logs_title')} : t('delete_logs.device_logs_title')}
</CModalTitle> </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> </CModalHeader>
<CModalBody> <CModalBody>
<h6>{t('delete_logs.explanation', { object })}</h6> <h6>{t('delete_logs.explanation', { object })}</h6>

View File

@@ -70,7 +70,14 @@ const DeviceActions = () => {
const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (newWindow) newWindow.opener = null; 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(() => { .finally(() => {
setConnectLoading(false); setConnectLoading(false);
}); });
@@ -131,7 +138,7 @@ const DeviceActions = () => {
</CButton> </CButton>
</CCol> </CCol>
</CRow> </CRow>
<CRow className="mt-3"> <CRow className="mt-4">
<CCol> <CCol>
<CButton block color="primary" onClick={toggleUpgradeModal}> <CButton block color="primary" onClick={toggleUpgradeModal}>
{t('actions.firmware_upgrade')} {t('actions.firmware_upgrade')}
@@ -143,7 +150,7 @@ const DeviceActions = () => {
</CButton> </CButton>
</CCol> </CCol>
</CRow> </CRow>
<CRow className="mt-3"> <CRow className="mt-4">
<CCol> <CCol>
<CButton block color="primary" onClick={toggleScanModal}> <CButton block color="primary" onClick={toggleScanModal}>
{t('actions.wifi_scan')} {t('actions.wifi_scan')}
@@ -155,7 +162,7 @@ const DeviceActions = () => {
</CButton> </CButton>
</CCol> </CCol>
</CRow> </CRow>
<CRow className="mt-3"> <CRow className="mt-4">
<CCol> <CCol>
<LoadingButton <LoadingButton
isLoading={connectLoading} isLoading={connectLoading}
@@ -170,7 +177,7 @@ const DeviceActions = () => {
</CButton> </CButton>
</CCol> </CCol>
</CRow> </CRow>
<CRow className="mt-3"> <CRow className="mt-4">
<CCol> <CCol>
<CButton block color="primary" onClick={toggleQueueModal}> <CButton block color="primary" onClick={toggleQueueModal}>
{t('commands.event_queue')} {t('commands.event_queue')}

View File

@@ -22,6 +22,7 @@ import {
NotesTable, NotesTable,
useAuth, useAuth,
useDevice, useDevice,
useToast,
} from 'ucentral-libs'; } from 'ucentral-libs';
import DeviceConfigurationModal from './DeviceConfigurationModal'; import DeviceConfigurationModal from './DeviceConfigurationModal';
@@ -29,6 +30,7 @@ const DeviceConfiguration = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentToken, endpoints } = useAuth(); const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice(); const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [collapse, setCollapse] = useState(false); const [collapse, setCollapse] = useState(false);
@@ -61,7 +63,14 @@ const DeviceConfiguration = () => {
.then((response) => { .then((response) => {
setDevice(response.data); 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) => { const saveNote = (currentNote) => {
@@ -153,46 +162,46 @@ const DeviceConfiguration = () => {
{device.firmware} {device.firmware}
</CCol> </CCol>
</CRow> </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}> <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"> <CRow className="mt-2">
<CCol md="3"> <CCol md="3">
<CLabel>{t('configuration.last_configuration_download')} : </CLabel> <CLabel>{t('configuration.last_configuration_download')} : </CLabel>

View File

@@ -13,6 +13,7 @@ import {
CPopover, CPopover,
} from '@coreui/react'; } from '@coreui/react';
import CIcon from '@coreui/icons-react'; import CIcon from '@coreui/icons-react';
import { cilTrash } from '@coreui/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker'; import DatePicker from 'react-widgets/DatePicker';
import { prettyDate, dateToUnix } from 'utils/helper'; import { prettyDate, dateToUnix } from 'utils/helper';
@@ -25,7 +26,6 @@ const DeviceHealth = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentToken, endpoints } = useAuth(); const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice(); const { deviceSerialNumber } = useDevice();
const [collapse, setCollapse] = useState(false);
const [details, setDetails] = useState([]); const [details, setDetails] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [healthChecks, setHealthChecks] = useState([]); const [healthChecks, setHealthChecks] = useState([]);
@@ -42,11 +42,6 @@ const DeviceHealth = () => {
setShowDeleteModal(!showDeleteModal); setShowDeleteModal(!showDeleteModal);
}; };
const toggle = (e) => {
setCollapse(!collapse);
e.preventDefault();
};
const modifyStart = (value) => { const modifyStart = (value) => {
setStart(value); setStart(value);
}; };
@@ -195,96 +190,71 @@ const DeviceHealth = () => {
color={barColor} color={barColor}
inverse="true" inverse="true"
footerSlot={ footerSlot={
<div className="p-4"> <div className="pb-1 px-3">
<CProgress className="mb-3" color="white" value={sanityLevel ?? 0} /> <CProgress className="mb-3" color="white" value={sanityLevel ?? 0} />
<CCollapse show={collapse}> <CRow className="mb-3">
<div className="text-right"> <CCol>
<CPopover content={t('common.delete')}> {t('common.from')}
<CButton :
color="light" <DatePicker includeTime onChange={(date) => modifyStart(date)} />
shape="square" </CCol>
size="sm" <CCol>
onClick={() => { {t('common.to')}
toggleDeleteModal(); :
}} <DatePicker includeTime onChange={(date) => modifyEnd(date)} />
> </CCol>
<CIcon name="cilTrash" size="lg" /> </CRow>
</CButton> <CCard className="p-0">
</CPopover> <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> </div>
<CRow className="mb-3"> </CCard>
<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>
<DeleteLogModal <DeleteLogModal
serialNumber={deviceSerialNumber} serialNumber={deviceSerialNumber}
object="healthchecks" object="healthchecks"
@@ -293,7 +263,15 @@ const DeviceHealth = () => {
/> />
</div> </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); setDevices(fullDevices);
setLoading(false); 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); setLoading(false);
}); });
}; };
@@ -130,7 +136,13 @@ const DeviceList = () => {
} }
getDeviceInformation(selectedPage); 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); setLoading(false);
}); });
}; };
@@ -179,7 +191,13 @@ const DeviceList = () => {
setDevices(newList); setDevices(newList);
setLoading(false); 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); setLoading(false);
}); });
}; };
@@ -282,10 +300,10 @@ const DeviceList = () => {
const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (newWindow) newWindow.opener = null; if (newWindow) newWindow.opener = null;
}) })
.catch(() => { .catch((e) => {
addToast({ addToast({
title: t('common.error'), title: t('common.error'),
body: t('common.unable_to_connect'), body: t('connect.error_trying_to_connect', { error: e.response?.data?.ErrorDescription }),
color: 'danger', color: 'danger',
autohide: true, autohide: true,
}); });

View File

@@ -12,6 +12,7 @@ import {
CPopover, CPopover,
} from '@coreui/react'; } from '@coreui/react';
import CIcon from '@coreui/icons-react'; import CIcon from '@coreui/icons-react';
import { cilTrash } from '@coreui/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker'; import DatePicker from 'react-widgets/DatePicker';
import { prettyDate, dateToUnix } from 'utils/helper'; import { prettyDate, dateToUnix } from 'utils/helper';
@@ -24,7 +25,6 @@ const DeviceLogs = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentToken, endpoints } = useAuth(); const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice(); const { deviceSerialNumber } = useDevice();
const [collapse, setCollapse] = useState(false);
const [details, setDetails] = useState([]); const [details, setDetails] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
@@ -39,11 +39,6 @@ const DeviceLogs = () => {
setShowDeleteModal(!showDeleteModal); setShowDeleteModal(!showDeleteModal);
}; };
const toggle = (e) => {
setCollapse(!collapse);
e.preventDefault();
};
const modifyStart = (value) => { const modifyStart = (value) => {
setStart(value); setStart(value);
}; };
@@ -176,91 +171,76 @@ const DeviceLogs = () => {
color="gradient-info" color="gradient-info"
header={t('device_logs.title')} header={t('device_logs.title')}
footerSlot={ footerSlot={
<div className="p-4"> <div className="pb-1 px-3">
<CCollapse show={collapse}> <CRow className="mb-3">
<div className="text-right"> <CCol>
<CPopover content={t('common.delete')}> {t('common.from')}
<CButton <DatePicker includeTime onChange={(date) => modifyStart(date)} />
color="light" </CCol>
shape="square" <CCol>
size="sm" {t('common.to')}
onClick={() => { <DatePicker includeTime onChange={(date) => modifyEnd(date)} />
toggleDeleteModal(); </CCol>
}} </CRow>
> <CCard>
<CIcon name="cilTrash" size="lg" /> <div className="overflow-auto" style={{ height: '250px' }}>
</CButton> <CDataTable
</CPopover> 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> </div>
<CRow className="mb-3"> </CCard>
<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>
</div> </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 <DeleteLogModal
serialNumber={deviceSerialNumber} serialNumber={deviceSerialNumber}
object="logs" object="logs"

View File

@@ -1,17 +1,42 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance'; 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 DeviceStatusCard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentToken, endpoints } = useAuth(); const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice(); const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [lastStats, setLastStats] = useState(null); const [lastStats, setLastStats] = useState(null);
const [status, setStatus] = useState(null); const [status, setStatus] = useState(null);
const [deviceConfig, setDeviceConfig] = useState(null);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [loading, setLoading] = 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 = () => { const getData = () => {
setLoading(true); setLoading(true);
const options = { const options = {
@@ -45,9 +70,17 @@ const DeviceStatusCard = () => {
}); });
}; };
const refresh = () => {
getData();
getDevice();
};
useEffect(() => { useEffect(() => {
setError(false); setError(false);
if (deviceSerialNumber) getData(); if (deviceSerialNumber) {
getDevice();
getData();
}
}, [deviceSerialNumber]); }, [deviceSerialNumber]);
return ( return (
@@ -56,7 +89,8 @@ const DeviceStatusCard = () => {
loading={loading} loading={loading}
error={error} error={error}
deviceSerialNumber={deviceSerialNumber} deviceSerialNumber={deviceSerialNumber}
getData={getData} getData={refresh}
deviceConfig={deviceConfig}
status={status} status={status}
lastStats={lastStats} lastStats={lastStats}
/> />

View File

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

View File

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

View File

@@ -10,7 +10,10 @@ import {
CForm, CForm,
CSwitch, CSwitch,
CAlert, CAlert,
CPopover,
} from '@coreui/react'; } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -84,8 +87,15 @@ const ConfigureModal = ({ show, toggleModal }) => {
return ( return (
<CModal show={show} onClose={toggleModal}> <CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton> <CModalHeader className="p-1">
<CModalTitle>{t('factory_reset.title')}</CModalTitle> <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> </CModalHeader>
{hadSuccess ? ( {hadSuccess ? (
<SuccessfulActionModalBody toggleModal={toggleModal} /> <SuccessfulActionModalBody toggleModal={toggleModal} />

View File

@@ -46,7 +46,7 @@ const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
return ( return (
<CModal size="xl" show={show} onClose={toggle} scrollable> <CModal size="xl" show={show} onClose={toggle} scrollable>
<CModalHeader closeButton> <CModalHeader closeButton>
<CModalTitle> <CModalTitle className="pl-1 pt-1">
#{serialNumber} {t('firmware.history_title')} #{serialNumber} {t('firmware.history_title')}
</CModalTitle> </CModalTitle>
</CModalHeader> </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 React, { useState, useEffect } from 'react';
import { import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
CButton, import CIcon from '@coreui/icons-react';
CModal, import { cilX } from '@coreui/icons';
CModalHeader,
CModalBody,
CModalTitle,
CModalFooter,
} from '@coreui/react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance'; import axiosInstance from 'utils/axiosInstance';
@@ -45,17 +40,19 @@ const LatestStatisticsModal = ({ show, toggle }) => {
return ( return (
<CModal size="lg" show={show} onClose={toggle}> <CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton> <CModalHeader className="p-1">
<CModalTitle className="text-dark">{t('statistics.latest_statistics')}</CModalTitle> <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> </CModalHeader>
<CModalBody> <CModalBody>
<pre className="ignore">{JSON.stringify(latestStats, null, 4)}</pre> <pre className="ignore">{JSON.stringify(latestStats, null, 4)}</pre>
</CModalBody> </CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal> </CModal>
); );
}; };

View File

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

View File

@@ -8,7 +8,10 @@ import {
CSwitch, CSwitch,
CCol, CCol,
CRow, CRow,
CPopover,
} from '@coreui/react'; } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker'; import DatePicker from 'react-widgets/DatePicker';
@@ -82,8 +85,15 @@ const ActionModal = ({ show, toggleModal }) => {
return ( return (
<CModal show={show} onClose={toggleModal}> <CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton> <CModalHeader className="p-1">
<CModalTitle>{t('reboot.title')}</CModalTitle> <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> </CModalHeader>
{result === 'success' ? ( {result === 'success' ? (
<SuccessfulActionModalBody toggleModal={toggleModal} /> <SuccessfulActionModalBody toggleModal={toggleModal} />

View File

@@ -13,7 +13,10 @@ import {
CInputRadio, CInputRadio,
CFormGroup, CFormGroup,
CLabel, CLabel,
CPopover,
} from '@coreui/react'; } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -256,8 +259,15 @@ const TraceModal = ({ show, toggleModal }) => {
return ( return (
<CModal show={show} onClose={toggleModal}> <CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton> <CModalHeader className="p-1">
<CModalTitle>{t('trace.title')}</CModalTitle> <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> </CModalHeader>
{getBody()} {getBody()}
</CModal> </CModal>

View File

@@ -10,7 +10,10 @@ import {
CSwitch, CSwitch,
CCol, CCol,
CSpinner, CSpinner,
CPopover,
} from '@coreui/react'; } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -124,8 +127,15 @@ const WifiScanModal = ({ show, toggleModal }) => {
return ( return (
<CModal size="lg" show={show} onClose={toggleModal}> <CModal size="lg" show={show} onClose={toggleModal}>
<CModalHeader closeButton> <CModalHeader className="p-1">
<CModalTitle>{t('actions.wifi_scan')}</CModalTitle> <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> </CModalHeader>
<CModalBody> <CModalBody>
<div hidden={hideOptions || waiting}> <div hidden={hideOptions || waiting}>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; 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 axiosInstance from 'utils/axiosInstance';
import { testRegex } from 'utils/helper'; import { testRegex } from 'utils/helper';
import { useUser, EditMyProfile, useAuth, useToast } from 'ucentral-libs'; import { useUser, EditMyProfile, useAuth, useToast } from 'ucentral-libs';
@@ -122,10 +124,10 @@ const ProfilePage = () => {
setNewAvatarFile(null); setNewAvatarFile(null);
setFileInputKey(fileInputKey + 1); setFileInputKey(fileInputKey + 1);
}) })
.catch(() => { .catch((e) => {
addToast({ addToast({
title: t('user.update_failure_title'), title: t('user.update_failure_title'),
body: t('user.update_failure'), body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger', color: 'danger',
autohide: true, autohide: true,
}); });
@@ -180,10 +182,10 @@ const ProfilePage = () => {
autohide: true, autohide: true,
}); });
}) })
.catch(() => { .catch((e) => {
addToast({ addToast({
title: t('user.update_failure_title'), title: t('user.update_failure_title'),
body: t('user.update_failure'), body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger', color: 'danger',
autohide: true, autohide: true,
}); });
@@ -260,12 +262,20 @@ const ProfilePage = () => {
return ( return (
<CCard> <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> <CCardBody>
<EditMyProfile <EditMyProfile
t={t} t={t}
user={userForm} user={userForm}
updateUserWithId={updateWithId} updateUserWithId={updateWithId}
saveUser={updateUser}
loading={loading} loading={loading}
policies={policies} policies={policies}
addNote={addNote} addNote={addNote}

View File

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

View File

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