Version 2.5.18
597
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.4.3",
|
"version": "2.5.18",
|
||||||
"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": "^1.0.37",
|
"ucentral-libs": "^1.0.57",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -82,7 +82,6 @@
|
|||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"mini-css-extract-plugin": "^1.6.1",
|
"mini-css-extract-plugin": "^1.6.1",
|
||||||
"node-sass": "^5.0.0",
|
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "LEDs Blinken",
|
"blink": "LEDs Blinken",
|
||||||
"device_leds": "LEDs",
|
"device_leds": "LEDs",
|
||||||
"execute_now": "Möchten Sie dieses Muster jetzt einstellen?",
|
"execute_now": "Möchten Sie dieses Muster jetzt einstellen?",
|
||||||
|
"explanation": "Welches Muster möchten Sie auf diesem Gerät für 30 Sekunden einstellen?",
|
||||||
"pattern": "Wählen Sie das Muster, das Sie verwenden möchten:",
|
"pattern": "Wählen Sie das Muster, das Sie verwenden möchten:",
|
||||||
"set_leds": "LEDs einstellen",
|
"set_leds": "LEDs einstellen",
|
||||||
"when_blink_leds": "Wann möchten Sie die LEDs blinken lassen?"
|
"when_blink_leds": "Wann möchten Sie die LEDs blinken lassen?"
|
||||||
@@ -37,9 +38,11 @@
|
|||||||
"add_note": "Notiz hinzufügen",
|
"add_note": "Notiz hinzufügen",
|
||||||
"add_note_explanation": "Schreiben Sie unten Ihre neue Notiz und klicken Sie auf die Schaltfläche \"+\", wo Sie fertig sind",
|
"add_note_explanation": "Schreiben Sie unten Ihre neue Notiz und klicken Sie auf die Schaltfläche \"+\", wo Sie fertig sind",
|
||||||
"adding_ellipsis": "Hinzufügen ...",
|
"adding_ellipsis": "Hinzufügen ...",
|
||||||
|
"all": "Alles",
|
||||||
"are_you_sure": "Bist du sicher?",
|
"are_you_sure": "Bist du sicher?",
|
||||||
"back_to_login": "Zurück zur Anmeldung",
|
"back_to_login": "Zurück zur Anmeldung",
|
||||||
"back_to_start": "Zurück zum Start",
|
"back_to_start": "Zurück zum Start",
|
||||||
|
"blacklist": "Schwarze Liste",
|
||||||
"by": "Durch",
|
"by": "Durch",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"certificate": "Zertifikat",
|
"certificate": "Zertifikat",
|
||||||
@@ -62,12 +65,14 @@
|
|||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"created": "Erstellt",
|
"created": "Erstellt",
|
||||||
"created_by": "Erstellt von",
|
"created_by": "Erstellt von",
|
||||||
|
"creator": "Schöpfer",
|
||||||
"current": "Aktuell",
|
"current": "Aktuell",
|
||||||
"custom_date": "Benutzerdefiniertes Datum",
|
"custom_date": "Benutzerdefiniertes Datum",
|
||||||
"dashboard": "Instrumententafel",
|
"dashboard": "Instrumententafel",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"day": "tag",
|
"day": "tag",
|
||||||
"days": "tage",
|
"days": "tage",
|
||||||
|
"default_map": "Standardkarte",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"delete_device": "Gerät löschen",
|
"delete_device": "Gerät löschen",
|
||||||
"details": "Einzelheiten",
|
"details": "Einzelheiten",
|
||||||
@@ -85,6 +90,7 @@
|
|||||||
"dismiss": "entlassen",
|
"dismiss": "entlassen",
|
||||||
"do_now": "Sofort",
|
"do_now": "Sofort",
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
|
"duplicate": "Duplikat",
|
||||||
"duration": "Dauer",
|
"duration": "Dauer",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"edit_user": "Bearbeiten",
|
"edit_user": "Bearbeiten",
|
||||||
@@ -94,6 +100,7 @@
|
|||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"error_adding_note": "Fehler beim Hinzufügen einer Notiz",
|
"error_adding_note": "Fehler beim Hinzufügen einer Notiz",
|
||||||
"error_code": "Fehlercode",
|
"error_code": "Fehlercode",
|
||||||
|
"errors": "Fehler",
|
||||||
"execute_now": "Möchten Sie diesen Befehl jetzt ausführen?",
|
"execute_now": "Möchten Sie diesen Befehl jetzt ausführen?",
|
||||||
"executed": "Ausgeführt",
|
"executed": "Ausgeführt",
|
||||||
"exit": "Ausgang",
|
"exit": "Ausgang",
|
||||||
@@ -142,14 +149,16 @@
|
|||||||
"no_items": "Keine Gegenstände",
|
"no_items": "Keine Gegenstände",
|
||||||
"none": "Keiner",
|
"none": "Keiner",
|
||||||
"not_connected": "Nicht verbunden",
|
"not_connected": "Nicht verbunden",
|
||||||
"of_connected": "% der Geräte",
|
"of_connected": "% der verbundenen Geräte",
|
||||||
"off": "Aus",
|
"off": "Aus",
|
||||||
"on": "An",
|
"on": "An",
|
||||||
"optional": "Wahlweise",
|
"optional": "Wahlweise",
|
||||||
"overall_health": "Allgemeine Gesundheit",
|
"overall_health": "Allgemeine Gesundheit",
|
||||||
"password_policy": "Kennwortrichtlinie",
|
"password_policy": "Kennwortrichtlinie",
|
||||||
|
"preferences": "Einstellungen",
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"program": "Programm",
|
"program": "Programm",
|
||||||
|
"reason": "Grund",
|
||||||
"recorded": "Verzeichnet",
|
"recorded": "Verzeichnet",
|
||||||
"refresh": "Aktualisierung",
|
"refresh": "Aktualisierung",
|
||||||
"refresh_device": "Gerät aktualisieren",
|
"refresh_device": "Gerät aktualisieren",
|
||||||
@@ -170,12 +179,14 @@
|
|||||||
"show_all": "Zeige alles",
|
"show_all": "Zeige alles",
|
||||||
"socket_connection_closed": "Verbindung geschlossen!",
|
"socket_connection_closed": "Verbindung geschlossen!",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
|
"status": "Status",
|
||||||
"stop_editing": "Stoppen Sie die Bearbeitung",
|
"stop_editing": "Stoppen Sie die Bearbeitung",
|
||||||
"submit": "Absenden",
|
"submit": "Absenden",
|
||||||
"submitted": "Eingereicht",
|
"submitted": "Eingereicht",
|
||||||
"success": "Erfolg",
|
"success": "Erfolg",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"table": "Tabelle",
|
"table": "Tabelle",
|
||||||
|
"time_per_device": "Gerät/Sekunde",
|
||||||
"timestamp": "Zeit",
|
"timestamp": "Zeit",
|
||||||
"to": "zu",
|
"to": "zu",
|
||||||
"type": "Art",
|
"type": "Art",
|
||||||
@@ -190,6 +201,7 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Anbieter",
|
"vendors": "Anbieter",
|
||||||
"view_more": "Mehr anzeigen",
|
"view_more": "Mehr anzeigen",
|
||||||
|
"visibility": "Sichtweite",
|
||||||
"yes": "Ja"
|
"yes": "Ja"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +222,8 @@
|
|||||||
"creation_success": "Konfiguration erfolgreich erstellt!",
|
"creation_success": "Konfiguration erfolgreich erstellt!",
|
||||||
"currently_associated": "Aktuell zugeordnete Konfiguration: {{config}}",
|
"currently_associated": "Aktuell zugeordnete Konfiguration: {{config}}",
|
||||||
"currently_selected_config": "Derzeit ausgewählte Konfiguration: {{config}}",
|
"currently_selected_config": "Derzeit ausgewählte Konfiguration: {{config}}",
|
||||||
|
"default_configs": "Standardkonfigurationen",
|
||||||
|
"default_configurations": "Standardkonfigurationen",
|
||||||
"delete_config": "Konfiguration löschen",
|
"delete_config": "Konfiguration löschen",
|
||||||
"details": "Gerätedetails",
|
"details": "Gerätedetails",
|
||||||
"device_password": "Passwort",
|
"device_password": "Passwort",
|
||||||
@@ -218,6 +232,7 @@
|
|||||||
"devices_affected": "Von dieser Konfiguration betroffene Geräte:",
|
"devices_affected": "Von dieser Konfiguration betroffene Geräte:",
|
||||||
"edit_configuration": "Konfiguration bearbeiten",
|
"edit_configuration": "Konfiguration bearbeiten",
|
||||||
"error_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
"error_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
||||||
|
"error_delete_blacklist": "Fehler beim Löschen aus der schwarzen Liste: {{error}}",
|
||||||
"error_fetching_config": "Fehler beim Abrufen der Konfiguration",
|
"error_fetching_config": "Fehler beim Abrufen der Konfiguration",
|
||||||
"error_trying_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
"error_trying_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
||||||
"error_update": "Fehler: {{error}}",
|
"error_update": "Fehler: {{error}}",
|
||||||
@@ -261,6 +276,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "Zugangs-PIN",
|
"access_pin": "Zugangs-PIN",
|
||||||
"add_contact": "Kontakt hinzufügen",
|
"add_contact": "Kontakt hinzufügen",
|
||||||
|
"contact": "Kontakt",
|
||||||
"create_contact": "Kontakt erstellen",
|
"create_contact": "Kontakt erstellen",
|
||||||
"currently_selected_contact": "Aktuell ausgewählter Kontakt: {{contact}}",
|
"currently_selected_contact": "Aktuell ausgewählter Kontakt: {{contact}}",
|
||||||
"delete": "Kontakt löschen?",
|
"delete": "Kontakt löschen?",
|
||||||
@@ -300,12 +316,23 @@
|
|||||||
"healthchecks_title": "Healthchecks löschen"
|
"healthchecks_title": "Healthchecks löschen"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Gerät zur Blacklist hinzufügen",
|
||||||
|
"all_devices": "Alle Geräte",
|
||||||
|
"blacklisted_on": "Datum",
|
||||||
|
"capabilities": "Fähigkeiten",
|
||||||
"certificate_explanation": "Zertifikate der angeschlossenen Geräte",
|
"certificate_explanation": "Zertifikate der angeschlossenen Geräte",
|
||||||
|
"edit_blacklist": "Gerät auf der schwarzen Liste bearbeiten",
|
||||||
|
"error_adding_blacklist": "Fehler beim Hinzufügen des Geräts zur Blacklist: {{error}}",
|
||||||
|
"error_edit_blacklist": "Fehler beim Bearbeiten der schwarzen Liste: {{error}}",
|
||||||
"error_fetching_device": "Fehler beim Abrufen der Geräteinformationen: {{error}}",
|
"error_fetching_device": "Fehler beim Abrufen der Geräteinformationen: {{error}}",
|
||||||
"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}",
|
"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}",
|
||||||
"health_explanation": "Zustand der angeschlossenen Geräte",
|
"health_explanation": "Zustand der verbundenen Geräte ((Geräte = 100 % * 100 + Geräte > 90 % * 95 + Geräte > 60 % * 75 + Geräte < 60 % * 35) / Verbundene Geräte)",
|
||||||
"memory_explanation": "Von angeschlossenen Geräten belegter Speicher",
|
"memory_explanation": "Anzahl verbundener Geräte mit entsprechendem belegtem Speicher %",
|
||||||
"uptimes_explanation": "Zeit, zu der verbundene Geräte aktiv und verbunden waren"
|
"remove_from_blacklist": "Von der schwarzen Liste entfernen",
|
||||||
|
"success_added_blacklist": "Gerät erfolgreich zur Blacklist hinzugefügt!",
|
||||||
|
"success_edit_blacklist": "Blacklist erfolgreich bearbeitet!",
|
||||||
|
"success_removed_blacklist": "Gerät erfolgreich von Blacklist entfernt!",
|
||||||
|
"uptimes_explanation": "Anzahl der verbundenen Geräte basierend auf ihrer Betriebszeit"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Protokoll",
|
"log": "Protokoll",
|
||||||
@@ -320,23 +347,32 @@
|
|||||||
"add_success": "Entität erfolgreich erstellt!",
|
"add_success": "Entität erfolgreich erstellt!",
|
||||||
"assigned_inventory": "Zugewiesenes Inventar",
|
"assigned_inventory": "Zugewiesenes Inventar",
|
||||||
"cannot_delete": "Entitäten mit untergeordneten Elementen können nicht gelöscht werden. Löschen Sie die untergeordneten Elemente dieser Entität, um sie löschen zu können.",
|
"cannot_delete": "Entitäten mit untergeordneten Elementen können nicht gelöscht werden. Löschen Sie die untergeordneten Elemente dieser Entität, um sie löschen zu können.",
|
||||||
|
"confirm_map_delete": "Möchten Sie die Karte {{name}}wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden",
|
||||||
"currently_selected_entity": "Derzeit ausgewähltes Unternehmen: {{config}}",
|
"currently_selected_entity": "Derzeit ausgewähltes Unternehmen: {{config}}",
|
||||||
"currently_selected_venue": "Aktuell ausgewählter Veranstaltungsort: {{config}}",
|
"currently_selected_venue": "Aktuell ausgewählter Veranstaltungsort: {{config}}",
|
||||||
"delete_success": "Entität erfolgreich gelöscht",
|
"delete_success": "Entität erfolgreich gelöscht",
|
||||||
"delete_warning": "Achtung: Dieser Vorgang kann nicht rückgängig gemacht werden",
|
"delete_warning": "Achtung: Dieser Vorgang kann nicht rückgängig gemacht werden",
|
||||||
|
"duplicate_from_node": "Mit einem bestimmten Root-Knoten duplizieren",
|
||||||
|
"duplicate_map": "Karte duplizieren",
|
||||||
|
"duplicate_with_node": "Dupliziere {{mapName}} mit {{rootName}} als Root-Knoten",
|
||||||
"edit_failure": "Aktualisierung fehlgeschlagen : {{error}}",
|
"edit_failure": "Aktualisierung fehlgeschlagen : {{error}}",
|
||||||
"enter_here": "Geben Sie hier die IP(s) ein, die Sie hinzufügen möchten",
|
"enter_here": "Geben Sie hier die IP(s) ein, die Sie hinzufügen möchten",
|
||||||
"entire_tree": "Seitenverzeichnis",
|
"entire_tree": "Netzwerkkarte",
|
||||||
"entities": "Entitäten",
|
"entities": "Entitäten",
|
||||||
"entity": "Entität",
|
"entity": "Entität",
|
||||||
|
"error_deleting_map": "Fehler beim Löschen der Karte: {{error}}",
|
||||||
"error_fetch_entity": "Fehler beim Abrufen von Entitätsinformationen",
|
"error_fetch_entity": "Fehler beim Abrufen von Entitätsinformationen",
|
||||||
"error_fetching": "Fehler beim Abrufen von Entitäten",
|
"error_fetching": "Fehler beim Abrufen von Entitäten",
|
||||||
"error_fetching_map": "Fehler beim Abrufen der Karte: {{error}}",
|
"error_fetching_map": "Fehler beim Abrufen der Karte: {{error}}",
|
||||||
|
"error_fetching_tree": "Fehler beim Abrufen des Baums: {{error}}",
|
||||||
"error_saving": "Fehler beim Speichern der Entität",
|
"error_saving": "Fehler beim Speichern der Entität",
|
||||||
|
"error_saving_map": "Fehler beim Speichern der Karte: {{error}}",
|
||||||
"higher_priority": "Stellen Sie eine höhere Priorität ein",
|
"higher_priority": "Stellen Sie eine höhere Priorität ein",
|
||||||
"ip_detection": "IP-Erkennung",
|
"ip_detection": "IP-Erkennung",
|
||||||
"ip_formats": "Sie können IPv4- oder IPv6-Adressen in den folgenden Formaten hinzufügen:",
|
"ip_formats": "Sie können IPv4- oder IPv6-Adressen in den folgenden Formaten hinzufügen:",
|
||||||
"lower_priority": "Niedrigere Priorität setzen",
|
"lower_priority": "Niedrigere Priorität setzen",
|
||||||
|
"map": "Karte",
|
||||||
|
"map_delete_success": "Karte erfolgreich gelöscht!",
|
||||||
"need_select_entity": "sSie müssen eine Entität aus der folgenden Tabelle auswählen",
|
"need_select_entity": "sSie müssen eine Entität aus der folgenden Tabelle auswählen",
|
||||||
"no_ips": "Keine IPs ausgewählt",
|
"no_ips": "Keine IPs ausgewählt",
|
||||||
"not_assigned": "Nicht zugeordnet",
|
"not_assigned": "Nicht zugeordnet",
|
||||||
@@ -344,6 +380,7 @@
|
|||||||
"select_entity": "Wählen Sie diese Entität aus",
|
"select_entity": "Wählen Sie diese Entität aus",
|
||||||
"selected_entity": "Ausgewählte Einheit",
|
"selected_entity": "Ausgewählte Einheit",
|
||||||
"selected_map": "Ausgewählte Karte",
|
"selected_map": "Ausgewählte Karte",
|
||||||
|
"tree_saved": "Karte erfolgreich gespeichert!",
|
||||||
"update_failure_error": "Fehler beim Versuch, die Entität zu aktualisieren: {{error}}",
|
"update_failure_error": "Fehler beim Versuch, die Entität zu aktualisieren: {{error}}",
|
||||||
"valid_serial": "Muss eine gültige Seriennummer sein (12 HEX-Zeichen)",
|
"valid_serial": "Muss eine gültige Seriennummer sein (12 HEX-Zeichen)",
|
||||||
"venues": "Veranstaltungsorte"
|
"venues": "Veranstaltungsorte"
|
||||||
@@ -541,6 +578,9 @@
|
|||||||
"verification_code": "Geben Sie hier Ihre Bestätigung ein",
|
"verification_code": "Geben Sie hier Ihre Bestätigung ein",
|
||||||
"wrong_code": "Der eingegebene Bestätigungscode ist ungültig."
|
"wrong_code": "Der eingegebene Bestätigungscode ist ungültig."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Bereitstellung"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "Wann möchten Sie dieses Gerät neu starten?",
|
"directions": "Wann möchten Sie dieses Gerät neu starten?",
|
||||||
"now": "Möchten Sie dieses Gerät jetzt neu starten?",
|
"now": "Möchten Sie dieses Gerät jetzt neu starten?",
|
||||||
@@ -584,7 +624,7 @@
|
|||||||
"mac_prefix": "MAC-Präfix",
|
"mac_prefix": "MAC-Präfix",
|
||||||
"max_associations": "max. Verbände",
|
"max_associations": "max. Verbände",
|
||||||
"max_clients": "Max. Kunden",
|
"max_clients": "Max. Kunden",
|
||||||
"messages_transmitted": "Gesendete Nachrichten",
|
"messages_transmitted": "Nachricht TX",
|
||||||
"min_associations": "Mindest. Verbände",
|
"min_associations": "Mindest. Verbände",
|
||||||
"min_clients": "Mindest. Kunden",
|
"min_clients": "Mindest. Kunden",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
@@ -592,7 +632,7 @@
|
|||||||
"prefix_length": "Erforderlich, muss eine Länge von 6 Zeichen haben",
|
"prefix_length": "Erforderlich, muss eine Länge von 6 Zeichen haben",
|
||||||
"previous_runs": "Vorherige Läufe",
|
"previous_runs": "Vorherige Läufe",
|
||||||
"received": "empfangen",
|
"received": "empfangen",
|
||||||
"received_messages": "Erhaltene Nachrichten",
|
"received_messages": "Nachricht RX",
|
||||||
"reconnect_interval": "Wiederverbindungsintervall",
|
"reconnect_interval": "Wiederverbindungsintervall",
|
||||||
"resume": "Fortsetzen",
|
"resume": "Fortsetzen",
|
||||||
"resume_success": "Lauf wieder aufgenommen!",
|
"resume_success": "Lauf wieder aufgenommen!",
|
||||||
@@ -634,6 +674,19 @@
|
|||||||
"uptime": "Betriebszeit",
|
"uptime": "Betriebszeit",
|
||||||
"used_total_memory": "{{used}} verwendet / {{total}} insgesamt"
|
"used_total_memory": "{{used}} verwendet / {{total}} insgesamt"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"create": "Abonnenten erstellen",
|
||||||
|
"edit": "Abonnent bearbeiten",
|
||||||
|
"error_create": "Fehler beim Erstellen des Abonnenten: {{error}}",
|
||||||
|
"error_delete": "Fehler beim Löschen des Abonnenten: {{error}}",
|
||||||
|
"error_fetching": "Fehler beim Abrufen von Abonnenten: {{error}}",
|
||||||
|
"error_fetching_single": "Fehler beim Abrufen des Abonnenten: {{error}}",
|
||||||
|
"error_update": "Fehler beim Aktualisieren des Abonnenten: {{error}}",
|
||||||
|
"subscribers": "Abonnenten",
|
||||||
|
"success_create": "Abonnent erfolgreich erstellt!",
|
||||||
|
"success_delete": "Abonnent erfolgreich gelöscht!",
|
||||||
|
"success_update": "Abonnent erfolgreich aktualisiert!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Fehler beim Abrufen von Systeminformationen",
|
"error_fetching": "Fehler beim Abrufen von Systeminformationen",
|
||||||
"error_reloading": "Fehler beim Neuladen: {{error}}",
|
"error_reloading": "Fehler beim Neuladen: {{error}}",
|
||||||
@@ -723,6 +776,7 @@
|
|||||||
"send_code_again": "Code nochmal senden",
|
"send_code_again": "Code nochmal senden",
|
||||||
"show_hide_password": "Passwort anzeigen/verbergen",
|
"show_hide_password": "Passwort anzeigen/verbergen",
|
||||||
"successful_validation": "Telefonnummer bestätigt! Klicken Sie auf die Schaltfläche Speichern, um es mit Ihrem Profil zu verknüpfen",
|
"successful_validation": "Telefonnummer bestätigt! Klicken Sie auf die Schaltfläche Speichern, um es mit Ihrem Profil zu verknüpfen",
|
||||||
|
"table_title": "Admin-Benutzer",
|
||||||
"update_failure": "Fehler beim Aktualisieren: {{error}}",
|
"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",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Blink",
|
"blink": "Blink",
|
||||||
"device_leds": "Device LEDs",
|
"device_leds": "Device LEDs",
|
||||||
"execute_now": "Would you like to set this pattern now?",
|
"execute_now": "Would you like to set this pattern now?",
|
||||||
|
"explanation": "What pattern would you like to set on this device for 30 seconds?",
|
||||||
"pattern": "LEDs pattern: ",
|
"pattern": "LEDs pattern: ",
|
||||||
"set_leds": "Set LEDs",
|
"set_leds": "Set LEDs",
|
||||||
"when_blink_leds": "When would you like to make the device LEDs blink?"
|
"when_blink_leds": "When would you like to make the device LEDs blink?"
|
||||||
@@ -37,9 +38,11 @@
|
|||||||
"add_note": "Add Note",
|
"add_note": "Add Note",
|
||||||
"add_note_explanation": "Write your new note below and click the '+' button where you are done",
|
"add_note_explanation": "Write your new note below and click the '+' button where you are done",
|
||||||
"adding_ellipsis": "Adding...",
|
"adding_ellipsis": "Adding...",
|
||||||
|
"all": "All",
|
||||||
"are_you_sure": "Are you sure?",
|
"are_you_sure": "Are you sure?",
|
||||||
"back_to_login": "Back to Login",
|
"back_to_login": "Back to Login",
|
||||||
"back_to_start": "Back to start",
|
"back_to_start": "Back to start",
|
||||||
|
"blacklist": "Blacklist",
|
||||||
"by": "By",
|
"by": "By",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"certificate": "Certificate",
|
"certificate": "Certificate",
|
||||||
@@ -62,12 +65,14 @@
|
|||||||
"create": "Create",
|
"create": "Create",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"created_by": "Created By",
|
"created_by": "Created By",
|
||||||
|
"creator": "Creator",
|
||||||
"current": "Current ",
|
"current": "Current ",
|
||||||
"custom_date": "Custom Date",
|
"custom_date": "Custom Date",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"day": "day",
|
"day": "day",
|
||||||
"days": "days",
|
"days": "days",
|
||||||
|
"default_map": "Default Map",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_device": "Delete Device",
|
"delete_device": "Delete Device",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
@@ -85,6 +90,7 @@
|
|||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
"do_now": "Do Now!",
|
"do_now": "Do Now!",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"edit_user": "Edit",
|
"edit_user": "Edit",
|
||||||
@@ -94,6 +100,7 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_adding_note": "Error while adding note",
|
"error_adding_note": "Error while adding note",
|
||||||
"error_code": "Error Code",
|
"error_code": "Error Code",
|
||||||
|
"errors": "Errors",
|
||||||
"execute_now": "Would you like to execute this command now?",
|
"execute_now": "Would you like to execute this command now?",
|
||||||
"executed": "Executed",
|
"executed": "Executed",
|
||||||
"exit": "Exit",
|
"exit": "Exit",
|
||||||
@@ -142,14 +149,16 @@
|
|||||||
"no_items": "No Items",
|
"no_items": "No Items",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"not_connected": "Not Connected",
|
"not_connected": "Not Connected",
|
||||||
"of_connected": "% of devices",
|
"of_connected": "% of connected devices",
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"on": "On",
|
"on": "On",
|
||||||
"optional": "Optional",
|
"optional": "Optional",
|
||||||
"overall_health": "Overall Health",
|
"overall_health": "Overall Health",
|
||||||
"password_policy": "Password Policy",
|
"password_policy": "Password Policy",
|
||||||
|
"preferences": "Preferences",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"program": "Program",
|
"program": "Program",
|
||||||
|
"reason": "Reason",
|
||||||
"recorded": "Recorded",
|
"recorded": "Recorded",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"refresh_device": "Refresh Device",
|
"refresh_device": "Refresh Device",
|
||||||
@@ -170,12 +179,14 @@
|
|||||||
"show_all": "Show All",
|
"show_all": "Show All",
|
||||||
"socket_connection_closed": "Connection closed!",
|
"socket_connection_closed": "Connection closed!",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
|
"status": "Status",
|
||||||
"stop_editing": "Stop Editing",
|
"stop_editing": "Stop Editing",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"submitted": "Submitted",
|
"submitted": "Submitted",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
|
"time_per_device": "Devices/Second",
|
||||||
"timestamp": "Time",
|
"timestamp": "Time",
|
||||||
"to": "To",
|
"to": "To",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -190,6 +201,7 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendors",
|
"vendors": "Vendors",
|
||||||
"view_more": "View more",
|
"view_more": "View more",
|
||||||
|
"visibility": "Visibility",
|
||||||
"yes": "Yes"
|
"yes": "Yes"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +222,8 @@
|
|||||||
"creation_success": "Configuration successfully created!",
|
"creation_success": "Configuration successfully created!",
|
||||||
"currently_associated": "Currently Associated Configuration: {{config}}",
|
"currently_associated": "Currently Associated Configuration: {{config}}",
|
||||||
"currently_selected_config": "Currently Selected Configuration: {{config}}",
|
"currently_selected_config": "Currently Selected Configuration: {{config}}",
|
||||||
|
"default_configs": "Default Configs",
|
||||||
|
"default_configurations": "Default Configurations",
|
||||||
"delete_config": "Delete Config",
|
"delete_config": "Delete Config",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"device_password": "Password",
|
"device_password": "Password",
|
||||||
@@ -218,6 +232,7 @@
|
|||||||
"devices_affected": "Devices affected by this configuration: ",
|
"devices_affected": "Devices affected by this configuration: ",
|
||||||
"edit_configuration": "Edit Configuration",
|
"edit_configuration": "Edit Configuration",
|
||||||
"error_delete": "Error while trying to delete: {{error}}",
|
"error_delete": "Error while trying to delete: {{error}}",
|
||||||
|
"error_delete_blacklist": "Error deleting from blacklist: {{error}}",
|
||||||
"error_fetching_config": "Error while fetching configuration",
|
"error_fetching_config": "Error while fetching configuration",
|
||||||
"error_trying_delete": "Error while trying to delete: {{error}}",
|
"error_trying_delete": "Error while trying to delete: {{error}}",
|
||||||
"error_update": "Error: {{error}}",
|
"error_update": "Error: {{error}}",
|
||||||
@@ -261,6 +276,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "Access PIN",
|
"access_pin": "Access PIN",
|
||||||
"add_contact": "Add Contact",
|
"add_contact": "Add Contact",
|
||||||
|
"contact": "Contact",
|
||||||
"create_contact": "Create Contact",
|
"create_contact": "Create Contact",
|
||||||
"currently_selected_contact": "Currently Selected Contact: {{contact}}",
|
"currently_selected_contact": "Currently Selected Contact: {{contact}}",
|
||||||
"delete": "Delete Contact?",
|
"delete": "Delete Contact?",
|
||||||
@@ -300,12 +316,23 @@
|
|||||||
"healthchecks_title": "Delete Healthchecks"
|
"healthchecks_title": "Delete Healthchecks"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Add Device To Blacklist",
|
||||||
|
"all_devices": "All Devices",
|
||||||
|
"blacklisted_on": "Date",
|
||||||
|
"capabilities": "Capabilities",
|
||||||
"certificate_explanation": "Certificates of connected devices",
|
"certificate_explanation": "Certificates of connected devices",
|
||||||
|
"edit_blacklist": "Edit Blacklisted Device",
|
||||||
|
"error_adding_blacklist": "Error adding device to blacklist: {{error}}",
|
||||||
|
"error_edit_blacklist": "Error editing blacklist: {{error}}",
|
||||||
"error_fetching_device": "Error fetching device information: {{error}}",
|
"error_fetching_device": "Error fetching device information: {{error}}",
|
||||||
"error_fetching_devices": "Error while fetching devices: {{error}}",
|
"error_fetching_devices": "Error while fetching devices: {{error}}",
|
||||||
"health_explanation": "Health of connected devices",
|
"health_explanation": "Health of connected devices ((Devices=100% * 100 + Devices>90% * 95 + Devices>60% * 75 + Devices<60% * 35) / ConnectedDevices)",
|
||||||
"memory_explanation": "Memory used by connected devices",
|
"memory_explanation": "Amount of connected devices with corresponding memory used percentage",
|
||||||
"uptimes_explanation": "Time connected devices have been up and connected"
|
"remove_from_blacklist": "Remove from blacklist",
|
||||||
|
"success_added_blacklist": "Device successfully added to blacklist!",
|
||||||
|
"success_edit_blacklist": "Successfully edited blacklist!",
|
||||||
|
"success_removed_blacklist": "Successfully removed device from blacklist!",
|
||||||
|
"uptimes_explanation": "Amount of devices connected based on their uptime"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
@@ -320,23 +347,32 @@
|
|||||||
"add_success": "Entity Successfully Created!",
|
"add_success": "Entity Successfully Created!",
|
||||||
"assigned_inventory": "Assigned Inventory",
|
"assigned_inventory": "Assigned Inventory",
|
||||||
"cannot_delete": "You cannot delete entities which have children. Delete this entity's children to be able to delete it.",
|
"cannot_delete": "You cannot delete entities which have children. Delete this entity's children to be able to delete it.",
|
||||||
|
"confirm_map_delete": "Are you sure you want to delete the map {{name}}? This action cannot be reverted",
|
||||||
"currently_selected_entity": "Currently Selected Entity: {{config}}",
|
"currently_selected_entity": "Currently Selected Entity: {{config}}",
|
||||||
"currently_selected_venue": "Currently Selected Venue: {{config}}",
|
"currently_selected_venue": "Currently Selected Venue: {{config}}",
|
||||||
"delete_success": "Entity Successfully Deleted",
|
"delete_success": "Entity Successfully Deleted",
|
||||||
"delete_warning": "Warning: this operation cannot be reverted",
|
"delete_warning": "Warning: this operation cannot be reverted",
|
||||||
|
"duplicate_from_node": "Duplicate with specific Root Node",
|
||||||
|
"duplicate_map": "Duplicate Map",
|
||||||
|
"duplicate_with_node": "Duplicate {{mapName}} with {{rootName}} as root node",
|
||||||
"edit_failure": "Update unsuccessful : {{error}}",
|
"edit_failure": "Update unsuccessful : {{error}}",
|
||||||
"enter_here": "Enter the IP(s) you'd like to add here",
|
"enter_here": "Enter the IP(s) you'd like to add here",
|
||||||
"entire_tree": "Site Map",
|
"entire_tree": "Network Map",
|
||||||
"entities": "Entities",
|
"entities": "Entities",
|
||||||
"entity": "Entity",
|
"entity": "Entity",
|
||||||
|
"error_deleting_map": "Error deleting map: {{error}}",
|
||||||
"error_fetch_entity": "Error while fetching entity information",
|
"error_fetch_entity": "Error while fetching entity information",
|
||||||
"error_fetching": "Error while fetching entities",
|
"error_fetching": "Error while fetching entities",
|
||||||
"error_fetching_map": "Error fetching map: {{error}}",
|
"error_fetching_map": "Error fetching map: {{error}}",
|
||||||
|
"error_fetching_tree": "Error while fetching tree: {{error}}",
|
||||||
"error_saving": "Error while saving entity",
|
"error_saving": "Error while saving entity",
|
||||||
|
"error_saving_map": "Error saving map: {{error}}",
|
||||||
"higher_priority": "Make Higher Priority",
|
"higher_priority": "Make Higher Priority",
|
||||||
"ip_detection": "IP Detection",
|
"ip_detection": "IP Detection",
|
||||||
"ip_formats": "You can add IPv4 or IPv6 addresses in the following formats:",
|
"ip_formats": "You can add IPv4 or IPv6 addresses in the following formats:",
|
||||||
"lower_priority": "Make Lower Priority",
|
"lower_priority": "Make Lower Priority",
|
||||||
|
"map": "Map",
|
||||||
|
"map_delete_success": "Map Successfully Deleted!",
|
||||||
"need_select_entity": "You need to select an entity from the table below",
|
"need_select_entity": "You need to select an entity from the table below",
|
||||||
"no_ips": "No IPs selected",
|
"no_ips": "No IPs selected",
|
||||||
"not_assigned": "Not Assigned",
|
"not_assigned": "Not Assigned",
|
||||||
@@ -344,6 +380,7 @@
|
|||||||
"select_entity": "Select this Entity",
|
"select_entity": "Select this Entity",
|
||||||
"selected_entity": "Selected Entity",
|
"selected_entity": "Selected Entity",
|
||||||
"selected_map": "Selected Map",
|
"selected_map": "Selected Map",
|
||||||
|
"tree_saved": "Map Successfully Saved!",
|
||||||
"update_failure_error": "Error while trying to update entity: {{error}}",
|
"update_failure_error": "Error while trying to update entity: {{error}}",
|
||||||
"valid_serial": "Needs to be a valid serial number (12 HEX characters)",
|
"valid_serial": "Needs to be a valid serial number (12 HEX characters)",
|
||||||
"venues": "Venues"
|
"venues": "Venues"
|
||||||
@@ -541,6 +578,9 @@
|
|||||||
"verification_code": "Enter your verification here",
|
"verification_code": "Enter your verification here",
|
||||||
"wrong_code": "The verification code that was entered is not valid. "
|
"wrong_code": "The verification code that was entered is not valid. "
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Provisioning"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "When would you like to reboot this device?",
|
"directions": "When would you like to reboot this device?",
|
||||||
"now": "Would you like to reboot this device now?",
|
"now": "Would you like to reboot this device now?",
|
||||||
@@ -584,7 +624,7 @@
|
|||||||
"mac_prefix": "MAC Prefix",
|
"mac_prefix": "MAC Prefix",
|
||||||
"max_associations": "Max. Associations",
|
"max_associations": "Max. Associations",
|
||||||
"max_clients": "Max. Clients",
|
"max_clients": "Max. Clients",
|
||||||
"messages_transmitted": "Messages Transmitted",
|
"messages_transmitted": "Msgs TX",
|
||||||
"min_associations": "Min. Associations",
|
"min_associations": "Min. Associations",
|
||||||
"min_clients": "Min. Clients",
|
"min_clients": "Min. Clients",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
@@ -592,7 +632,7 @@
|
|||||||
"prefix_length": "Required, needs to be of a length of 6 characters",
|
"prefix_length": "Required, needs to be of a length of 6 characters",
|
||||||
"previous_runs": "Previous Runs",
|
"previous_runs": "Previous Runs",
|
||||||
"received": "Received",
|
"received": "Received",
|
||||||
"received_messages": "Messages Received",
|
"received_messages": "Msgs RX",
|
||||||
"reconnect_interval": "Reconnect Interval",
|
"reconnect_interval": "Reconnect Interval",
|
||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
"resume_success": "Run Resumed!",
|
"resume_success": "Run Resumed!",
|
||||||
@@ -634,6 +674,19 @@
|
|||||||
"uptime": "Uptime",
|
"uptime": "Uptime",
|
||||||
"used_total_memory": "{{used}} used / {{total}} total "
|
"used_total_memory": "{{used}} used / {{total}} total "
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"create": "Create Subscriber",
|
||||||
|
"edit": "Edit Subscriber",
|
||||||
|
"error_create": "Error creating subscriber: {{error}}",
|
||||||
|
"error_delete": "Error deleting subscriber: {{error}}",
|
||||||
|
"error_fetching": "Error fetching subscribers: {{error}}",
|
||||||
|
"error_fetching_single": "Error fetching subscriber: {{error}}",
|
||||||
|
"error_update": "Error updating subscriber: {{error}}",
|
||||||
|
"subscribers": "Subscribers",
|
||||||
|
"success_create": "Subscriber successfully created!",
|
||||||
|
"success_delete": "Subscriber successfully deleted!",
|
||||||
|
"success_update": "Successfully updated subscriber!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Error while fetching system information",
|
"error_fetching": "Error while fetching system information",
|
||||||
"error_reloading": "Error while reloading: {{error}}",
|
"error_reloading": "Error while reloading: {{error}}",
|
||||||
@@ -723,6 +776,7 @@
|
|||||||
"send_code_again": "Send Code Again",
|
"send_code_again": "Send Code Again",
|
||||||
"show_hide_password": "Show/Hide Password",
|
"show_hide_password": "Show/Hide Password",
|
||||||
"successful_validation": "Phone Number Validated! Click the save button to link it to your profile",
|
"successful_validation": "Phone Number Validated! Click the save button to link it to your profile",
|
||||||
|
"table_title": "Admin Users",
|
||||||
"update_failure": "Error while trying to update: {{error}}",
|
"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",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Parpadeo",
|
"blink": "Parpadeo",
|
||||||
"device_leds": "LED de dispositivo",
|
"device_leds": "LED de dispositivo",
|
||||||
"execute_now": "¿Le gustaría establecer este patrón ahora?",
|
"execute_now": "¿Le gustaría establecer este patrón ahora?",
|
||||||
|
"explanation": "¿Qué patrón le gustaría establecer en este dispositivo durante 30 segundos?",
|
||||||
"pattern": "Elija el patrón que le gustaría usar:",
|
"pattern": "Elija el patrón que le gustaría usar:",
|
||||||
"set_leds": "Establecer LED",
|
"set_leds": "Establecer LED",
|
||||||
"when_blink_leds": "¿Cuándo desea que los LED del dispositivo parpadeen?"
|
"when_blink_leds": "¿Cuándo desea que los LED del dispositivo parpadeen?"
|
||||||
@@ -37,9 +38,11 @@
|
|||||||
"add_note": "Añadir la nota",
|
"add_note": "Añadir la nota",
|
||||||
"add_note_explanation": "Escriba su nueva nota a continuación y haga clic en el botón '+' donde haya terminado",
|
"add_note_explanation": "Escriba su nueva nota a continuación y haga clic en el botón '+' donde haya terminado",
|
||||||
"adding_ellipsis": "Añadiendo ...",
|
"adding_ellipsis": "Añadiendo ...",
|
||||||
|
"all": "TODOS",
|
||||||
"are_you_sure": "¿Estás seguro?",
|
"are_you_sure": "¿Estás seguro?",
|
||||||
"back_to_login": "Atrás para iniciar sesión",
|
"back_to_login": "Atrás para iniciar sesión",
|
||||||
"back_to_start": "volver a empezar",
|
"back_to_start": "volver a empezar",
|
||||||
|
"blacklist": "Lista negra",
|
||||||
"by": "Por",
|
"by": "Por",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"certificate": "Certificado",
|
"certificate": "Certificado",
|
||||||
@@ -62,12 +65,14 @@
|
|||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
"created": "creado",
|
"created": "creado",
|
||||||
"created_by": "Creado por",
|
"created_by": "Creado por",
|
||||||
|
"creator": "Creador",
|
||||||
"current": "Corriente",
|
"current": "Corriente",
|
||||||
"custom_date": "Fecha personalizada",
|
"custom_date": "Fecha personalizada",
|
||||||
"dashboard": "Tablero",
|
"dashboard": "Tablero",
|
||||||
"date": "Fecha",
|
"date": "Fecha",
|
||||||
"day": "día",
|
"day": "día",
|
||||||
"days": "días",
|
"days": "días",
|
||||||
|
"default_map": "Mapa predeterminado",
|
||||||
"delete": "Borrar",
|
"delete": "Borrar",
|
||||||
"delete_device": "Eliminar dispositivo",
|
"delete_device": "Eliminar dispositivo",
|
||||||
"details": "Detalles",
|
"details": "Detalles",
|
||||||
@@ -85,6 +90,7 @@
|
|||||||
"dismiss": "Despedir",
|
"dismiss": "Despedir",
|
||||||
"do_now": "¡Hagan ahora!",
|
"do_now": "¡Hagan ahora!",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
|
"duplicate": "Duplicar",
|
||||||
"duration": "Duración",
|
"duration": "Duración",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"edit_user": "Editar",
|
"edit_user": "Editar",
|
||||||
@@ -94,6 +100,7 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_adding_note": "Error al agregar una nota",
|
"error_adding_note": "Error al agregar una nota",
|
||||||
"error_code": "código de error",
|
"error_code": "código de error",
|
||||||
|
"errors": "Los errores",
|
||||||
"execute_now": "¿Le gustaría ejecutar este comando ahora?",
|
"execute_now": "¿Le gustaría ejecutar este comando ahora?",
|
||||||
"executed": "ejecutado",
|
"executed": "ejecutado",
|
||||||
"exit": "salida",
|
"exit": "salida",
|
||||||
@@ -142,14 +149,16 @@
|
|||||||
"no_items": "No hay articulos",
|
"no_items": "No hay articulos",
|
||||||
"none": "Ninguna",
|
"none": "Ninguna",
|
||||||
"not_connected": "No conectado",
|
"not_connected": "No conectado",
|
||||||
"of_connected": "% de dispositivos",
|
"of_connected": "% de dispositivos conectados",
|
||||||
"off": "Apagado",
|
"off": "Apagado",
|
||||||
"on": "en",
|
"on": "en",
|
||||||
"optional": "Opcional",
|
"optional": "Opcional",
|
||||||
"overall_health": "Salud en general",
|
"overall_health": "Salud en general",
|
||||||
"password_policy": "Política de contraseñas",
|
"password_policy": "Política de contraseñas",
|
||||||
|
"preferences": "Preferencias",
|
||||||
"preview": "Avance",
|
"preview": "Avance",
|
||||||
"program": "Programa",
|
"program": "Programa",
|
||||||
|
"reason": "Razón",
|
||||||
"recorded": "Grabado",
|
"recorded": "Grabado",
|
||||||
"refresh": "Refrescar",
|
"refresh": "Refrescar",
|
||||||
"refresh_device": "Actualizar dispositivo",
|
"refresh_device": "Actualizar dispositivo",
|
||||||
@@ -170,12 +179,14 @@
|
|||||||
"show_all": "Mostrar todo",
|
"show_all": "Mostrar todo",
|
||||||
"socket_connection_closed": "¡Conexión cerrada!",
|
"socket_connection_closed": "¡Conexión cerrada!",
|
||||||
"start": "comienzo",
|
"start": "comienzo",
|
||||||
|
"status": "Estado",
|
||||||
"stop_editing": "Dejar de editar",
|
"stop_editing": "Dejar de editar",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"submitted": "Presentado",
|
"submitted": "Presentado",
|
||||||
"success": "Éxito",
|
"success": "Éxito",
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"table": "Mesa",
|
"table": "Mesa",
|
||||||
|
"time_per_device": "Dispositivo / segundo",
|
||||||
"timestamp": "hora",
|
"timestamp": "hora",
|
||||||
"to": "a",
|
"to": "a",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
@@ -190,6 +201,7 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendedores",
|
"vendors": "Vendedores",
|
||||||
"view_more": "Ver más",
|
"view_more": "Ver más",
|
||||||
|
"visibility": "Visibilidad",
|
||||||
"yes": "Sí"
|
"yes": "Sí"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +222,8 @@
|
|||||||
"creation_success": "¡Configuración creada con éxito!",
|
"creation_success": "¡Configuración creada con éxito!",
|
||||||
"currently_associated": "Configuración asociada actual: {{config}}",
|
"currently_associated": "Configuración asociada actual: {{config}}",
|
||||||
"currently_selected_config": "Configuración seleccionada actualmente: {{config}}",
|
"currently_selected_config": "Configuración seleccionada actualmente: {{config}}",
|
||||||
|
"default_configs": "Configuraciones predeterminadas",
|
||||||
|
"default_configurations": "Configuraciones predeterminadas",
|
||||||
"delete_config": "Eliminar Configuración",
|
"delete_config": "Eliminar Configuración",
|
||||||
"details": "Detalles",
|
"details": "Detalles",
|
||||||
"device_password": "Contraseña",
|
"device_password": "Contraseña",
|
||||||
@@ -218,6 +232,7 @@
|
|||||||
"devices_affected": "Dispositivos afectados por esta configuración:",
|
"devices_affected": "Dispositivos afectados por esta configuración:",
|
||||||
"edit_configuration": "Editar configuración",
|
"edit_configuration": "Editar configuración",
|
||||||
"error_delete": "Error al intentar eliminar: {{error}}",
|
"error_delete": "Error al intentar eliminar: {{error}}",
|
||||||
|
"error_delete_blacklist": "Error al eliminar de la lista negra: {{error}}",
|
||||||
"error_fetching_config": "Error al obtener la configuración",
|
"error_fetching_config": "Error al obtener la configuración",
|
||||||
"error_trying_delete": "Error al intentar eliminar: {{error}}",
|
"error_trying_delete": "Error al intentar eliminar: {{error}}",
|
||||||
"error_update": "Error: {{error}}",
|
"error_update": "Error: {{error}}",
|
||||||
@@ -261,6 +276,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "PIN de acceso",
|
"access_pin": "PIN de acceso",
|
||||||
"add_contact": "Agregar contacto",
|
"add_contact": "Agregar contacto",
|
||||||
|
"contact": "Contacto",
|
||||||
"create_contact": "Crear contacto",
|
"create_contact": "Crear contacto",
|
||||||
"currently_selected_contact": "Contacto seleccionado actualmente: {{contact}}",
|
"currently_selected_contact": "Contacto seleccionado actualmente: {{contact}}",
|
||||||
"delete": "¿Borrar contacto?",
|
"delete": "¿Borrar contacto?",
|
||||||
@@ -300,12 +316,23 @@
|
|||||||
"healthchecks_title": "Eliminar comprobaciones de estado"
|
"healthchecks_title": "Eliminar comprobaciones de estado"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Agregar dispositivo a la lista negra",
|
||||||
|
"all_devices": "Todos los dispositivos",
|
||||||
|
"blacklisted_on": "Fecha",
|
||||||
|
"capabilities": "capacidades",
|
||||||
"certificate_explanation": "Certificados de dispositivos conectados",
|
"certificate_explanation": "Certificados de dispositivos conectados",
|
||||||
|
"edit_blacklist": "Editar dispositivo incluido en la lista negra",
|
||||||
|
"error_adding_blacklist": "Error al agregar el dispositivo a la lista negra: {{error}}",
|
||||||
|
"error_edit_blacklist": "Error al editar la lista negra: {{error}}",
|
||||||
"error_fetching_device": "Error al obtener la información del dispositivo: {{error}}",
|
"error_fetching_device": "Error al obtener la información del dispositivo: {{error}}",
|
||||||
"error_fetching_devices": "Error al recuperar dispositivos: {{error}}",
|
"error_fetching_devices": "Error al recuperar dispositivos: {{error}}",
|
||||||
"health_explanation": "Salud de los dispositivos conectados",
|
"health_explanation": "Estado de los dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos conectados)",
|
||||||
"memory_explanation": "Memoria utilizada por dispositivos conectados",
|
"memory_explanation": "Cantidad de dispositivos conectados con la memoria correspondiente utilizada%",
|
||||||
"uptimes_explanation": "Tiempo que los dispositivos conectados han estado en funcionamiento y conectados"
|
"remove_from_blacklist": "ELIMINAR DE LA LISTA NEGRA",
|
||||||
|
"success_added_blacklist": "¡Dispositivo agregado exitosamente a la lista negra!",
|
||||||
|
"success_edit_blacklist": "Lista negra editada con éxito!",
|
||||||
|
"success_removed_blacklist": "¡Dispositivo eliminado con éxito de la lista negra!",
|
||||||
|
"uptimes_explanation": "Cantidad de dispositivos conectados según su tiempo de actividad"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Iniciar sesión",
|
"log": "Iniciar sesión",
|
||||||
@@ -320,23 +347,32 @@
|
|||||||
"add_success": "¡Entidad creada con éxito!",
|
"add_success": "¡Entidad creada con éxito!",
|
||||||
"assigned_inventory": "Inventario asignado",
|
"assigned_inventory": "Inventario asignado",
|
||||||
"cannot_delete": "No puede eliminar entidades que tienen hijos. Elimina los hijos de esta entidad para poder eliminarla.",
|
"cannot_delete": "No puede eliminar entidades que tienen hijos. Elimina los hijos de esta entidad para poder eliminarla.",
|
||||||
|
"confirm_map_delete": "¿Está seguro de que desea eliminar el mapa {{name}}? Esta acción no se puede revertir",
|
||||||
"currently_selected_entity": "Entidad seleccionada actualmente: {{config}}",
|
"currently_selected_entity": "Entidad seleccionada actualmente: {{config}}",
|
||||||
"currently_selected_venue": "Lugar seleccionado actualmente: {{config}}",
|
"currently_selected_venue": "Lugar seleccionado actualmente: {{config}}",
|
||||||
"delete_success": "Entidad eliminada correctamente",
|
"delete_success": "Entidad eliminada correctamente",
|
||||||
"delete_warning": "Advertencia: esta operación no se puede revertir",
|
"delete_warning": "Advertencia: esta operación no se puede revertir",
|
||||||
|
"duplicate_from_node": "Duplicar con un nodo raíz específico",
|
||||||
|
"duplicate_map": "Mapa duplicado",
|
||||||
|
"duplicate_with_node": "Duplicar {{mapName}} con {{rootName}} como nodo raíz",
|
||||||
"edit_failure": "Actualización fallida: {{error}}",
|
"edit_failure": "Actualización fallida: {{error}}",
|
||||||
"enter_here": "Ingrese las IP que desea agregar aquí",
|
"enter_here": "Ingrese las IP que desea agregar aquí",
|
||||||
"entire_tree": "Site MAp",
|
"entire_tree": "Mapa de red",
|
||||||
"entities": "entidades",
|
"entities": "entidades",
|
||||||
"entity": "Entidad",
|
"entity": "Entidad",
|
||||||
|
"error_deleting_map": "Error al eliminar el mapa: {{error}}",
|
||||||
"error_fetch_entity": "Error al obtener la información de la entidad",
|
"error_fetch_entity": "Error al obtener la información de la entidad",
|
||||||
"error_fetching": "Error al recuperar entidades",
|
"error_fetching": "Error al recuperar entidades",
|
||||||
"error_fetching_map": "Error al obtener el mapa: {{error}}",
|
"error_fetching_map": "Error al obtener el mapa: {{error}}",
|
||||||
|
"error_fetching_tree": "Error al obtener el árbol: {{error}}",
|
||||||
"error_saving": "Error al guardar la entidad",
|
"error_saving": "Error al guardar la entidad",
|
||||||
|
"error_saving_map": "Error al guardar el mapa: {{error}}",
|
||||||
"higher_priority": "Dar mayor prioridad",
|
"higher_priority": "Dar mayor prioridad",
|
||||||
"ip_detection": "Detección de IP",
|
"ip_detection": "Detección de IP",
|
||||||
"ip_formats": "Puede agregar direcciones IPv4 o IPv6 en los siguientes formatos:",
|
"ip_formats": "Puede agregar direcciones IPv4 o IPv6 en los siguientes formatos:",
|
||||||
"lower_priority": "Hacer una prioridad más baja",
|
"lower_priority": "Hacer una prioridad más baja",
|
||||||
|
"map": "Mapa",
|
||||||
|
"map_delete_success": "¡Mapa eliminado correctamente!",
|
||||||
"need_select_entity": "Debe seleccionar una entidad de la siguiente tabla",
|
"need_select_entity": "Debe seleccionar una entidad de la siguiente tabla",
|
||||||
"no_ips": "No se seleccionaron direcciones IP",
|
"no_ips": "No se seleccionaron direcciones IP",
|
||||||
"not_assigned": "No asignado",
|
"not_assigned": "No asignado",
|
||||||
@@ -344,6 +380,7 @@
|
|||||||
"select_entity": "Seleccione esta entidad",
|
"select_entity": "Seleccione esta entidad",
|
||||||
"selected_entity": "Entidad seleccionada",
|
"selected_entity": "Entidad seleccionada",
|
||||||
"selected_map": "Mapa seleccionado",
|
"selected_map": "Mapa seleccionado",
|
||||||
|
"tree_saved": "¡Mapa guardado con éxito!",
|
||||||
"update_failure_error": "Error al intentar actualizar la entidad: {{error}}",
|
"update_failure_error": "Error al intentar actualizar la entidad: {{error}}",
|
||||||
"valid_serial": "Debe ser un número de serie válido (12 caracteres HEX)",
|
"valid_serial": "Debe ser un número de serie válido (12 caracteres HEX)",
|
||||||
"venues": "Sedes"
|
"venues": "Sedes"
|
||||||
@@ -541,6 +578,9 @@
|
|||||||
"verification_code": "Ingrese su verificación aquí",
|
"verification_code": "Ingrese su verificación aquí",
|
||||||
"wrong_code": "El código de verificación que se ingresó no es válido."
|
"wrong_code": "El código de verificación que se ingresó no es válido."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Aprovisionamiento"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "¿Cuándo le gustaría reiniciar este dispositivo?",
|
"directions": "¿Cuándo le gustaría reiniciar este dispositivo?",
|
||||||
"now": "¿Le gustaría reiniciar este dispositivo ahora?",
|
"now": "¿Le gustaría reiniciar este dispositivo ahora?",
|
||||||
@@ -584,7 +624,7 @@
|
|||||||
"mac_prefix": "Prefijo MAC",
|
"mac_prefix": "Prefijo MAC",
|
||||||
"max_associations": "Max. Asociaciones",
|
"max_associations": "Max. Asociaciones",
|
||||||
"max_clients": "Max. Clientela",
|
"max_clients": "Max. Clientela",
|
||||||
"messages_transmitted": "Mensajes transmitidos",
|
"messages_transmitted": "Mensajes TX",
|
||||||
"min_associations": "Min. Asociaciones",
|
"min_associations": "Min. Asociaciones",
|
||||||
"min_clients": "Min. Clientela",
|
"min_clients": "Min. Clientela",
|
||||||
"pause": "pausa",
|
"pause": "pausa",
|
||||||
@@ -592,7 +632,7 @@
|
|||||||
"prefix_length": "Obligatorio, debe tener una longitud de 6 caracteres",
|
"prefix_length": "Obligatorio, debe tener una longitud de 6 caracteres",
|
||||||
"previous_runs": "Ejecuciones anteriores",
|
"previous_runs": "Ejecuciones anteriores",
|
||||||
"received": "recibido",
|
"received": "recibido",
|
||||||
"received_messages": "Mensajes recibidos",
|
"received_messages": "Msgs RX",
|
||||||
"reconnect_interval": "Intervalo de reconexión",
|
"reconnect_interval": "Intervalo de reconexión",
|
||||||
"resume": "Currículum",
|
"resume": "Currículum",
|
||||||
"resume_success": "¡Ejecutar reanudado!",
|
"resume_success": "¡Ejecutar reanudado!",
|
||||||
@@ -634,6 +674,19 @@
|
|||||||
"uptime": "Tiempo de actividad",
|
"uptime": "Tiempo de actividad",
|
||||||
"used_total_memory": "{{used}} usado / {{total}} total"
|
"used_total_memory": "{{used}} usado / {{total}} total"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"create": "Crear suscriptor",
|
||||||
|
"edit": "Editar suscriptor",
|
||||||
|
"error_create": "Error al crear el suscriptor: {{error}}",
|
||||||
|
"error_delete": "Error al eliminar el suscriptor: {{error}}",
|
||||||
|
"error_fetching": "Error al obtener suscriptores: {{error}}",
|
||||||
|
"error_fetching_single": "Error al obtener el suscriptor: {{error}}",
|
||||||
|
"error_update": "Error al actualizar el suscriptor: {{error}}",
|
||||||
|
"subscribers": "Suscriptores",
|
||||||
|
"success_create": "¡Suscriptor creado correctamente!",
|
||||||
|
"success_delete": "¡Suscriptor eliminado correctamente!",
|
||||||
|
"success_update": "Suscriptor actualizado con éxito!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Error al obtener información del sistema",
|
"error_fetching": "Error al obtener información del sistema",
|
||||||
"error_reloading": "Error al recargar: {{error}}",
|
"error_reloading": "Error al recargar: {{error}}",
|
||||||
@@ -723,6 +776,7 @@
|
|||||||
"send_code_again": "Enviar Código De nuevo",
|
"send_code_again": "Enviar Código De nuevo",
|
||||||
"show_hide_password": "Mostrar / Ocultar contraseña",
|
"show_hide_password": "Mostrar / Ocultar contraseña",
|
||||||
"successful_validation": "¡Número de teléfono validado! Haga clic en el botón guardar para vincularlo a su perfil",
|
"successful_validation": "¡Número de teléfono validado! Haga clic en el botón guardar para vincularlo a su perfil",
|
||||||
|
"table_title": "Usuarios administrativos",
|
||||||
"update_failure": "Error al intentar actualizar: {{error}}",
|
"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",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Cligner",
|
"blink": "Cligner",
|
||||||
"device_leds": "LED de l'appareil",
|
"device_leds": "LED de l'appareil",
|
||||||
"execute_now": "Souhaitez-vous définir ce modèle maintenant ?",
|
"execute_now": "Souhaitez-vous définir ce modèle maintenant ?",
|
||||||
|
"explanation": "Quel modèle souhaitez-vous définir sur cet appareil pendant 30 secondes ?",
|
||||||
"pattern": "Choisissez le modèle que vous souhaitez utiliser :",
|
"pattern": "Choisissez le modèle que vous souhaitez utiliser :",
|
||||||
"set_leds": "Définir les LED",
|
"set_leds": "Définir les LED",
|
||||||
"when_blink_leds": "Quand souhaitez-vous faire clignoter les LED de l'appareil ?"
|
"when_blink_leds": "Quand souhaitez-vous faire clignoter les LED de l'appareil ?"
|
||||||
@@ -37,9 +38,11 @@
|
|||||||
"add_note": "Ajouter une note",
|
"add_note": "Ajouter une note",
|
||||||
"add_note_explanation": "Écrivez votre nouvelle note ci-dessous et cliquez sur le bouton '+' où vous avez terminé",
|
"add_note_explanation": "Écrivez votre nouvelle note ci-dessous et cliquez sur le bouton '+' où vous avez terminé",
|
||||||
"adding_ellipsis": "Ajouter...",
|
"adding_ellipsis": "Ajouter...",
|
||||||
|
"all": "Tout",
|
||||||
"are_you_sure": "Êtes-vous sûr?",
|
"are_you_sure": "Êtes-vous sûr?",
|
||||||
"back_to_login": "Retour connexion",
|
"back_to_login": "Retour connexion",
|
||||||
"back_to_start": "Retour au début",
|
"back_to_start": "Retour au début",
|
||||||
|
"blacklist": "Liste noire",
|
||||||
"by": "Par",
|
"by": "Par",
|
||||||
"cancel": "annuler",
|
"cancel": "annuler",
|
||||||
"certificate": "Certificat",
|
"certificate": "Certificat",
|
||||||
@@ -62,12 +65,14 @@
|
|||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
"created": "Créé",
|
"created": "Créé",
|
||||||
"created_by": "Créé par",
|
"created_by": "Créé par",
|
||||||
|
"creator": "Créateur",
|
||||||
"current": "Actuel",
|
"current": "Actuel",
|
||||||
"custom_date": "Date personnalisée",
|
"custom_date": "Date personnalisée",
|
||||||
"dashboard": "Tableau de bord",
|
"dashboard": "Tableau de bord",
|
||||||
"date": "Rendez-vous amoureux",
|
"date": "Rendez-vous amoureux",
|
||||||
"day": "journée",
|
"day": "journée",
|
||||||
"days": "journées",
|
"days": "journées",
|
||||||
|
"default_map": "Carte par défaut",
|
||||||
"delete": "Effacer",
|
"delete": "Effacer",
|
||||||
"delete_device": "Supprimer le périphérique",
|
"delete_device": "Supprimer le périphérique",
|
||||||
"details": "Détails",
|
"details": "Détails",
|
||||||
@@ -85,6 +90,7 @@
|
|||||||
"dismiss": "Rejeter",
|
"dismiss": "Rejeter",
|
||||||
"do_now": "Faire maintenant!",
|
"do_now": "Faire maintenant!",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
|
"duplicate": "Dupliquer",
|
||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"edit": "modifier",
|
"edit": "modifier",
|
||||||
"edit_user": "Modifier",
|
"edit_user": "Modifier",
|
||||||
@@ -94,6 +100,7 @@
|
|||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"error_adding_note": "Erreur lors de l'ajout de la note",
|
"error_adding_note": "Erreur lors de l'ajout de la note",
|
||||||
"error_code": "Code d'erreur",
|
"error_code": "Code d'erreur",
|
||||||
|
"errors": "les erreurs",
|
||||||
"execute_now": "Souhaitez-vous exécuter cette commande maintenant ?",
|
"execute_now": "Souhaitez-vous exécuter cette commande maintenant ?",
|
||||||
"executed": "réalisé",
|
"executed": "réalisé",
|
||||||
"exit": "Sortie",
|
"exit": "Sortie",
|
||||||
@@ -142,14 +149,16 @@
|
|||||||
"no_items": "Pas d'objet",
|
"no_items": "Pas d'objet",
|
||||||
"none": "Aucun",
|
"none": "Aucun",
|
||||||
"not_connected": "Pas connecté",
|
"not_connected": "Pas connecté",
|
||||||
"of_connected": "% d'appareils",
|
"of_connected": "% d'appareils connectés",
|
||||||
"off": "De",
|
"off": "De",
|
||||||
"on": "sur",
|
"on": "sur",
|
||||||
"optional": "Optionnel",
|
"optional": "Optionnel",
|
||||||
"overall_health": "Santé globale",
|
"overall_health": "Santé globale",
|
||||||
"password_policy": "Politique de mot de passe",
|
"password_policy": "Politique de mot de passe",
|
||||||
|
"preferences": "Préférences",
|
||||||
"preview": "Aperçu",
|
"preview": "Aperçu",
|
||||||
"program": "Programme",
|
"program": "Programme",
|
||||||
|
"reason": "raison",
|
||||||
"recorded": "Enregistré",
|
"recorded": "Enregistré",
|
||||||
"refresh": "Rafraîchir",
|
"refresh": "Rafraîchir",
|
||||||
"refresh_device": "Actualiser l'appareil",
|
"refresh_device": "Actualiser l'appareil",
|
||||||
@@ -170,12 +179,14 @@
|
|||||||
"show_all": "Montre tout",
|
"show_all": "Montre tout",
|
||||||
"socket_connection_closed": "Connexion fermée !",
|
"socket_connection_closed": "Connexion fermée !",
|
||||||
"start": "Début",
|
"start": "Début",
|
||||||
|
"status": "Statut",
|
||||||
"stop_editing": "Arrêter la modification",
|
"stop_editing": "Arrêter la modification",
|
||||||
"submit": "Soumettre",
|
"submit": "Soumettre",
|
||||||
"submitted": "Soumis",
|
"submitted": "Soumis",
|
||||||
"success": "Succès",
|
"success": "Succès",
|
||||||
"system": "Système",
|
"system": "Système",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
|
"time_per_device": "Appareils/Seconde",
|
||||||
"timestamp": "Temps",
|
"timestamp": "Temps",
|
||||||
"to": "à",
|
"to": "à",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -190,6 +201,7 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendeurs",
|
"vendors": "Vendeurs",
|
||||||
"view_more": "Afficher plus",
|
"view_more": "Afficher plus",
|
||||||
|
"visibility": "Visibilité",
|
||||||
"yes": "Oui"
|
"yes": "Oui"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +222,8 @@
|
|||||||
"creation_success": "Configuration créée avec succès !",
|
"creation_success": "Configuration créée avec succès !",
|
||||||
"currently_associated": "Configuration associée actuelle : {{config}}",
|
"currently_associated": "Configuration associée actuelle : {{config}}",
|
||||||
"currently_selected_config": "Configuration actuellement sélectionnée : {{config}}",
|
"currently_selected_config": "Configuration actuellement sélectionnée : {{config}}",
|
||||||
|
"default_configs": "Configurations par défaut",
|
||||||
|
"default_configurations": "Configurations par défaut",
|
||||||
"delete_config": "Supprimer la configuration",
|
"delete_config": "Supprimer la configuration",
|
||||||
"details": "Détails",
|
"details": "Détails",
|
||||||
"device_password": "Mot de passe",
|
"device_password": "Mot de passe",
|
||||||
@@ -218,6 +232,7 @@
|
|||||||
"devices_affected": "Appareils concernés par cette configuration :",
|
"devices_affected": "Appareils concernés par cette configuration :",
|
||||||
"edit_configuration": "Modifier la configuration",
|
"edit_configuration": "Modifier la configuration",
|
||||||
"error_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
"error_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
||||||
|
"error_delete_blacklist": "Erreur lors de la suppression de la liste noire : {{error}}",
|
||||||
"error_fetching_config": "Erreur lors de la récupération de la configuration",
|
"error_fetching_config": "Erreur lors de la récupération de la configuration",
|
||||||
"error_trying_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
"error_trying_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
||||||
"error_update": "Erreur: {{error}}",
|
"error_update": "Erreur: {{error}}",
|
||||||
@@ -261,6 +276,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "NIP d'accès",
|
"access_pin": "NIP d'accès",
|
||||||
"add_contact": "Ajouter le contact",
|
"add_contact": "Ajouter le contact",
|
||||||
|
"contact": "Contact",
|
||||||
"create_contact": "Créer un contact",
|
"create_contact": "Créer un contact",
|
||||||
"currently_selected_contact": "Contact actuellement sélectionné : {{contact}}",
|
"currently_selected_contact": "Contact actuellement sélectionné : {{contact}}",
|
||||||
"delete": "Effacer le contact?",
|
"delete": "Effacer le contact?",
|
||||||
@@ -300,12 +316,23 @@
|
|||||||
"healthchecks_title": "Supprimer les vérifications d'état"
|
"healthchecks_title": "Supprimer les vérifications d'état"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Ajouter un appareil à la liste noire",
|
||||||
|
"all_devices": "Tous les dispositifs",
|
||||||
|
"blacklisted_on": "Rendez-vous amoureux",
|
||||||
|
"capabilities": "Capacités",
|
||||||
"certificate_explanation": "Certificats des appareils connectés",
|
"certificate_explanation": "Certificats des appareils connectés",
|
||||||
|
"edit_blacklist": "Modifier l'appareil sur liste noire",
|
||||||
|
"error_adding_blacklist": "Erreur lors de l'ajout de l'appareil à la liste noire : {{error}}",
|
||||||
|
"error_edit_blacklist": "Erreur lors de la modification de la liste noire : {{error}}",
|
||||||
"error_fetching_device": "Erreur lors de la récupération des informations sur l'appareil : {{error}}",
|
"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}}",
|
"error_fetching_devices": "Erreur lors de la récupération des appareils : {{error}}",
|
||||||
"health_explanation": "Santé des appareils connectés",
|
"health_explanation": "Santé des appareils connectés ((Appareils = 100 % * 100 + Appareils> 90 % * 95 + Appareils> 60 % * 75 + Appareils < 60 % * 35) / Appareils connectés)",
|
||||||
"memory_explanation": "Mémoire utilisée par les appareils connectés",
|
"memory_explanation": "Nombre d'appareils connectés avec la mémoire correspondante utilisée %",
|
||||||
"uptimes_explanation": "Heure à laquelle les appareils connectés ont été activés et connectés"
|
"remove_from_blacklist": "Supprimer de la liste noire",
|
||||||
|
"success_added_blacklist": "Appareil ajouté avec succès à la liste noire !",
|
||||||
|
"success_edit_blacklist": "Liste noire modifiée avec succès !",
|
||||||
|
"success_removed_blacklist": "Appareil supprimé de la liste noire !",
|
||||||
|
"uptimes_explanation": "Nombre d'appareils connectés en fonction de leur disponibilité"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Bûche",
|
"log": "Bûche",
|
||||||
@@ -320,23 +347,32 @@
|
|||||||
"add_success": "Entité créée avec succès !",
|
"add_success": "Entité créée avec succès !",
|
||||||
"assigned_inventory": "Inventaire assigné",
|
"assigned_inventory": "Inventaire assigné",
|
||||||
"cannot_delete": "Vous ne pouvez pas supprimer des entités qui ont des enfants. Supprimez les enfants de cette entité pour pouvoir la supprimer.",
|
"cannot_delete": "Vous ne pouvez pas supprimer des entités qui ont des enfants. Supprimez les enfants de cette entité pour pouvoir la supprimer.",
|
||||||
|
"confirm_map_delete": "Êtes-vous sûr de vouloir supprimer la carte {{name}}? Cette action ne peut pas être annulée",
|
||||||
"currently_selected_entity": "Entité actuellement sélectionnée : {{config}}",
|
"currently_selected_entity": "Entité actuellement sélectionnée : {{config}}",
|
||||||
"currently_selected_venue": "Lieu actuellement sélectionné : {{config}}",
|
"currently_selected_venue": "Lieu actuellement sélectionné : {{config}}",
|
||||||
"delete_success": "Entité supprimée avec succès",
|
"delete_success": "Entité supprimée avec succès",
|
||||||
"delete_warning": "Attention : cette opération ne peut pas être annulée",
|
"delete_warning": "Attention : cette opération ne peut pas être annulée",
|
||||||
|
"duplicate_from_node": "Dupliquer avec un nœud racine spécifique",
|
||||||
|
"duplicate_map": "Carte en double",
|
||||||
|
"duplicate_with_node": "Dupliquer {{mapName}} avec {{rootName}} comme nœud racine",
|
||||||
"edit_failure": "Échec de la mise à jour : {{error}}",
|
"edit_failure": "Échec de la mise à jour : {{error}}",
|
||||||
"enter_here": "Entrez les IP que vous souhaitez ajouter ici",
|
"enter_here": "Entrez les IP que vous souhaitez ajouter ici",
|
||||||
"entire_tree": "Site MAp",
|
"entire_tree": "Carte du réseau",
|
||||||
"entities": "Entités",
|
"entities": "Entités",
|
||||||
"entity": "Entité",
|
"entity": "Entité",
|
||||||
|
"error_deleting_map": "Erreur lors de la suppression de la carte : {{error}}",
|
||||||
"error_fetch_entity": "Erreur lors de la récupération des informations sur l'entité",
|
"error_fetch_entity": "Erreur lors de la récupération des informations sur l'entité",
|
||||||
"error_fetching": "Erreur lors de la récupération des entités",
|
"error_fetching": "Erreur lors de la récupération des entités",
|
||||||
"error_fetching_map": "Erreur lors de la récupération de la carte : {{error}}",
|
"error_fetching_map": "Erreur lors de la récupération de la carte : {{error}}",
|
||||||
|
"error_fetching_tree": "Erreur lors de la récupération de l'arborescence : {{error}}",
|
||||||
"error_saving": "Erreur lors de l'enregistrement de l'entité",
|
"error_saving": "Erreur lors de l'enregistrement de l'entité",
|
||||||
|
"error_saving_map": "Erreur lors de l'enregistrement de la carte : {{error}}",
|
||||||
"higher_priority": "Faire une priorité plus élevée",
|
"higher_priority": "Faire une priorité plus élevée",
|
||||||
"ip_detection": "Détection IP",
|
"ip_detection": "Détection IP",
|
||||||
"ip_formats": "Vous pouvez ajouter des adresses IPv4 ou IPv6 aux formats suivants :",
|
"ip_formats": "Vous pouvez ajouter des adresses IPv4 ou IPv6 aux formats suivants :",
|
||||||
"lower_priority": "Faire une priorité inférieure",
|
"lower_priority": "Faire une priorité inférieure",
|
||||||
|
"map": "Carte",
|
||||||
|
"map_delete_success": "Carte supprimée avec succès !",
|
||||||
"need_select_entity": "Vous devez sélectionner une entité dans le tableau ci-dessous",
|
"need_select_entity": "Vous devez sélectionner une entité dans le tableau ci-dessous",
|
||||||
"no_ips": "Aucune adresse IP sélectionnée",
|
"no_ips": "Aucune adresse IP sélectionnée",
|
||||||
"not_assigned": "Non attribué",
|
"not_assigned": "Non attribué",
|
||||||
@@ -344,6 +380,7 @@
|
|||||||
"select_entity": "Sélectionnez cette entité",
|
"select_entity": "Sélectionnez cette entité",
|
||||||
"selected_entity": "Entité sélectionnée",
|
"selected_entity": "Entité sélectionnée",
|
||||||
"selected_map": "Carte sélectionnée",
|
"selected_map": "Carte sélectionnée",
|
||||||
|
"tree_saved": "Carte enregistrée avec succès !",
|
||||||
"update_failure_error": "Erreur lors de la tentative de mise à jour de l'entité : {{error}}",
|
"update_failure_error": "Erreur lors de la tentative de mise à jour de l'entité : {{error}}",
|
||||||
"valid_serial": "Doit être un numéro de série valide (12 caractères HEX)",
|
"valid_serial": "Doit être un numéro de série valide (12 caractères HEX)",
|
||||||
"venues": "Les lieux"
|
"venues": "Les lieux"
|
||||||
@@ -541,6 +578,9 @@
|
|||||||
"verification_code": "Entrez votre vérification ici",
|
"verification_code": "Entrez votre vérification ici",
|
||||||
"wrong_code": "Le code de vérification saisi n'est pas valide."
|
"wrong_code": "Le code de vérification saisi n'est pas valide."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Provisioning"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "Quand souhaitez-vous redémarrer cet appareil ?",
|
"directions": "Quand souhaitez-vous redémarrer cet appareil ?",
|
||||||
"now": "Souhaitez-vous redémarrer cet appareil maintenant ?",
|
"now": "Souhaitez-vous redémarrer cet appareil maintenant ?",
|
||||||
@@ -584,7 +624,7 @@
|
|||||||
"mac_prefix": "Préfixe MAC",
|
"mac_prefix": "Préfixe MAC",
|
||||||
"max_associations": "Max. Les associations",
|
"max_associations": "Max. Les associations",
|
||||||
"max_clients": "Max. Clients",
|
"max_clients": "Max. Clients",
|
||||||
"messages_transmitted": "Messages transmis",
|
"messages_transmitted": "Émission de messages",
|
||||||
"min_associations": "Min. Les associations",
|
"min_associations": "Min. Les associations",
|
||||||
"min_clients": "Min. Clients",
|
"min_clients": "Min. Clients",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
@@ -592,7 +632,7 @@
|
|||||||
"prefix_length": "Obligatoire, doit être d'une longueur de 6 caractères",
|
"prefix_length": "Obligatoire, doit être d'une longueur de 6 caractères",
|
||||||
"previous_runs": "Courses précédentes",
|
"previous_runs": "Courses précédentes",
|
||||||
"received": "reçu",
|
"received": "reçu",
|
||||||
"received_messages": "Messages reçus",
|
"received_messages": "Réception des messages",
|
||||||
"reconnect_interval": "Intervalle de reconnexion",
|
"reconnect_interval": "Intervalle de reconnexion",
|
||||||
"resume": "CV",
|
"resume": "CV",
|
||||||
"resume_success": "Exécution reprise !",
|
"resume_success": "Exécution reprise !",
|
||||||
@@ -634,6 +674,19 @@
|
|||||||
"uptime": "La disponibilité",
|
"uptime": "La disponibilité",
|
||||||
"used_total_memory": "{{used}} utilisé / {{total}} total"
|
"used_total_memory": "{{used}} utilisé / {{total}} total"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"create": "Créer un abonné",
|
||||||
|
"edit": "Modifier l'abonné",
|
||||||
|
"error_create": "Erreur lors de la création de l'abonné : {{error}}",
|
||||||
|
"error_delete": "Erreur lors de la suppression de l'abonné : {{error}}",
|
||||||
|
"error_fetching": "Erreur lors de la récupération des abonnés : {{error}}",
|
||||||
|
"error_fetching_single": "Erreur lors de la récupération de l'abonné : {{error}}",
|
||||||
|
"error_update": "Erreur lors de la mise à jour de l'abonné : {{error}}",
|
||||||
|
"subscribers": "Les abonnés",
|
||||||
|
"success_create": "Abonné créé avec succès !",
|
||||||
|
"success_delete": "Abonné supprimé avec succès !",
|
||||||
|
"success_update": "Abonné mis à jour avec succès !"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Erreur lors de la récupération des informations système",
|
"error_fetching": "Erreur lors de la récupération des informations système",
|
||||||
"error_reloading": "Erreur lors du rechargement : {{error}}",
|
"error_reloading": "Erreur lors du rechargement : {{error}}",
|
||||||
@@ -723,6 +776,7 @@
|
|||||||
"send_code_again": "Envoyer code à nouveau",
|
"send_code_again": "Envoyer code à nouveau",
|
||||||
"show_hide_password": "Afficher/Masquer le mot de passe",
|
"show_hide_password": "Afficher/Masquer le mot de passe",
|
||||||
"successful_validation": "Numéro de téléphone validé ! Cliquez sur le bouton Enregistrer pour le lier à votre profil",
|
"successful_validation": "Numéro de téléphone validé ! Cliquez sur le bouton Enregistrer pour le lier à votre profil",
|
||||||
|
"table_title": "Utilisateurs administrateurs",
|
||||||
"update_failure": "Erreur lors de la tentative de mise à jour : {{error}}",
|
"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",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Piscar",
|
"blink": "Piscar",
|
||||||
"device_leds": "LEDs do dispositivo",
|
"device_leds": "LEDs do dispositivo",
|
||||||
"execute_now": "Você gostaria de definir este padrão agora?",
|
"execute_now": "Você gostaria de definir este padrão agora?",
|
||||||
|
"explanation": "Que padrão você gostaria de definir neste dispositivo por 30 segundos?",
|
||||||
"pattern": "Escolha o padrão que deseja usar:",
|
"pattern": "Escolha o padrão que deseja usar:",
|
||||||
"set_leds": "Definir LEDs",
|
"set_leds": "Definir LEDs",
|
||||||
"when_blink_leds": "Quando você gostaria de fazer os LEDs do dispositivo piscarem?"
|
"when_blink_leds": "Quando você gostaria de fazer os LEDs do dispositivo piscarem?"
|
||||||
@@ -37,9 +38,11 @@
|
|||||||
"add_note": "Adicionar nota",
|
"add_note": "Adicionar nota",
|
||||||
"add_note_explanation": "Escreva sua nova nota abaixo e clique no botão '+' quando terminar",
|
"add_note_explanation": "Escreva sua nova nota abaixo e clique no botão '+' quando terminar",
|
||||||
"adding_ellipsis": "Adicionando ...",
|
"adding_ellipsis": "Adicionando ...",
|
||||||
|
"all": "Todos",
|
||||||
"are_you_sure": "Você tem certeza?",
|
"are_you_sure": "Você tem certeza?",
|
||||||
"back_to_login": "Volte ao login",
|
"back_to_login": "Volte ao login",
|
||||||
"back_to_start": "Voltar ao Início",
|
"back_to_start": "Voltar ao Início",
|
||||||
|
"blacklist": "Lista negra",
|
||||||
"by": "Por",
|
"by": "Por",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"certificate": "Certificado",
|
"certificate": "Certificado",
|
||||||
@@ -62,12 +65,14 @@
|
|||||||
"create": "Crio",
|
"create": "Crio",
|
||||||
"created": "Criado",
|
"created": "Criado",
|
||||||
"created_by": "Criado Por",
|
"created_by": "Criado Por",
|
||||||
|
"creator": "O Criador",
|
||||||
"current": "Atual",
|
"current": "Atual",
|
||||||
"custom_date": "Data personalizada",
|
"custom_date": "Data personalizada",
|
||||||
"dashboard": "painel de controle",
|
"dashboard": "painel de controle",
|
||||||
"date": "Encontro",
|
"date": "Encontro",
|
||||||
"day": "dia",
|
"day": "dia",
|
||||||
"days": "dias",
|
"days": "dias",
|
||||||
|
"default_map": "Mapa Padrão",
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"delete_device": "Apagar dispositivo",
|
"delete_device": "Apagar dispositivo",
|
||||||
"details": "Detalhes",
|
"details": "Detalhes",
|
||||||
@@ -85,6 +90,7 @@
|
|||||||
"dismiss": "Dispensar",
|
"dismiss": "Dispensar",
|
||||||
"do_now": "Faça agora!",
|
"do_now": "Faça agora!",
|
||||||
"download": "Baixar",
|
"download": "Baixar",
|
||||||
|
"duplicate": "Duplicado",
|
||||||
"duration": "Duração",
|
"duration": "Duração",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"edit_user": "Editar",
|
"edit_user": "Editar",
|
||||||
@@ -94,6 +100,7 @@
|
|||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"error_adding_note": "Erro ao adicionar nota",
|
"error_adding_note": "Erro ao adicionar nota",
|
||||||
"error_code": "Erro de código",
|
"error_code": "Erro de código",
|
||||||
|
"errors": "Erros",
|
||||||
"execute_now": "Você gostaria de executar este comando agora?",
|
"execute_now": "Você gostaria de executar este comando agora?",
|
||||||
"executed": "Executado",
|
"executed": "Executado",
|
||||||
"exit": "Saída",
|
"exit": "Saída",
|
||||||
@@ -142,14 +149,16 @@
|
|||||||
"no_items": "Nenhum item",
|
"no_items": "Nenhum item",
|
||||||
"none": "Nenhum",
|
"none": "Nenhum",
|
||||||
"not_connected": "Não conectado",
|
"not_connected": "Não conectado",
|
||||||
"of_connected": "% de dispositivos",
|
"of_connected": "% de dispositivos conectados",
|
||||||
"off": "Fora",
|
"off": "Fora",
|
||||||
"on": "em",
|
"on": "em",
|
||||||
"optional": "Opcional",
|
"optional": "Opcional",
|
||||||
"overall_health": "Saúde geral",
|
"overall_health": "Saúde geral",
|
||||||
"password_policy": "Política de Senha",
|
"password_policy": "Política de Senha",
|
||||||
|
"preferences": "Preferências",
|
||||||
"preview": "Visualizar",
|
"preview": "Visualizar",
|
||||||
"program": "Programa",
|
"program": "Programa",
|
||||||
|
"reason": "RAZÃO",
|
||||||
"recorded": "Gravado",
|
"recorded": "Gravado",
|
||||||
"refresh": "REFRESH",
|
"refresh": "REFRESH",
|
||||||
"refresh_device": "Atualizar dispositivo",
|
"refresh_device": "Atualizar dispositivo",
|
||||||
@@ -170,12 +179,14 @@
|
|||||||
"show_all": "mostre tudo",
|
"show_all": "mostre tudo",
|
||||||
"socket_connection_closed": "Conexão fechada!",
|
"socket_connection_closed": "Conexão fechada!",
|
||||||
"start": "Começar",
|
"start": "Começar",
|
||||||
|
"status": "Status",
|
||||||
"stop_editing": "Pare de editar",
|
"stop_editing": "Pare de editar",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"submitted": "Submetido",
|
"submitted": "Submetido",
|
||||||
"success": "Sucesso",
|
"success": "Sucesso",
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"table": "Mesa",
|
"table": "Mesa",
|
||||||
|
"time_per_device": "Dispositivo / segundo",
|
||||||
"timestamp": "tempo",
|
"timestamp": "tempo",
|
||||||
"to": "Para",
|
"to": "Para",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
@@ -190,6 +201,7 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendedores",
|
"vendors": "Vendedores",
|
||||||
"view_more": "Veja mais",
|
"view_more": "Veja mais",
|
||||||
|
"visibility": "visibilidade",
|
||||||
"yes": "sim"
|
"yes": "sim"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +222,8 @@
|
|||||||
"creation_success": "Configuração criada com sucesso!",
|
"creation_success": "Configuração criada com sucesso!",
|
||||||
"currently_associated": "Configuração atual associada: {{config}}",
|
"currently_associated": "Configuração atual associada: {{config}}",
|
||||||
"currently_selected_config": "Configuração atualmente selecionada: {{config}}",
|
"currently_selected_config": "Configuração atualmente selecionada: {{config}}",
|
||||||
|
"default_configs": "Configurações padrão",
|
||||||
|
"default_configurations": "Configurações padrão",
|
||||||
"delete_config": "Excluir configuração",
|
"delete_config": "Excluir configuração",
|
||||||
"details": "Detalhes",
|
"details": "Detalhes",
|
||||||
"device_password": "Senha",
|
"device_password": "Senha",
|
||||||
@@ -218,6 +232,7 @@
|
|||||||
"devices_affected": "Dispositivos afetados por esta configuração:",
|
"devices_affected": "Dispositivos afetados por esta configuração:",
|
||||||
"edit_configuration": "Editar configuração",
|
"edit_configuration": "Editar configuração",
|
||||||
"error_delete": "Erro ao tentar excluir: {{error}}",
|
"error_delete": "Erro ao tentar excluir: {{error}}",
|
||||||
|
"error_delete_blacklist": "Erro ao excluir da lista negra: {{error}}",
|
||||||
"error_fetching_config": "Erro ao buscar configuração",
|
"error_fetching_config": "Erro ao buscar configuração",
|
||||||
"error_trying_delete": "Erro ao tentar excluir: {{error}}",
|
"error_trying_delete": "Erro ao tentar excluir: {{error}}",
|
||||||
"error_update": "Erro: {{error}}",
|
"error_update": "Erro: {{error}}",
|
||||||
@@ -261,6 +276,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "PIN de acesso",
|
"access_pin": "PIN de acesso",
|
||||||
"add_contact": "Adicionar contato",
|
"add_contact": "Adicionar contato",
|
||||||
|
"contact": "Contato",
|
||||||
"create_contact": "Criar Contato",
|
"create_contact": "Criar Contato",
|
||||||
"currently_selected_contact": "Contato atualmente selecionado: {{contact}}",
|
"currently_selected_contact": "Contato atualmente selecionado: {{contact}}",
|
||||||
"delete": "Excluir contato?",
|
"delete": "Excluir contato?",
|
||||||
@@ -300,12 +316,23 @@
|
|||||||
"healthchecks_title": "Excluir verificações de saúde"
|
"healthchecks_title": "Excluir verificações de saúde"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Adicionar dispositivo à lista negra",
|
||||||
|
"all_devices": "Todos os dispositivos",
|
||||||
|
"blacklisted_on": "Encontro",
|
||||||
|
"capabilities": "Recursos",
|
||||||
"certificate_explanation": "Certificados de dispositivos conectados",
|
"certificate_explanation": "Certificados de dispositivos conectados",
|
||||||
|
"edit_blacklist": "Editar dispositivo na lista negra",
|
||||||
|
"error_adding_blacklist": "Erro ao adicionar dispositivo à lista negra: {{error}}",
|
||||||
|
"error_edit_blacklist": "Erro ao editar a lista negra: {{error}}",
|
||||||
"error_fetching_device": "Erro ao buscar informações do dispositivo: {{error}}",
|
"error_fetching_device": "Erro ao buscar informações do dispositivo: {{error}}",
|
||||||
"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}",
|
"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}",
|
||||||
"health_explanation": "Saúde de dispositivos conectados",
|
"health_explanation": "Integridade dos dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos Conectados)",
|
||||||
"memory_explanation": "Memória usada por dispositivos conectados",
|
"memory_explanation": "Quantidade de dispositivos conectados com a memória correspondente usada%",
|
||||||
"uptimes_explanation": "Há tempo em que os dispositivos conectados estão ativados e conectados"
|
"remove_from_blacklist": "Remover da lista negra",
|
||||||
|
"success_added_blacklist": "Dispositivo adicionado à lista negra com sucesso!",
|
||||||
|
"success_edit_blacklist": "Lista negra editada com sucesso!",
|
||||||
|
"success_removed_blacklist": "Dispositivo removido com sucesso da lista negra!",
|
||||||
|
"uptimes_explanation": "Quantidade de dispositivos conectados com base em seu tempo de atividade"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Registro",
|
"log": "Registro",
|
||||||
@@ -320,23 +347,32 @@
|
|||||||
"add_success": "Entidade criada com sucesso!",
|
"add_success": "Entidade criada com sucesso!",
|
||||||
"assigned_inventory": "Estoque Atribuído",
|
"assigned_inventory": "Estoque Atribuído",
|
||||||
"cannot_delete": "Você não pode excluir entidades que têm filhos. Exclua os filhos desta entidade para poder excluí-la.",
|
"cannot_delete": "Você não pode excluir entidades que têm filhos. Exclua os filhos desta entidade para poder excluí-la.",
|
||||||
|
"confirm_map_delete": "Tem certeza que deseja excluir o mapa {{name}}? Esta ação não pode ser revertida",
|
||||||
"currently_selected_entity": "Entidade atualmente selecionada: {{config}}",
|
"currently_selected_entity": "Entidade atualmente selecionada: {{config}}",
|
||||||
"currently_selected_venue": "Local selecionado atualmente: {{config}}",
|
"currently_selected_venue": "Local selecionado atualmente: {{config}}",
|
||||||
"delete_success": "Entidade excluída com sucesso",
|
"delete_success": "Entidade excluída com sucesso",
|
||||||
"delete_warning": "Aviso: esta operação não pode ser revertida",
|
"delete_warning": "Aviso: esta operação não pode ser revertida",
|
||||||
|
"duplicate_from_node": "Duplicar com nó raiz específico",
|
||||||
|
"duplicate_map": "Mapa duplicado",
|
||||||
|
"duplicate_with_node": "Duplicar {{mapName}} com {{rootName}} como nó raiz",
|
||||||
"edit_failure": "Atualização malsucedida: {{error}}",
|
"edit_failure": "Atualização malsucedida: {{error}}",
|
||||||
"enter_here": "Digite o (s) IP (s) que deseja adicionar aqui",
|
"enter_here": "Digite o (s) IP (s) que deseja adicionar aqui",
|
||||||
"entire_tree": "Mapa do Site",
|
"entire_tree": "Mapa de Rede",
|
||||||
"entities": "Entidades",
|
"entities": "Entidades",
|
||||||
"entity": "Entidade",
|
"entity": "Entidade",
|
||||||
|
"error_deleting_map": "Erro ao excluir mapa: {{error}}",
|
||||||
"error_fetch_entity": "Erro ao buscar informações da entidade",
|
"error_fetch_entity": "Erro ao buscar informações da entidade",
|
||||||
"error_fetching": "Erro ao buscar entidades",
|
"error_fetching": "Erro ao buscar entidades",
|
||||||
"error_fetching_map": "Erro ao buscar mapa: {{error}}",
|
"error_fetching_map": "Erro ao buscar mapa: {{error}}",
|
||||||
|
"error_fetching_tree": "Erro ao buscar árvore: {{error}}",
|
||||||
"error_saving": "Erro ao salvar entidade",
|
"error_saving": "Erro ao salvar entidade",
|
||||||
|
"error_saving_map": "Erro ao salvar o mapa: {{error}}",
|
||||||
"higher_priority": "Dê maior prioridade",
|
"higher_priority": "Dê maior prioridade",
|
||||||
"ip_detection": "Detecção de IP",
|
"ip_detection": "Detecção de IP",
|
||||||
"ip_formats": "Você pode adicionar endereços IPv4 ou IPv6 nos seguintes formatos:",
|
"ip_formats": "Você pode adicionar endereços IPv4 ou IPv6 nos seguintes formatos:",
|
||||||
"lower_priority": "Faça menor prioridade",
|
"lower_priority": "Faça menor prioridade",
|
||||||
|
"map": "Mapa",
|
||||||
|
"map_delete_success": "Mapa excluído com sucesso!",
|
||||||
"need_select_entity": "Você precisa selecionar uma entidade da tabela abaixo",
|
"need_select_entity": "Você precisa selecionar uma entidade da tabela abaixo",
|
||||||
"no_ips": "Nenhum IP selecionado",
|
"no_ips": "Nenhum IP selecionado",
|
||||||
"not_assigned": "Não atribuído",
|
"not_assigned": "Não atribuído",
|
||||||
@@ -344,6 +380,7 @@
|
|||||||
"select_entity": "Selecione esta Entidade",
|
"select_entity": "Selecione esta Entidade",
|
||||||
"selected_entity": "Entidade Selecionada",
|
"selected_entity": "Entidade Selecionada",
|
||||||
"selected_map": "Mapa Selecionado",
|
"selected_map": "Mapa Selecionado",
|
||||||
|
"tree_saved": "Mapa salvo com sucesso!",
|
||||||
"update_failure_error": "Erro ao tentar atualizar a entidade: {{error}}",
|
"update_failure_error": "Erro ao tentar atualizar a entidade: {{error}}",
|
||||||
"valid_serial": "Precisa ser um número de série válido (12 caracteres HEX)",
|
"valid_serial": "Precisa ser um número de série válido (12 caracteres HEX)",
|
||||||
"venues": "Locais"
|
"venues": "Locais"
|
||||||
@@ -541,6 +578,9 @@
|
|||||||
"verification_code": "Insira sua verificação aqui",
|
"verification_code": "Insira sua verificação aqui",
|
||||||
"wrong_code": "O código de verificação inserido não é válido."
|
"wrong_code": "O código de verificação inserido não é válido."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Provisioning"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "Quando você gostaria de reinicializar este dispositivo?",
|
"directions": "Quando você gostaria de reinicializar este dispositivo?",
|
||||||
"now": "Você gostaria de reiniciar este dispositivo agora?",
|
"now": "Você gostaria de reiniciar este dispositivo agora?",
|
||||||
@@ -584,7 +624,7 @@
|
|||||||
"mac_prefix": "Prefixo MAC",
|
"mac_prefix": "Prefixo MAC",
|
||||||
"max_associations": "Máx. Associações",
|
"max_associations": "Máx. Associações",
|
||||||
"max_clients": "Máx. Clientes",
|
"max_clients": "Máx. Clientes",
|
||||||
"messages_transmitted": "Mensagens Transmitidas",
|
"messages_transmitted": "Msgs TX",
|
||||||
"min_associations": "Min. Associações",
|
"min_associations": "Min. Associações",
|
||||||
"min_clients": "Min. Clientes",
|
"min_clients": "Min. Clientes",
|
||||||
"pause": "pausa",
|
"pause": "pausa",
|
||||||
@@ -592,7 +632,7 @@
|
|||||||
"prefix_length": "Obrigatório, deve ter 6 caracteres",
|
"prefix_length": "Obrigatório, deve ter 6 caracteres",
|
||||||
"previous_runs": "Execuções anteriores",
|
"previous_runs": "Execuções anteriores",
|
||||||
"received": "recebido",
|
"received": "recebido",
|
||||||
"received_messages": "Mensagens recebidas",
|
"received_messages": "Msgs RX",
|
||||||
"reconnect_interval": "Intervalo de reconexão",
|
"reconnect_interval": "Intervalo de reconexão",
|
||||||
"resume": "Currículo",
|
"resume": "Currículo",
|
||||||
"resume_success": "Executar retomado!",
|
"resume_success": "Executar retomado!",
|
||||||
@@ -634,6 +674,19 @@
|
|||||||
"uptime": "Tempo de atividade",
|
"uptime": "Tempo de atividade",
|
||||||
"used_total_memory": "{{used}} usado / {{total}} total"
|
"used_total_memory": "{{used}} usado / {{total}} total"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"create": "Criar assinante",
|
||||||
|
"edit": "Editar Assinante",
|
||||||
|
"error_create": "Erro ao criar assinante: {{error}}",
|
||||||
|
"error_delete": "Erro ao excluir assinante: {{error}}",
|
||||||
|
"error_fetching": "Erro ao buscar assinantes: {{error}}",
|
||||||
|
"error_fetching_single": "Erro ao buscar assinante: {{error}}",
|
||||||
|
"error_update": "Erro ao atualizar assinante: {{error}}",
|
||||||
|
"subscribers": "Inscritos",
|
||||||
|
"success_create": "Assinante criado com sucesso!",
|
||||||
|
"success_delete": "Assinante excluído com sucesso!",
|
||||||
|
"success_update": "Assinante atualizado com sucesso!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Erro ao buscar informações do sistema",
|
"error_fetching": "Erro ao buscar informações do sistema",
|
||||||
"error_reloading": "Erro ao recarregar: {{error}}",
|
"error_reloading": "Erro ao recarregar: {{error}}",
|
||||||
@@ -723,6 +776,7 @@
|
|||||||
"send_code_again": "Envie o Código Novamente",
|
"send_code_again": "Envie o Código Novamente",
|
||||||
"show_hide_password": "Mostrar / ocultar senha",
|
"show_hide_password": "Mostrar / ocultar senha",
|
||||||
"successful_validation": "Número de telefone validado! Clique no botão Salvar para vinculá-lo ao seu perfil",
|
"successful_validation": "Número de telefone validado! Clique no botão Salvar para vinculá-lo ao seu perfil",
|
||||||
|
"table_title": "Usuários administrativos",
|
||||||
"update_failure": "Erro ao tentar atualizar: {{error}}",
|
"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",
|
||||||
|
|||||||
BIN
src/assets/NotFound.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/devices/cig_wf160d.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
src/assets/devices/cig_wf188.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/devices/cig_wf188n.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/devices/cig_wf194c.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/assets/devices/cig_wf194c4.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/assets/devices/cig_wf808.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
src/assets/devices/cig_wf809.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
src/assets/devices/edgecore_eap101.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
src/assets/devices/edgecore_eap102.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
src/assets/devices/edgecore_ecs4100-12ph.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/devices/edgecore_ecw5211.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
src/assets/devices/edgecore_ecw5410.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
src/assets/devices/edgecore_oap100.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/assets/devices/edgecore_spw2ac1200-lan-poe.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/assets/devices/edgecore_spw2ac1200.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/assets/devices/edgecore_ssw2ac2600.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
src/assets/devices/hfcl_ion4.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src/assets/devices/hfcl_ion4.yml.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src/assets/devices/indio_um-305ac.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/devices/linksys_e8450-ubi.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
src/assets/devices/linksys_ea6350-v4.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
src/assets/devices/linksys_ea6350.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
src/assets/devices/linksys_ea8300.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
src/assets/devices/tp-link_ec420-g1.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
src/assets/devices/tplink_ec420.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
src/assets/devices/tplink_ex227.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/devices/tplink_ex228.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/devices/tplink_ex447.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/devices/wallys_dr40x9.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/assets/devices/wallys_dr6018.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/devices/wallys_dr6018_v4.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
@@ -13,6 +13,7 @@ import {
|
|||||||
cilArrowTop,
|
cilArrowTop,
|
||||||
cilAsterisk,
|
cilAsterisk,
|
||||||
cilBan,
|
cilBan,
|
||||||
|
cilBarcode,
|
||||||
cilBasket,
|
cilBasket,
|
||||||
cilBell,
|
cilBell,
|
||||||
cilBold,
|
cilBold,
|
||||||
@@ -108,6 +109,7 @@ export const icons = {
|
|||||||
cilArrowTop,
|
cilArrowTop,
|
||||||
cilAsterisk,
|
cilAsterisk,
|
||||||
cilBan,
|
cilBan,
|
||||||
|
cilBarcode,
|
||||||
cilBasket,
|
cilBasket,
|
||||||
cilBell,
|
cilBell,
|
||||||
cilBold,
|
cilBold,
|
||||||
|
|||||||
163
src/components/AddConfigurationModal/Form.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import {
|
||||||
|
CForm,
|
||||||
|
CInput,
|
||||||
|
CLabel,
|
||||||
|
CCol,
|
||||||
|
CFormGroup,
|
||||||
|
CInvalidFeedback,
|
||||||
|
CFormText,
|
||||||
|
CRow,
|
||||||
|
CTextarea,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CopyToClipboardButton } from 'ucentral-libs';
|
||||||
|
|
||||||
|
const AddDefaultConfigurationForm = ({
|
||||||
|
t,
|
||||||
|
disable,
|
||||||
|
fields,
|
||||||
|
updateField,
|
||||||
|
updateFieldWithKey,
|
||||||
|
deviceTypes,
|
||||||
|
}) => {
|
||||||
|
const [typeOptions, setTypeOptions] = useState([]);
|
||||||
|
const [chosenTypes, setChosenTypes] = useState([]);
|
||||||
|
|
||||||
|
const parseOptions = () => {
|
||||||
|
const options = [{ value: '*', label: 'All' }];
|
||||||
|
const newOptions = deviceTypes.map((option) => ({
|
||||||
|
value: option,
|
||||||
|
label: option,
|
||||||
|
}));
|
||||||
|
options.push(...newOptions);
|
||||||
|
setTypeOptions(options);
|
||||||
|
setChosenTypes([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeOnChange = (chosenArray) => {
|
||||||
|
const allIndex = chosenArray.findIndex((el) => el.value === '*');
|
||||||
|
|
||||||
|
// If the All option was chosen before, we take it out of the array
|
||||||
|
if (allIndex === 0 && chosenTypes.length > 0) {
|
||||||
|
const newResults = chosenArray.slice(1);
|
||||||
|
setChosenTypes(newResults);
|
||||||
|
updateFieldWithKey('deviceTypes', {
|
||||||
|
value: newResults.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else if (allIndex > 0) {
|
||||||
|
setChosenTypes([{ value: '*', label: 'All' }]);
|
||||||
|
updateFieldWithKey('deviceTypes', { value: ['*'], error: false, notEmpty: true });
|
||||||
|
} else if (chosenArray.length > 0) {
|
||||||
|
setChosenTypes(chosenArray);
|
||||||
|
updateFieldWithKey('deviceTypes', {
|
||||||
|
value: chosenArray.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setChosenTypes([]);
|
||||||
|
updateFieldWithKey('deviceTypes', { value: [], error: false, notEmpty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseOptions();
|
||||||
|
}, [deviceTypes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CForm>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="name">
|
||||||
|
{t('user.name')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<CInput
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.name.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.name.error}
|
||||||
|
disabled={disable}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="description">
|
||||||
|
{t('user.description')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.description.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.description.error}
|
||||||
|
disabled={disable}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CLabel col htmlFor="deviceTypes">
|
||||||
|
<div>{t('configuration.supported_device_types')}:</div>
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
id="deviceTypes"
|
||||||
|
options={typeOptions}
|
||||||
|
onChange={typeOnChange}
|
||||||
|
value={chosenTypes}
|
||||||
|
className={`basic-multi-select ${fields.deviceTypes.error ? 'border-danger' : ''}`}
|
||||||
|
classNamePrefix="select"
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.deviceTypes.error} color="danger">
|
||||||
|
{t('configuration.need_device_type')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<div className="pb-3">
|
||||||
|
{t('configure.enter_new')}
|
||||||
|
<CopyToClipboardButton t={t} size="sm" content={fields.configuration.value} />
|
||||||
|
</div>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CCol>
|
||||||
|
<CTextarea
|
||||||
|
style={{ overflowY: 'scroll', height: '500px' }}
|
||||||
|
id="configuration"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.configuration.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.configuration.error}
|
||||||
|
disabled={disable}
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.configuration.error} color="danger">
|
||||||
|
{t('configure.valid_json')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddDefaultConfigurationForm.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
disable: PropTypes.bool.isRequired,
|
||||||
|
fields: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
updateField: PropTypes.func.isRequired,
|
||||||
|
updateFieldWithKey: PropTypes.func.isRequired,
|
||||||
|
deviceTypes: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddDefaultConfigurationForm;
|
||||||
183
src/components/AddConfigurationModal/index.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CModal, CModalHeader, CModalTitle, CModalBody, CButton, CPopover } from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX, cilSave } from '@coreui/icons';
|
||||||
|
import { useToast, useFormFields, useAuth } from 'ucentral-libs';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { checkIfJson } from 'utils/helper';
|
||||||
|
import Form from './Form';
|
||||||
|
|
||||||
|
const initialForm = {
|
||||||
|
name: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
deviceTypes: {
|
||||||
|
value: [],
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddConfigurationModal = ({ show, toggle, refresh }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialForm);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [deviceTypes, setDeviceTypes] = useState([]);
|
||||||
|
|
||||||
|
const getDeviceTypes = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owfms}/api/v1/firmwares?deviceSet=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setDeviceTypes([...response.data.deviceTypes]);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = () => {
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
for (const [key, field] of Object.entries(fields)) {
|
||||||
|
if (field.required && field.value === '') {
|
||||||
|
updateField(key, { error: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (field.notEmpty && field.value.length === 0) {
|
||||||
|
updateField(key, { error: true, notEmpty: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkIfJson(fields.configuration.value)) {
|
||||||
|
updateField('configuration', { error: true });
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addConfiguration = () => {
|
||||||
|
if (validation()) {
|
||||||
|
setLoading(true);
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
name: fields.name.value,
|
||||||
|
description: fields.description.value,
|
||||||
|
modelIds: fields.deviceTypes.value,
|
||||||
|
configuration: fields.configuration.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.post(
|
||||||
|
`${endpoints.owgw}/api/v1/default_configuration/${fields.name.value}`,
|
||||||
|
parameters,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (refresh !== null) refresh();
|
||||||
|
toggle();
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('configuration.creation_success'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('entity.add_failure', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
getDeviceTypes();
|
||||||
|
setFormFields(initialForm);
|
||||||
|
}
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('configuration.create')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.add')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={addConfiguration}>
|
||||||
|
<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 className="px-5">
|
||||||
|
<Form
|
||||||
|
t={t}
|
||||||
|
disable={loading}
|
||||||
|
fields={fields}
|
||||||
|
updateField={updateFieldWithId}
|
||||||
|
updateFieldWithKey={updateField}
|
||||||
|
deviceTypes={deviceTypes}
|
||||||
|
show={show}
|
||||||
|
/>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddConfigurationModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddConfigurationModal.defaultProps = {
|
||||||
|
refresh: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddConfigurationModal;
|
||||||
158
src/components/AddToBlacklistModal/index.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CLabel,
|
||||||
|
CTextarea,
|
||||||
|
CInput,
|
||||||
|
CInvalidFeedback,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { useAuth, useToast } from 'ucentral-libs';
|
||||||
|
import { cilPlus, cilX } from '@coreui/icons';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
|
||||||
|
const AddToBlacklistModal = ({ show, toggle, serialNumber, refresh }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { endpoints, currentToken } = useAuth();
|
||||||
|
const [chosenSerialNumber, setChosenSerialNumber] = useState('');
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
|
const addToBlacklist = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
serialNumber: chosenSerialNumber,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.post(`${endpoints.owgw}/api/v1/blacklist/${chosenSerialNumber}`, parameters, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('device.success_added_blacklist'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
toggle();
|
||||||
|
if (refresh) refresh();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_adding_blacklist', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
if (serialNumber) setChosenSerialNumber(serialNumber);
|
||||||
|
else setChosenSerialNumber('');
|
||||||
|
}
|
||||||
|
}, [show, serialNumber]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('device.add_to_blacklist')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.add')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={addToBlacklist}
|
||||||
|
disabled={
|
||||||
|
chosenSerialNumber.length !== 12 ||
|
||||||
|
!chosenSerialNumber.match('^[a-fA-F0-9]+$') ||
|
||||||
|
reason === '' ||
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPlus} />
|
||||||
|
</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>
|
||||||
|
<CRow>
|
||||||
|
<CLabel col sm="3">
|
||||||
|
{t('common.serial_number')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="9" className="pt-1">
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={chosenSerialNumber}
|
||||||
|
onChange={(e) => setChosenSerialNumber(e.target.value)}
|
||||||
|
invalid={
|
||||||
|
chosenSerialNumber.length !== 12 && chosenSerialNumber.match('^[a-fA-F0-9]+$')
|
||||||
|
}
|
||||||
|
disabled={loading}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('entity.valid_serial')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CLabel col sm="3">
|
||||||
|
{t('common.reason')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="9" className="pt-2">
|
||||||
|
<CTextarea
|
||||||
|
name="reason"
|
||||||
|
id="reason"
|
||||||
|
rows="3"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddToBlacklistModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
serialNumber: PropTypes.string,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddToBlacklistModal.defaultProps = {
|
||||||
|
serialNumber: '',
|
||||||
|
refresh: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddToBlacklistModal;
|
||||||
210
src/components/BlacklistTable/Table/index.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import {
|
||||||
|
CCardBody,
|
||||||
|
CDataTable,
|
||||||
|
CButton,
|
||||||
|
CLink,
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CPopover,
|
||||||
|
CSelect,
|
||||||
|
CButtonToolbar,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { cilSearch, cilPencil, cilPlus, cilTrash } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
|
||||||
|
const BlacklistTable = ({
|
||||||
|
currentPage,
|
||||||
|
devices,
|
||||||
|
toggleAddBlacklist,
|
||||||
|
toggleEditModal,
|
||||||
|
devicesPerPage,
|
||||||
|
loading,
|
||||||
|
removeFromBlacklist,
|
||||||
|
updateDevicesPerPage,
|
||||||
|
pageCount,
|
||||||
|
updatePage,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
|
||||||
|
{ key: 'created', label: t('device.blacklisted_on'), _style: { width: '1%' } },
|
||||||
|
{ key: 'author', label: t('common.by'), filter: false, _style: { width: '15%' } },
|
||||||
|
{ key: 'reason', label: t('common.reason'), filter: false },
|
||||||
|
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hideTooltips = () => ReactTooltip.hide();
|
||||||
|
|
||||||
|
const escFunction = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
hideTooltips();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', escFunction, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', escFunction, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CCard className="m-0 p-0">
|
||||||
|
<CCardHeader className="p-0 text-right">
|
||||||
|
<CPopover content={t('device.add_to_blacklist')}>
|
||||||
|
<CButton size="sm" color="primary" onClick={toggleAddBlacklist}>
|
||||||
|
<CIcon content={cilPlus} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-0">
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
items={devices ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
scopedSlots={{
|
||||||
|
serialNumber: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
{item.serialNumber}
|
||||||
|
</CLink>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
created: (item) => (
|
||||||
|
<td className="text-left align-middle">
|
||||||
|
<div style={{ width: '130px' }}>
|
||||||
|
<FormattedDate date={item.created} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
author: (item) => <td className="align-middle">{item.author}</td>,
|
||||||
|
reason: (item) => <td className="align-middle">{item.reason}</td>,
|
||||||
|
actions: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CButtonToolbar
|
||||||
|
role="group"
|
||||||
|
className="justify-content-center"
|
||||||
|
style={{ width: '130px' }}
|
||||||
|
>
|
||||||
|
<CPopover content={t('configuration.details')}>
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-search" content={cilSearch} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CLink>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('device.remove_from_blacklist')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => removeFromBlacklist(item.serialNumber)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon content={cilTrash} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.edit')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => toggleEditModal(item.serialNumber)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPencil} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</CButtonToolbar>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row pl-3">
|
||||||
|
<div className="pr-3">
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={updatePage}
|
||||||
|
forcePage={Number(currentPage)}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
|
||||||
|
<div style={{ width: '100px' }} className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={devicesPerPage}
|
||||||
|
onChange={(e) => updateDevicesPerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlacklistTable.propTypes = {
|
||||||
|
currentPage: PropTypes.string,
|
||||||
|
devices: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
toggleAddBlacklist: PropTypes.func.isRequired,
|
||||||
|
toggleEditModal: PropTypes.func.isRequired,
|
||||||
|
updateDevicesPerPage: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
updatePage: PropTypes.func.isRequired,
|
||||||
|
devicesPerPage: PropTypes.string.isRequired,
|
||||||
|
removeFromBlacklist: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
BlacklistTable.defaultProps = {
|
||||||
|
currentPage: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(BlacklistTable);
|
||||||
30
src/components/BlacklistTable/Table/index.module.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.firmwareTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipHeader {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-top-left-radius: 1rem !important;
|
||||||
|
border-top-right-radius: 1rem !important;
|
||||||
|
}
|
||||||
244
src/components/BlacklistTable/index.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { getItem, setItem } from 'utils/localStorageHelper';
|
||||||
|
import { useAuth, useToast, useToggle } from 'ucentral-libs';
|
||||||
|
import AddToBlacklistModal from 'components/AddToBlacklistModal';
|
||||||
|
import EditBlacklistModal from 'components/EditBlacklistModal';
|
||||||
|
import Table from './Table';
|
||||||
|
|
||||||
|
const BlacklistTable = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const history = useHistory();
|
||||||
|
const [page, setPage] = useState(parseInt(sessionStorage.getItem('deviceTable') ?? 0, 10));
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [deviceCount, setDeviceCount] = useState(0);
|
||||||
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
const [devicesPerPage, setDevicesPerPage] = useState(getItem('devicesPerPage') || '10');
|
||||||
|
const [devices, setDevices] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [editSerial, setEditSerial] = useState('');
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [showAddModal, toggleAddModal] = useToggle(false);
|
||||||
|
|
||||||
|
const toggleEditModal = (serialNumber) => {
|
||||||
|
if (serialNumber) setEditSerial(serialNumber);
|
||||||
|
setShowEditModal(!showEditModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeviceInformation = (selectedPage = page, devicePerPage = devicesPerPage) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/blacklist?limit=${devicePerPage}&offset=${
|
||||||
|
devicePerPage * selectedPage
|
||||||
|
}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setDevices(response.data.devices);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCount = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/blacklist?countOnly=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const devicesCount = response.data.count;
|
||||||
|
const pagesCount = Math.ceil(devicesCount / devicesPerPage);
|
||||||
|
setPageCount(pagesCount);
|
||||||
|
setDeviceCount(devicesCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= pagesCount) {
|
||||||
|
history.push(`/devices?page=${pagesCount - 1}`);
|
||||||
|
selectedPage = pagesCount - 1;
|
||||||
|
}
|
||||||
|
getDeviceInformation(selectedPage);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDevice = (serialNumber) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let newDevice;
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/blacklist?deviceWithStatus=true&select=${encodeURIComponent(
|
||||||
|
serialNumber,
|
||||||
|
)}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
({
|
||||||
|
data: {
|
||||||
|
devicesWithStatus: [device],
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
newDevice = device;
|
||||||
|
|
||||||
|
return axiosInstance.get(
|
||||||
|
`${endpoints.owfms}/api/v1/firmwareAge?select=${serialNumber}`,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
newDevice.firmwareInfo = {
|
||||||
|
age: response.data.ages[0].age,
|
||||||
|
latest: response.data.ages[0].latest,
|
||||||
|
};
|
||||||
|
const foundIndex = devices.findIndex((obj) => obj.serialNumber === serialNumber);
|
||||||
|
const newList = devices;
|
||||||
|
newList[foundIndex] = newDevice;
|
||||||
|
setDevices(newList);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDevicesPerPage = (value) => {
|
||||||
|
setItem('devicesPerPage', value);
|
||||||
|
setDevicesPerPage(value);
|
||||||
|
|
||||||
|
const newPageCount = Math.ceil(deviceCount / value);
|
||||||
|
setPageCount(newPageCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= newPageCount) {
|
||||||
|
history.push(`/blacklist?page=${newPageCount - 1}`);
|
||||||
|
selectedPage = newPageCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceInformation(selectedPage, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePageCount = ({ selected: selectedPage }) => {
|
||||||
|
sessionStorage.setItem('deviceTable', selectedPage);
|
||||||
|
setPage(selectedPage);
|
||||||
|
getDeviceInformation(selectedPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFromBlacklist = (serialNumber) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.delete(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('device.success_removed_blacklist'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
getCount();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_adding_blacklist', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCount();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
currentPage={page}
|
||||||
|
t={t}
|
||||||
|
devices={devices}
|
||||||
|
loading={loading}
|
||||||
|
toggleAddBlacklist={toggleAddModal}
|
||||||
|
toggleEditModal={toggleEditModal}
|
||||||
|
updateDevicesPerPage={updateDevicesPerPage}
|
||||||
|
devicesPerPage={devicesPerPage}
|
||||||
|
pageCount={pageCount}
|
||||||
|
updatePage={updatePageCount}
|
||||||
|
pageRangeDisplayed={5}
|
||||||
|
refreshDevice={refreshDevice}
|
||||||
|
removeFromBlacklist={removeFromBlacklist}
|
||||||
|
/>
|
||||||
|
{showAddModal ? (
|
||||||
|
<AddToBlacklistModal show={showAddModal} toggle={toggleAddModal} refresh={getCount} />
|
||||||
|
) : null}
|
||||||
|
<EditBlacklistModal
|
||||||
|
show={showEditModal}
|
||||||
|
toggle={toggleEditModal}
|
||||||
|
refresh={getCount}
|
||||||
|
serialNumber={editSerial}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlacklistTable;
|
||||||
@@ -5,21 +5,18 @@ import {
|
|||||||
CModalTitle,
|
CModalTitle,
|
||||||
CModalBody,
|
CModalBody,
|
||||||
CModalFooter,
|
CModalFooter,
|
||||||
CSwitch,
|
|
||||||
CCol,
|
CCol,
|
||||||
CRow,
|
|
||||||
CFormGroup,
|
CFormGroup,
|
||||||
CInputRadio,
|
CInputRadio,
|
||||||
CLabel,
|
CLabel,
|
||||||
CPopover,
|
CPopover,
|
||||||
|
CRow,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilX } from '@coreui/icons';
|
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 PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { dateToUnix } from 'utils/helper';
|
|
||||||
import 'react-widgets/styles.css';
|
import 'react-widgets/styles.css';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import eventBus from 'utils/eventBus';
|
import eventBus from 'utils/eventBus';
|
||||||
@@ -31,38 +28,24 @@ const BlinkModal = ({ show, toggleModal }) => {
|
|||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const { deviceSerialNumber } = useDevice();
|
const { deviceSerialNumber } = useDevice();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const [isNow, setIsNow] = useState(false);
|
|
||||||
const [waiting, setWaiting] = useState(false);
|
const [waiting, setWaiting] = useState(false);
|
||||||
const [chosenDate, setChosenDate] = useState(new Date().toString());
|
const [chosenPattern, setPattern] = useState('blink');
|
||||||
const [chosenPattern, setPattern] = useState('on');
|
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
|
|
||||||
const toggleNow = () => {
|
|
||||||
setIsNow(!isNow);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setDate = (date) => {
|
|
||||||
if (date) {
|
|
||||||
setChosenDate(date.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) {
|
if (show) {
|
||||||
setWaiting(false);
|
setWaiting(false);
|
||||||
setChosenDate(new Date().toString());
|
setPattern('blink');
|
||||||
setPattern('on');
|
|
||||||
setResult(null);
|
setResult(null);
|
||||||
}
|
}
|
||||||
}, [show]);
|
}, [show]);
|
||||||
|
|
||||||
const doAction = () => {
|
const doAction = () => {
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
const utcDate = new Date(chosenDate);
|
|
||||||
|
|
||||||
const parameters = {
|
const parameters = {
|
||||||
serialNumber: deviceSerialNumber,
|
serialNumber: deviceSerialNumber,
|
||||||
when: isNow ? 0 : dateToUnix(utcDate),
|
when: 0,
|
||||||
pattern: chosenPattern,
|
pattern: chosenPattern,
|
||||||
duration: 30,
|
duration: 30,
|
||||||
};
|
};
|
||||||
@@ -113,11 +96,26 @@ const BlinkModal = ({ show, toggleModal }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<CModalBody>
|
<CModalBody>
|
||||||
<CFormGroup row>
|
<CRow className="mb-3">
|
||||||
|
<CCol>{t('blink.explanation')}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CFormGroup row className="mb-0">
|
||||||
<CCol md="3">
|
<CCol md="3">
|
||||||
<CLabel>{t('blink.pattern')}</CLabel>
|
<CLabel>{t('blink.pattern')}</CLabel>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
|
<CFormGroup variant="custom-radio" onClick={() => setPattern('blink')} inline>
|
||||||
|
<CInputRadio
|
||||||
|
custom
|
||||||
|
defaultChecked={chosenPattern === 'blink'}
|
||||||
|
id="radio3"
|
||||||
|
name="radios"
|
||||||
|
value="option3"
|
||||||
|
/>
|
||||||
|
<CLabel variant="custom-checkbox" htmlFor="radio3">
|
||||||
|
{t('blink.blink')}
|
||||||
|
</CLabel>
|
||||||
|
</CFormGroup>
|
||||||
<CFormGroup variant="custom-radio" onClick={() => setPattern('on')} inline>
|
<CFormGroup variant="custom-radio" onClick={() => setPattern('on')} inline>
|
||||||
<CInputRadio
|
<CInputRadio
|
||||||
custom
|
custom
|
||||||
@@ -142,55 +140,12 @@ const BlinkModal = ({ show, toggleModal }) => {
|
|||||||
{t('common.off')}
|
{t('common.off')}
|
||||||
</CLabel>
|
</CLabel>
|
||||||
</CFormGroup>
|
</CFormGroup>
|
||||||
<CFormGroup variant="custom-radio" onClick={() => setPattern('blink')} inline>
|
|
||||||
<CInputRadio
|
|
||||||
custom
|
|
||||||
defaultChecked={chosenPattern === 'blink'}
|
|
||||||
id="radio3"
|
|
||||||
name="radios"
|
|
||||||
value="option3"
|
|
||||||
/>
|
|
||||||
<CLabel variant="custom-checkbox" htmlFor="radio3">
|
|
||||||
{t('blink.blink')}
|
|
||||||
</CLabel>
|
|
||||||
</CFormGroup>
|
|
||||||
</CCol>
|
</CCol>
|
||||||
</CFormGroup>
|
</CFormGroup>
|
||||||
<CRow className="pt-1">
|
|
||||||
<CCol md="8">
|
|
||||||
<p>{t('blink.execute_now')}</p>
|
|
||||||
</CCol>
|
|
||||||
<CCol>
|
|
||||||
<CSwitch
|
|
||||||
disabled={waiting}
|
|
||||||
color="primary"
|
|
||||||
defaultChecked={isNow}
|
|
||||||
onClick={toggleNow}
|
|
||||||
labelOn={t('common.yes')}
|
|
||||||
labelOff={t('common.no')}
|
|
||||||
/>
|
|
||||||
</CCol>
|
|
||||||
</CRow>
|
|
||||||
<CRow hidden={isNow} className="pt-3">
|
|
||||||
<CCol md="4" className="pt-2">
|
|
||||||
<p>{t('common.custom_date')}</p>
|
|
||||||
</CCol>
|
|
||||||
<CCol xs="12" md="8">
|
|
||||||
<DatePicker
|
|
||||||
selected={new Date(chosenDate)}
|
|
||||||
includeTime
|
|
||||||
value={new Date(chosenDate)}
|
|
||||||
placeholder="Select custom date"
|
|
||||||
disabled={waiting}
|
|
||||||
onChange={(date) => setDate(date)}
|
|
||||||
min={new Date()}
|
|
||||||
/>
|
|
||||||
</CCol>
|
|
||||||
</CRow>
|
|
||||||
</CModalBody>
|
</CModalBody>
|
||||||
<CModalFooter>
|
<CModalFooter>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
label={isNow ? t('blink.set_leds') : t('common.schedule')}
|
label={t('blink.set_leds')}
|
||||||
isLoadingLabel={t('common.loading_ellipsis')}
|
isLoadingLabel={t('common.loading_ellipsis')}
|
||||||
isLoading={waiting}
|
isLoading={waiting}
|
||||||
action={doAction}
|
action={doAction}
|
||||||
|
|||||||
100
src/components/CapabilitiesDisplay/index.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardHeader,
|
||||||
|
CLabel,
|
||||||
|
CPopover,
|
||||||
|
CSpinner,
|
||||||
|
CButton,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilSync } from '@coreui/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CopyToClipboardButton, useAuth, useToast, FormattedDate } from 'ucentral-libs';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
|
||||||
|
const CapabilitiesDisplay = ({ serialNumber }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [capabilities, setCapabilities] = useState({});
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const getCapabilities = () => {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(serialNumber)}/capabilities`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setCapabilities(response.data);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCapabilities();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CCard className="m-0">
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.refresh')}>
|
||||||
|
<CButton size="sm" color="info" onClick={getCapabilities}>
|
||||||
|
<CIcon content={cilSync} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<h5>
|
||||||
|
{t('device.capabilities')}
|
||||||
|
<CopyToClipboardButton
|
||||||
|
t={t}
|
||||||
|
size="sm"
|
||||||
|
content={JSON.stringify(capabilities?.capabilities ?? {})}
|
||||||
|
/>
|
||||||
|
</h5>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CLabel>
|
||||||
|
{t('inventory.last_modification')}: <FormattedDate date={capabilities?.lastUpdate} />
|
||||||
|
</CLabel>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
{loading ? <CSpinner /> : null}
|
||||||
|
<pre className="ignore">{JSON.stringify(capabilities?.capabilities ?? {}, null, 4)}</pre>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CapabilitiesDisplay.propTypes = {
|
||||||
|
serialNumber: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CapabilitiesDisplay;
|
||||||
@@ -325,17 +325,9 @@ const DeviceCommands = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.command === 'trace' ? (
|
{item.command === 'trace' ? (
|
||||||
<CIcon
|
<CIcon name="cil-cloud-download" content={cilCloudDownload} />
|
||||||
name="cil-cloud-download"
|
|
||||||
content={cilCloudDownload}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<CIcon
|
<CIcon name="cil-calendar-check" content={cilCalendarCheck} />
|
||||||
name="cil-calendar-check"
|
|
||||||
content={cilCalendarCheck}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CPopover>
|
</CPopover>
|
||||||
@@ -350,7 +342,7 @@ const DeviceCommands = () => {
|
|||||||
toggleResponse(item);
|
toggleResponse(item);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CIcon name="cilList" size="md" />
|
<CIcon name="cilList" />
|
||||||
</CButton>
|
</CButton>
|
||||||
</CPopover>
|
</CPopover>
|
||||||
<CPopover content={t('common.delete')}>
|
<CPopover content={t('common.delete')}>
|
||||||
@@ -364,7 +356,7 @@ const DeviceCommands = () => {
|
|||||||
toggleConfirmModal(item.UUID, index);
|
toggleConfirmModal(item.UUID, index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CIcon name="cilTrash" size="mdå" />
|
<CIcon name="cilTrash" />
|
||||||
</CButton>
|
</CButton>
|
||||||
</CPopover>
|
</CPopover>
|
||||||
</CButtonToolbar>
|
</CButtonToolbar>
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ const ConfigurationDisplay = ({ getData, deviceConfig }) => {
|
|||||||
/>
|
/>
|
||||||
</h5>
|
</h5>
|
||||||
<CRow>
|
<CRow>
|
||||||
<CCol md="2" xl="2" xxl="1">
|
<CCol>
|
||||||
<CLabel>{t('configuration.last_configuration_change')}: </CLabel>
|
<CLabel>
|
||||||
|
{t('configuration.last_configuration_change')}:{' '}
|
||||||
|
{prettyDate(deviceConfig?.lastConfigurationChange)}
|
||||||
|
</CLabel>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>{prettyDate(deviceConfig?.lastConfigurationChange)}</CCol>
|
|
||||||
</CRow>
|
</CRow>
|
||||||
<pre className="ignore">{JSON.stringify(deviceConfig?.configuration ?? {}, null, 4)}</pre>
|
<pre className="ignore">{JSON.stringify(deviceConfig?.configuration ?? {}, null, 4)}</pre>
|
||||||
</CCardBody>
|
</CCardBody>
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { CModal, CModalHeader, CModalBody, CModalTitle, CPopover, CButton } from '@coreui/react';
|
|
||||||
import CIcon from '@coreui/icons-react';
|
|
||||||
import { cilSave, cilX } from '@coreui/icons';
|
|
||||||
import { CreateUserForm, useFormFields, useAuth, useToast } from 'ucentral-libs';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
|
||||||
import { testRegex, validateEmail } from 'utils/helper';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
name: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
currentPassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
changePassword: {
|
|
||||||
value: 'on',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
userRole: {
|
|
||||||
value: 'accounting',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateUserModal = ({ show, toggle, getUsers, policies }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentToken, endpoints } = useAuth();
|
|
||||||
const { addToast } = useToast();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [formFields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialState);
|
|
||||||
|
|
||||||
const toggleChange = () => {
|
|
||||||
updateField('changePassword', { value: !formFields.changePassword.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const createUser = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
id: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let validationSuccess = true;
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(formFields)) {
|
|
||||||
if (!value.optional && value.value === '') {
|
|
||||||
validationSuccess = false;
|
|
||||||
updateField(key, { value: value.value, error: true });
|
|
||||||
} else if (key === 'currentPassword' && !testRegex(value.value, policies.passwordPattern)) {
|
|
||||||
validationSuccess = false;
|
|
||||||
updateField(key, { value: value.value, error: true });
|
|
||||||
} else if (key === 'email' && !validateEmail(value.value)) {
|
|
||||||
validationSuccess = false;
|
|
||||||
updateField(key, { value: value.value, error: true });
|
|
||||||
} else if (key === 'notes') {
|
|
||||||
parameters[key] = [{ note: value.value }];
|
|
||||||
} else if (key === 'changePassword') {
|
|
||||||
parameters[key] = value.value === 'on';
|
|
||||||
} else {
|
|
||||||
parameters[key] = value.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validationSuccess) {
|
|
||||||
const headers = {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.post(`${endpoints.owsec}/api/v1/user/0`, parameters, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
getUsers();
|
|
||||||
setFormFields(initialState);
|
|
||||||
addToast({
|
|
||||||
title: t('common.success'),
|
|
||||||
body: t('user.create_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
toggle();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('user.create_failure', { error: e.response?.data?.ErrorDescription }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
setFormFields(initialState);
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CModal show={show} onClose={toggle} size="xl">
|
|
||||||
<CModalHeader className="p-1">
|
|
||||||
<CModalTitle className="pl-1 pt-1">{t('user.create')}</CModalTitle>
|
|
||||||
<div className="text-right">
|
|
||||||
<CPopover content={t('user.create')}>
|
|
||||||
<CButton color="primary" variant="outline" onClick={createUser} disabled={loading}>
|
|
||||||
<CIcon content={cilSave} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
<CPopover content={t('common.close')}>
|
|
||||||
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
|
||||||
<CIcon content={cilX} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
</div>
|
|
||||||
</CModalHeader>
|
|
||||||
<CModalBody>
|
|
||||||
<CreateUserForm
|
|
||||||
t={t}
|
|
||||||
fields={formFields}
|
|
||||||
updateField={updateFieldWithId}
|
|
||||||
policies={policies}
|
|
||||||
toggleChange={toggleChange}
|
|
||||||
/>
|
|
||||||
</CModalBody>
|
|
||||||
</CModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CreateUserModal.propTypes = {
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
toggle: PropTypes.func.isRequired,
|
|
||||||
getUsers: PropTypes.func.isRequired,
|
|
||||||
policies: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(CreateUserModal);
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
import { CButton, CCardBody, CCardHeader, CRow, CCol, CPopover, CButtonClose } from '@coreui/react';
|
||||||
|
import { cilTrash } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { LoadingButton } from 'ucentral-libs';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const DeleteButton = ({ t, config, deleteConfig, hideTooltips }) => {
|
||||||
|
const [tooltipId] = useState(createUuid());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CPopover content={t('common.delete')}>
|
||||||
|
<div className="d-inline">
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-2"
|
||||||
|
data-tip
|
||||||
|
data-for={tooltipId}
|
||||||
|
data-event="click"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-trash" content={cilTrash} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
<ReactTooltip
|
||||||
|
id={tooltipId}
|
||||||
|
place="top"
|
||||||
|
effect="solid"
|
||||||
|
globalEventOff="click"
|
||||||
|
clickable
|
||||||
|
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
|
||||||
|
border
|
||||||
|
borderColor="#321fdb"
|
||||||
|
arrowColor="white"
|
||||||
|
overridePosition={({ left, top }) => {
|
||||||
|
const element = document.getElementById(tooltipId);
|
||||||
|
const tooltipWidth = element ? element.offsetWidth : 0;
|
||||||
|
const newLeft = left - tooltipWidth * 0.25;
|
||||||
|
return { top, left: newLeft };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CCardHeader color="primary" className={styles.tooltipHeader}>
|
||||||
|
{t('configuration.delete_config')}
|
||||||
|
<CButtonClose
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="py-1 px-4">
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<LoadingButton
|
||||||
|
data-toggle="dropdown"
|
||||||
|
variant="outline"
|
||||||
|
color="danger"
|
||||||
|
label={t('common.confirm')}
|
||||||
|
isLoadingLabel={t('user.deleting')}
|
||||||
|
isLoading={false}
|
||||||
|
action={() => deleteConfig(config.name)}
|
||||||
|
block
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
</ReactTooltip>
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeleteButton.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
config: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
deleteConfig: PropTypes.func.isRequired,
|
||||||
|
hideTooltips: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteButton;
|
||||||
184
src/components/DefaultConfigurationTable/Table/index.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import {
|
||||||
|
CCardBody,
|
||||||
|
CDataTable,
|
||||||
|
CButton,
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CPopover,
|
||||||
|
CSelect,
|
||||||
|
CButtonToolbar,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { cilPencil, cilPlus } from '@coreui/icons';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
import DeleteButton from './DeleteButton';
|
||||||
|
|
||||||
|
const DefaultConfigurationTable = ({
|
||||||
|
currentPage,
|
||||||
|
configurations,
|
||||||
|
toggleAddBlacklist,
|
||||||
|
toggleEditModal,
|
||||||
|
configurationsPerPage,
|
||||||
|
loading,
|
||||||
|
deleteConfig,
|
||||||
|
updateDevicesPerPage,
|
||||||
|
pageCount,
|
||||||
|
updatePage,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'name', label: t('user.name'), _style: { width: '20%' } },
|
||||||
|
{ key: 'description', label: t('user.description'), _style: { width: '20%' } },
|
||||||
|
{ key: 'created', label: t('common.created'), _style: { width: '10%' } },
|
||||||
|
{ key: 'modified', label: t('common.modified'), _style: { width: '10%' } },
|
||||||
|
{ key: 'deviceTypes', label: t('firmware.device_types'), _style: { width: '20%' } },
|
||||||
|
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hideTooltips = () => ReactTooltip.hide();
|
||||||
|
|
||||||
|
const escFunction = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
hideTooltips();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', escFunction, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', escFunction, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CCard className="m-0 p-0">
|
||||||
|
<CCardHeader className="dark-header text-right">
|
||||||
|
<div className="text-value-lg float-left">
|
||||||
|
{t('configuration.default_configurations')}
|
||||||
|
</div>
|
||||||
|
<div className="text-right float-right">
|
||||||
|
<CPopover content={t('configuration.create_config')}>
|
||||||
|
<CButton size="sm" color="info" onClick={toggleAddBlacklist}>
|
||||||
|
<CIcon content={cilPlus} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-0">
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
items={configurations ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
scopedSlots={{
|
||||||
|
name: (item) => <td className="align-middle">{item.name}</td>,
|
||||||
|
description: (item) => <td className="align-middle">{item.description}</td>,
|
||||||
|
deviceTypes: (item) => <td className="align-middle">{item.modelIds.join(', ')}</td>,
|
||||||
|
created: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<FormattedDate date={item.created} />
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
modified: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<FormattedDate date={item.lastModified} />
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
actions: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CButtonToolbar
|
||||||
|
role="group"
|
||||||
|
className="justify-content-center"
|
||||||
|
style={{ width: '90px' }}
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
t={t}
|
||||||
|
config={item}
|
||||||
|
deleteConfig={deleteConfig}
|
||||||
|
hideTooltips={hideTooltips}
|
||||||
|
/>
|
||||||
|
<CPopover content={t('common.edit')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => toggleEditModal(item.name)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPencil} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</CButtonToolbar>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row pl-3">
|
||||||
|
<div className="pr-3">
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={updatePage}
|
||||||
|
forcePage={Number(currentPage)}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
|
||||||
|
<div style={{ width: '100px' }} className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={configurationsPerPage}
|
||||||
|
onChange={(e) => updateDevicesPerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DefaultConfigurationTable.propTypes = {
|
||||||
|
currentPage: PropTypes.string,
|
||||||
|
configurations: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
toggleAddBlacklist: PropTypes.func.isRequired,
|
||||||
|
toggleEditModal: PropTypes.func.isRequired,
|
||||||
|
updateDevicesPerPage: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
updatePage: PropTypes.func.isRequired,
|
||||||
|
configurationsPerPage: PropTypes.string.isRequired,
|
||||||
|
deleteConfig: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
DefaultConfigurationTable.defaultProps = {
|
||||||
|
currentPage: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DefaultConfigurationTable);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.firmwareTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipHeader {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-top-left-radius: 1rem !important;
|
||||||
|
border-top-right-radius: 1rem !important;
|
||||||
|
}
|
||||||
199
src/components/DefaultConfigurationTable/index.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { getItem, setItem } from 'utils/localStorageHelper';
|
||||||
|
import { useAuth, useToast, useToggle } from 'ucentral-libs';
|
||||||
|
import AddConfigurationModal from 'components/AddConfigurationModal';
|
||||||
|
import EditConfigurationModal from 'components/EditConfigurationModal';
|
||||||
|
import Table from './Table';
|
||||||
|
|
||||||
|
const DefaultConfigurationTable = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const history = useHistory();
|
||||||
|
const [page, setPage] = useState(parseInt(sessionStorage.getItem('configurationTable') ?? 0, 10));
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [configurationCount, setConfigurationCount] = useState(0);
|
||||||
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
const [configurationsPerPage, setConfigurationsPerPage] = useState(
|
||||||
|
getItem('configurationsPerPage') || '10',
|
||||||
|
);
|
||||||
|
const [configurations, setConfigurations] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [editId, setEditId] = useState('');
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [showAddModal, toggleAddModal] = useToggle(false);
|
||||||
|
|
||||||
|
const toggleEditModal = (serialNumber) => {
|
||||||
|
if (serialNumber) setEditId(serialNumber);
|
||||||
|
setShowEditModal(!showEditModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfigurationInformation = (
|
||||||
|
selectedPage = page,
|
||||||
|
configurationPerPage = configurationsPerPage,
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/default_configurations?limit=${configurationPerPage}&offset=${
|
||||||
|
configurationPerPage * selectedPage
|
||||||
|
}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setConfigurations(response.data.configurations);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_fetching_configurations', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCount = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/default_configurations?countOnly=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const configurationsCount = response.data.count;
|
||||||
|
const pagesCount = Math.ceil(configurationsCount / configurationsPerPage);
|
||||||
|
setPageCount(pagesCount);
|
||||||
|
setConfigurationCount(configurationsCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= pagesCount) {
|
||||||
|
history.push(`/defaultconfigurations?page=${pagesCount - 1}`);
|
||||||
|
selectedPage = pagesCount - 1;
|
||||||
|
}
|
||||||
|
getConfigurationInformation(selectedPage);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_fetching_configurations', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConfigurationsPerPage = (value) => {
|
||||||
|
setItem('configurationsPerPage', value);
|
||||||
|
setConfigurationsPerPage(value);
|
||||||
|
|
||||||
|
const newPageCount = Math.ceil(configurationCount / value);
|
||||||
|
setPageCount(newPageCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= newPageCount) {
|
||||||
|
history.push(`/default_configurations?page=${newPageCount - 1}`);
|
||||||
|
selectedPage = newPageCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigurationInformation(selectedPage, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePageCount = ({ selected: selectedPage }) => {
|
||||||
|
sessionStorage.setItem('configurationTable', selectedPage);
|
||||||
|
setPage(selectedPage);
|
||||||
|
getConfigurationInformation(selectedPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteConfig = (name) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.delete(`${endpoints.owgw}/api/v1/default_configuration/${name}`, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('configuration.successful_delete'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
getCount();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_adding_blacklist', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCount();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
currentPage={page}
|
||||||
|
t={t}
|
||||||
|
configurations={configurations}
|
||||||
|
loading={loading}
|
||||||
|
toggleAddBlacklist={toggleAddModal}
|
||||||
|
toggleEditModal={toggleEditModal}
|
||||||
|
updateConfigurationsPerPage={updateConfigurationsPerPage}
|
||||||
|
configurationsPerPage={configurationsPerPage}
|
||||||
|
pageCount={pageCount}
|
||||||
|
updatePage={updatePageCount}
|
||||||
|
pageRangeDisplayed={5}
|
||||||
|
deleteConfig={deleteConfig}
|
||||||
|
/>
|
||||||
|
{showAddModal ? (
|
||||||
|
<AddConfigurationModal show={showAddModal} toggle={toggleAddModal} refresh={getCount} />
|
||||||
|
) : null}
|
||||||
|
<EditConfigurationModal
|
||||||
|
show={showEditModal}
|
||||||
|
toggle={toggleEditModal}
|
||||||
|
refresh={getCount}
|
||||||
|
configId={editId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultConfigurationTable;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CButton, CCard, CCardHeader, CCardBody, CRow, CCol } from '@coreui/react';
|
import { CButton, CCard, CCardHeader, CCardBody, CRow, CCol } from '@coreui/react';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import { LoadingButton, useAuth, useDevice, useToast } from 'ucentral-libs';
|
import { LoadingButton, useAuth, useDevice, useToast, useToggle } from 'ucentral-libs';
|
||||||
import RebootModal from 'components/RebootModal';
|
import RebootModal from 'components/RebootModal';
|
||||||
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
||||||
import ConfigureModal from 'components/ConfigureModal';
|
import ConfigureModal from 'components/ConfigureModal';
|
||||||
@@ -13,7 +14,7 @@ import FactoryResetModal from 'components/FactoryResetModal';
|
|||||||
import EventQueueModal from 'components/EventQueueModal';
|
import EventQueueModal from 'components/EventQueueModal';
|
||||||
import TelemetryModal from 'components/TelemetryModal';
|
import TelemetryModal from 'components/TelemetryModal';
|
||||||
|
|
||||||
const DeviceActions = () => {
|
const DeviceActions = ({ device }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
@@ -21,35 +22,16 @@ const DeviceActions = () => {
|
|||||||
const [upgradeStatus, setUpgradeStatus] = useState({
|
const [upgradeStatus, setUpgradeStatus] = useState({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
const [device, setDevice] = useState({});
|
|
||||||
const [showRebootModal, setShowRebootModal] = useState(false);
|
|
||||||
const [showBlinkModal, setShowBlinkModal] = useState(false);
|
|
||||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
|
||||||
const [showTraceModal, setShowTraceModal] = useState(false);
|
|
||||||
const [showScanModal, setShowScanModal] = useState(false);
|
|
||||||
const [connectLoading, setConnectLoading] = useState(false);
|
const [connectLoading, setConnectLoading] = useState(false);
|
||||||
const [showConfigModal, setConfigModal] = useState(false);
|
const [showRebootModal, toggleRebootModal] = useToggle(false);
|
||||||
const [showFactoryModal, setShowFactoryModal] = useState(false);
|
const [showBlinkModal, toggleBlinkModal] = useToggle(false);
|
||||||
const [showQueueModal, setShowQueueModal] = useState(false);
|
const [showUpgradeModal, toggleUpgradeModal, setShowUpgradeModal] = useToggle(false);
|
||||||
const [showTelemetryModal, setShowTelemetryModal] = useState(false);
|
const [showTraceModal, toggleTraceModal] = useToggle(false);
|
||||||
|
const [showScanModal, toggleScanModal] = useToggle(false);
|
||||||
const toggleRebootModal = () => setShowRebootModal(!showRebootModal);
|
const [showConfigModal, toggleConfigModal] = useToggle(false);
|
||||||
|
const [showFactoryModal, toggleFactoryResetModal] = useToggle(false);
|
||||||
const toggleBlinkModal = () => setShowBlinkModal(!showBlinkModal);
|
const [showQueueModal, toggleQueueModal] = useToggle(false);
|
||||||
|
const [showTelemetryModal, toggleTelemetryModal] = useToggle(false);
|
||||||
const toggleUpgradeModal = () => setShowUpgradeModal(!showUpgradeModal);
|
|
||||||
|
|
||||||
const toggleTraceModal = () => setShowTraceModal(!showTraceModal);
|
|
||||||
|
|
||||||
const toggleScanModal = () => setShowScanModal(!showScanModal);
|
|
||||||
|
|
||||||
const toggleConfigModal = () => setConfigModal(!showConfigModal);
|
|
||||||
|
|
||||||
const toggleFactoryResetModal = () => setShowFactoryModal(!showFactoryModal);
|
|
||||||
|
|
||||||
const toggleQueueModal = () => setShowQueueModal(!showQueueModal);
|
|
||||||
|
|
||||||
const toggleTelemetryModal = () => setShowTelemetryModal(!showTelemetryModal);
|
|
||||||
|
|
||||||
const getRttysInfo = () => {
|
const getRttysInfo = () => {
|
||||||
setConnectLoading(true);
|
setConnectLoading(true);
|
||||||
@@ -67,6 +49,7 @@ const DeviceActions = () => {
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const url = `https://${response.data.server}:${response.data.viewport}/connect/${response.data.connectionId}`;
|
const url = `https://${response.data.server}:${response.data.viewport}/connect/${response.data.connectionId}`;
|
||||||
|
|
||||||
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;
|
||||||
})
|
})
|
||||||
@@ -83,22 +66,6 @@ const DeviceActions = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceInformation = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}`, options)
|
|
||||||
.then((response) => {
|
|
||||||
setDevice(response.data);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (upgradeStatus.result !== undefined) {
|
if (upgradeStatus.result !== undefined) {
|
||||||
addToast({
|
addToast({
|
||||||
@@ -116,10 +83,6 @@ const DeviceActions = () => {
|
|||||||
}
|
}
|
||||||
}, [upgradeStatus]);
|
}, [upgradeStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getDeviceInformation();
|
|
||||||
}, [deviceSerialNumber]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CCard>
|
<CCard>
|
||||||
<CCardHeader className="dark-header">
|
<CCardHeader className="dark-header">
|
||||||
@@ -128,36 +91,41 @@ const DeviceActions = () => {
|
|||||||
<CCardBody>
|
<CCardBody>
|
||||||
<CRow>
|
<CRow>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block onClick={toggleRebootModal} color="primary">
|
<CButton block disabled={device === null} onClick={toggleRebootModal} color="primary">
|
||||||
{t('actions.reboot')}
|
{t('actions.reboot')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block onClick={toggleBlinkModal} color="primary">
|
<CButton block disabled={device === null} onClick={toggleBlinkModal} color="primary">
|
||||||
{t('actions.blink')}
|
{t('actions.blink')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleUpgradeModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleUpgradeModal}>
|
||||||
{t('actions.firmware_upgrade')}
|
{t('actions.firmware_upgrade')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleTraceModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleTraceModal}>
|
||||||
{t('actions.trace')}
|
{t('actions.trace')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleScanModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleScanModal}>
|
||||||
{t('actions.wifi_scan')}
|
{t('actions.wifi_scan')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleFactoryResetModal}>
|
<CButton
|
||||||
|
block
|
||||||
|
disabled={device === null}
|
||||||
|
color="primary"
|
||||||
|
onClick={toggleFactoryResetModal}
|
||||||
|
>
|
||||||
{t('actions.factory_reset')}
|
{t('actions.factory_reset')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
@@ -165,6 +133,7 @@ const DeviceActions = () => {
|
|||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
disabled={device === null}
|
||||||
isLoading={connectLoading}
|
isLoading={connectLoading}
|
||||||
label={t('actions.connect')}
|
label={t('actions.connect')}
|
||||||
isLoadingLabel={t('actions.connecting')}
|
isLoadingLabel={t('actions.connecting')}
|
||||||
@@ -172,19 +141,24 @@ const DeviceActions = () => {
|
|||||||
/>
|
/>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleConfigModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleConfigModal}>
|
||||||
{t('actions.configure')}
|
{t('actions.configure')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleQueueModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleQueueModal}>
|
||||||
{t('commands.event_queue')}
|
{t('commands.event_queue')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleTelemetryModal}>
|
<CButton
|
||||||
|
block
|
||||||
|
disabled={device === null}
|
||||||
|
color="primary"
|
||||||
|
onClick={toggleTelemetryModal}
|
||||||
|
>
|
||||||
{t('actions.telemetry')}
|
{t('actions.telemetry')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
@@ -212,4 +186,12 @@ const DeviceActions = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DeviceActions.propTypes = {
|
||||||
|
device: PropTypes.instanceOf(Object),
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceActions.defaultProps = {
|
||||||
|
device: null,
|
||||||
|
};
|
||||||
|
|
||||||
export default DeviceActions;
|
export default DeviceActions;
|
||||||
|
|||||||
418
src/components/DeviceDashboard/Dashboard/index.js
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardHeader,
|
||||||
|
CCol,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CSpinner,
|
||||||
|
CWidgetIcon,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CChartBar, CChartHorizontalBar, CChartPie } from '@coreui/react-chartjs';
|
||||||
|
import { cilClock, cilInfo, cilMedicalCross, cilThumbUp, cilWarning } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const getColor = (health) => {
|
||||||
|
const numberHealth = health ? Number(health.replace('%', '')) : 0;
|
||||||
|
if (numberHealth >= 90) return 'success';
|
||||||
|
if (numberHealth >= 60) return 'warning';
|
||||||
|
return 'danger';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = (health) => {
|
||||||
|
const numberHealth = health ? Number(health.replace('%', '')) : 0;
|
||||||
|
if (numberHealth >= 90) return <CIcon width={36} name="cil-thumbs-up" content={cilThumbUp} />;
|
||||||
|
if (numberHealth >= 60) return <CIcon width={36} name="cil-warning" content={cilWarning} />;
|
||||||
|
return <CIcon width={36} name="cil-medical-cross" content={cilMedicalCross} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeviceDashboard = ({ t, data, loading }) => (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{ opacity: loading ? '20%' : '100%' }}>
|
||||||
|
<CRow className="mt-3">
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.last_dashboard_refresh')}
|
||||||
|
header={data.snapshot ? <FormattedDate date={data.snapshot} size="lg" /> : <h2>-</h2>}
|
||||||
|
color="info"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-clock" content={cilClock} />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.overall_health')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.health_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header={<h2>{data.overallHealth}</h2>}
|
||||||
|
color={getColor(data.overallHealth)}
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
{getIcon(data.overallHealth)}
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.devices')}
|
||||||
|
header={<h2>{data.numberOfDevices}</h2>}
|
||||||
|
color="primary"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-router" />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.device_status')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.status.datasets}
|
||||||
|
labels={data.status.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) => `${ds.datasets[0].data[item.index]}%`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.device_health')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.health_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.healths.datasets}
|
||||||
|
labels={data.healths.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]}${t('common.of_connected')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
{data.totalAssociations}{' '}
|
||||||
|
{data.totalAssociations === 1
|
||||||
|
? t('wifi_analysis.association')
|
||||||
|
: t('wifi_analysis.associations')}
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.associations.datasets}
|
||||||
|
labels={data.associations.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]}% of ${
|
||||||
|
data.totalAssociations
|
||||||
|
} associations`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.vendors')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartHorizontalBar
|
||||||
|
datasets={data.vendors.datasets}
|
||||||
|
labels={data.vendors.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => value.split(' ')[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('firmware.device_types')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.deviceType.datasets}
|
||||||
|
labels={data.deviceType.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.uptimes')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.uptimes_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.upTimes.datasets}
|
||||||
|
labels={data.upTimes.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.certificates')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.certificate_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.certificates.datasets}
|
||||||
|
labels={data.certificates.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]}${t('common.of_connected')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.commands')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.commands.datasets}
|
||||||
|
labels={data.commands.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.memory_used')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.memory_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.memoryUsed.datasets}
|
||||||
|
labels={data.memoryUsed.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div className={styles.centerContainer}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
DeviceDashboard.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceDashboard);
|
||||||
10
src/components/DeviceDashboard/Dashboard/index.module.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.centerContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
@@ -1,7 +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 { DeviceDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
|
import { useAuth, COLOR_LIST } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
const DeviceDashboard = () => {
|
const DeviceDashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
145
src/components/DeviceFirmwareModal/Modal/index.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CDataTable,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CInput,
|
||||||
|
CPopover,
|
||||||
|
CSwitch,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX } from '@coreui/icons';
|
||||||
|
import { LoadingButton } from 'ucentral-libs';
|
||||||
|
import { cleanBytesString, prettyDate } from 'utils/helper';
|
||||||
|
|
||||||
|
const DeviceFirmwareModal = ({
|
||||||
|
t,
|
||||||
|
device,
|
||||||
|
show,
|
||||||
|
toggle,
|
||||||
|
firmwareVersions,
|
||||||
|
upgradeToVersion,
|
||||||
|
loading,
|
||||||
|
upgradeStatus,
|
||||||
|
keepRedirector,
|
||||||
|
toggleRedirector,
|
||||||
|
}) => {
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ key: 'imageDate', label: t('firmware.image_date'), _style: { width: '17%' }, filter: false },
|
||||||
|
{ key: 'size', label: t('firmware.size'), _style: { width: '8%' }, filter: false },
|
||||||
|
{ key: 'revision', label: t('firmware.revision'), _style: { width: '60%' } },
|
||||||
|
{ key: 'show_details', label: '', _style: { width: '15%' }, filter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilter('');
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal show={show} onClose={toggle} size="xl">
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">#{device?.serialNumber}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody>
|
||||||
|
{show ? (
|
||||||
|
<div>
|
||||||
|
<CRow>
|
||||||
|
<CCol sm="2" className="pt-2">
|
||||||
|
{t('firmware.installed_firmware')}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="pt-2">{device.firmware}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="mt-3">
|
||||||
|
<CCol sm="2" className="pt-2">
|
||||||
|
{t('factory_reset.redirector')}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="pt-2">
|
||||||
|
<CSwitch
|
||||||
|
color="primary"
|
||||||
|
defaultChecked={keepRedirector}
|
||||||
|
onClick={toggleRedirector}
|
||||||
|
labelOn="Yes"
|
||||||
|
labelOff="No"
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-4">
|
||||||
|
<CCol sm="5">
|
||||||
|
<CInput
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
<CCol />
|
||||||
|
</CRow>
|
||||||
|
<CRow className="mb-4">
|
||||||
|
<CCol>
|
||||||
|
<div className="overflow-auto" style={{ height: '600px' }}>
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="table-sm"
|
||||||
|
items={firmwareVersions}
|
||||||
|
fields={fields}
|
||||||
|
loading={loading}
|
||||||
|
hover
|
||||||
|
tableFilterValue={filter}
|
||||||
|
border
|
||||||
|
scopedSlots={{
|
||||||
|
imageDate: (item) => <td>{prettyDate(item.imageDate)}</td>,
|
||||||
|
size: (item) => <td>{cleanBytesString(item.size)}</td>,
|
||||||
|
show_details: (item) => (
|
||||||
|
<td className="text-center">
|
||||||
|
<LoadingButton
|
||||||
|
label={t('firmware.upgrade')}
|
||||||
|
isLoadingLabel={t('firmware.upgrading')}
|
||||||
|
isLoading={false}
|
||||||
|
action={() => upgradeToVersion(item.uri)}
|
||||||
|
block={false}
|
||||||
|
disabled={upgradeStatus.loading}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceFirmwareModal.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
device: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
firmwareVersions: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
upgradeToVersion: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
keepRedirector: PropTypes.bool.isRequired,
|
||||||
|
toggleRedirector: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceFirmwareModal);
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { DeviceFirmwareModal as Modal, useAuth, useToast } from 'ucentral-libs';
|
import { useAuth, useToast, useToggle } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
const DeviceFirmwareModal = ({
|
const DeviceFirmwareModal = ({
|
||||||
device,
|
device,
|
||||||
@@ -17,6 +18,7 @@ const DeviceFirmwareModal = ({
|
|||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [firmwareVersions, setFirmwareVersions] = useState([]);
|
const [firmwareVersions, setFirmwareVersions] = useState([]);
|
||||||
|
const [keepRedirector, toggleKeepRedirector, setKeepRedirector] = useToggle(true);
|
||||||
|
|
||||||
const getPartialFirmware = async (offset) => {
|
const getPartialFirmware = async (offset) => {
|
||||||
const headers = {
|
const headers = {
|
||||||
@@ -78,6 +80,7 @@ const DeviceFirmwareModal = ({
|
|||||||
|
|
||||||
const parameters = {
|
const parameters = {
|
||||||
serialNumber: device.serialNumber,
|
serialNumber: device.serialNumber,
|
||||||
|
keepRedirector,
|
||||||
when: 0,
|
when: 0,
|
||||||
uri,
|
uri,
|
||||||
};
|
};
|
||||||
@@ -108,6 +111,7 @@ const DeviceFirmwareModal = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show && device.compatible) getFirmwareList();
|
if (show && device.compatible) getFirmwareList();
|
||||||
|
if (show) setKeepRedirector(true);
|
||||||
}, [device, show]);
|
}, [device, show]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -120,6 +124,8 @@ const DeviceFirmwareModal = ({
|
|||||||
upgradeToVersion={upgradeToVersion}
|
upgradeToVersion={upgradeToVersion}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
upgradeStatus={upgradeStatus}
|
upgradeStatus={upgradeStatus}
|
||||||
|
keepRedirector={keepRedirector}
|
||||||
|
toggleRedirector={toggleKeepRedirector}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
456
src/components/DeviceListTable/Table/index.js
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import {
|
||||||
|
CCardBody,
|
||||||
|
CDataTable,
|
||||||
|
CButton,
|
||||||
|
CLink,
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CPopover,
|
||||||
|
CSelect,
|
||||||
|
CButtonClose,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import {
|
||||||
|
cilSync,
|
||||||
|
cilArrowCircleTop,
|
||||||
|
cilCheckCircle,
|
||||||
|
cilTerminal,
|
||||||
|
cilTrash,
|
||||||
|
cilSearch,
|
||||||
|
} from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
import { cleanBytesString } from 'utils/helper';
|
||||||
|
import { DeviceBadge, LoadingButton } from 'ucentral-libs';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const DeviceListTable = ({
|
||||||
|
currentPage,
|
||||||
|
devices,
|
||||||
|
searchBar,
|
||||||
|
devicesPerPage,
|
||||||
|
loading,
|
||||||
|
updateDevicesPerPage,
|
||||||
|
pageCount,
|
||||||
|
updatePage,
|
||||||
|
refreshDevice,
|
||||||
|
t,
|
||||||
|
toggleFirmwareModal,
|
||||||
|
toggleHistoryModal,
|
||||||
|
upgradeToLatest,
|
||||||
|
upgradeStatus,
|
||||||
|
deviceIcons,
|
||||||
|
connectRtty,
|
||||||
|
deleteDevice,
|
||||||
|
deleteStatus,
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'deviceType', label: '', filter: false, sorter: false, _style: { width: '1%' } },
|
||||||
|
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
|
||||||
|
{ key: 'firmware', label: t('firmware.revision') },
|
||||||
|
{ key: 'firmware_button', label: '', filter: false, _style: { width: '1%' } },
|
||||||
|
{ key: 'compatible', label: t('common.type'), filter: false, _style: { width: '13%' } },
|
||||||
|
{ key: 'txBytes', label: 'Tx', filter: false, _style: { width: '14%' } },
|
||||||
|
{ key: 'rxBytes', label: 'Rx', filter: false, _style: { width: '14%' } },
|
||||||
|
{ key: 'ipAddress', label: t('IP'), _style: { width: '10%' } },
|
||||||
|
{ key: 'twoG', label: t('2G'), _style: { width: '10%' } },
|
||||||
|
{ key: 'fiveG', label: t('5G'), _style: { width: '10%' } },
|
||||||
|
{ key: 'actions', label: t('actions.actions'), _style: { width: '10%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hideTooltips = () => ReactTooltip.hide();
|
||||||
|
|
||||||
|
const escFunction = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
hideTooltips();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShortRevision = (revision) => {
|
||||||
|
if (revision.includes(' / ')) {
|
||||||
|
return revision.split(' / ')[1];
|
||||||
|
}
|
||||||
|
return revision;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', escFunction, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', escFunction, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getFirmwareButton = (latest, device) => {
|
||||||
|
const tooltipId = createUuid();
|
||||||
|
let text = t('firmware.unknown_firmware_status');
|
||||||
|
let upgradeText = t('firmware.upgrade_to_latest');
|
||||||
|
let icon = <CIcon name="cil-arrow-circle-top" content={cilArrowCircleTop} />;
|
||||||
|
let color = 'secondary';
|
||||||
|
if (latest !== undefined) {
|
||||||
|
text = t('firmware.newer_firmware_available');
|
||||||
|
color = 'warning';
|
||||||
|
|
||||||
|
if (latest) {
|
||||||
|
icon = <CIcon name="cil-check-circle" content={cilCheckCircle} />;
|
||||||
|
text = t('firmware.latest_version_installed');
|
||||||
|
upgradeText = t('firmware.reinstall_latest');
|
||||||
|
color = 'success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CButton size="sm" color={color} data-tip data-for={tooltipId} data-event="click">
|
||||||
|
{icon}
|
||||||
|
</CButton>
|
||||||
|
<ReactTooltip
|
||||||
|
id={tooltipId}
|
||||||
|
place="top"
|
||||||
|
effect="solid"
|
||||||
|
globalEventOff="click"
|
||||||
|
clickable
|
||||||
|
className={[styles.firmwareTooltip, 'tooltipLeft'].join(' ')}
|
||||||
|
border
|
||||||
|
borderColor="#321fdb"
|
||||||
|
arrowColor="white"
|
||||||
|
overridePosition={({ left, top }) => {
|
||||||
|
const element = document.getElementById(tooltipId);
|
||||||
|
const tooltipWidth = element ? element.offsetWidth : 0;
|
||||||
|
const newLeft = left + tooltipWidth * 0.25;
|
||||||
|
return { top, left: newLeft };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CCardHeader color="primary" className={styles.tooltipHeader}>
|
||||||
|
{text}
|
||||||
|
<CButtonClose
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<LoadingButton
|
||||||
|
variant="outline"
|
||||||
|
label={upgradeText}
|
||||||
|
isLoadingLabel={t('firmware.upgrading')}
|
||||||
|
isLoading={upgradeStatus.loading}
|
||||||
|
action={() => upgradeToLatest(device)}
|
||||||
|
block
|
||||||
|
disabled={
|
||||||
|
upgradeStatus.loading && upgradeStatus.serialNumber === device.serialNumber
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CButton
|
||||||
|
block
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
toggleFirmwareModal(device);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('firmware.choose_custom')}
|
||||||
|
</CButton>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CButton
|
||||||
|
block
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
toggleHistoryModal(device);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('firmware.history_title')}
|
||||||
|
</CButton>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
</ReactTooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteButton = (serialNumber) => {
|
||||||
|
const tooltipId = createUuid();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CPopover content={t('common.delete_device')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
data-tip
|
||||||
|
data-for={tooltipId}
|
||||||
|
data-event="click"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-trash" content={cilTrash} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<ReactTooltip
|
||||||
|
id={tooltipId}
|
||||||
|
place="top"
|
||||||
|
effect="solid"
|
||||||
|
globalEventOff="click"
|
||||||
|
clickable
|
||||||
|
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
|
||||||
|
border
|
||||||
|
borderColor="#321fdb"
|
||||||
|
arrowColor="white"
|
||||||
|
overridePosition={({ left, top }) => {
|
||||||
|
const element = document.getElementById(tooltipId);
|
||||||
|
const tooltipWidth = element ? element.offsetWidth : 0;
|
||||||
|
const newLeft = left - tooltipWidth * 0.25;
|
||||||
|
return { top, left: newLeft };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CCardHeader color="primary" className={styles.tooltipHeader}>
|
||||||
|
{t('common.device_delete', { serialNumber })}
|
||||||
|
<CButtonClose
|
||||||
|
className="p-0 mb-1"
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<LoadingButton
|
||||||
|
data-toggle="dropdown"
|
||||||
|
variant="outline"
|
||||||
|
color="danger"
|
||||||
|
label={t('common.confirm')}
|
||||||
|
isLoadingLabel={t('user.deleting')}
|
||||||
|
isLoading={deleteStatus.loading}
|
||||||
|
action={(e) => {
|
||||||
|
e.target.parentNode.parentNode.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
deleteDevice(serialNumber);
|
||||||
|
}}
|
||||||
|
block
|
||||||
|
disabled={deleteStatus.loading}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
</ReactTooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CCard className="m-0 p-0">
|
||||||
|
<CCardHeader className="p-0">
|
||||||
|
<div className="float-left" style={{ width: '400px' }}>
|
||||||
|
{searchBar}
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-0">
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
items={devices ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
scopedSlots={{
|
||||||
|
deviceType: (item) => (
|
||||||
|
<td className="align-middle text-center">
|
||||||
|
<DeviceBadge t={t} device={item} deviceIcons={deviceIcons} />
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
serialNumber: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
{item.serialNumber}
|
||||||
|
</CLink>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
firmware: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<CPopover
|
||||||
|
content={item.firmware ? item.firmware : t('common.na')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div style={{ width: 'calc(10vw)' }} className="text-truncate align-middle">
|
||||||
|
{getShortRevision(item.firmware)}
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
firmware_button: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
{item.firmwareInfo
|
||||||
|
? getFirmwareButton(item.firmwareInfo.latest, item)
|
||||||
|
: getFirmwareButton(undefined, item)}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
compatible: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<CPopover
|
||||||
|
content={item.compatible ? item.compatible : t('common.na')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div style={{ width: 'calc(10vw)' }} className="text-truncate align-middle">
|
||||||
|
{item.compatible}
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
txBytes: (item) => <td className="align-middle">{cleanBytesString(item.txBytes)}</td>,
|
||||||
|
rxBytes: (item) => <td className="align-middle">{cleanBytesString(item.rxBytes)}</td>,
|
||||||
|
ipAddress: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<CPopover
|
||||||
|
content={item.ipAddress ? item.ipAddress : t('common.na')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div style={{ width: 'calc(8vw)' }} className="text-truncate align-middle">
|
||||||
|
{item.ipAddress}
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
twoG: (item) => <td className="align-middle">{item.associations_2G ?? 0}</td>,
|
||||||
|
fiveG: (item) => <td className="align-middle">{item.associations_5G ?? 0}</td>,
|
||||||
|
actions: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<div role="group" className="justify-content-center" style={{ width: '190px' }}>
|
||||||
|
<CPopover content={t('actions.connect')}>
|
||||||
|
<CButton
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => connectRtty(item.serialNumber)}
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-terminal" content={cilTerminal} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
{deleteButton(item.serialNumber)}
|
||||||
|
<CPopover content={t('configuration.details')}>
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-search" content={cilSearch} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CLink>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.refresh_device')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => refreshDevice(item.serialNumber)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-sync" content={cilSync} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row pl-3">
|
||||||
|
<div className="pr-3">
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={updatePage}
|
||||||
|
forcePage={Number(currentPage)}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
|
||||||
|
<div style={{ width: '100px' }} className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={devicesPerPage}
|
||||||
|
onChange={(e) => updateDevicesPerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceListTable.propTypes = {
|
||||||
|
currentPage: PropTypes.oneOf(['string', 'number']),
|
||||||
|
devices: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
searchBar: PropTypes.node.isRequired,
|
||||||
|
updateDevicesPerPage: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
updatePage: PropTypes.func.isRequired,
|
||||||
|
devicesPerPage: PropTypes.string.isRequired,
|
||||||
|
refreshDevice: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
toggleFirmwareModal: PropTypes.func.isRequired,
|
||||||
|
toggleHistoryModal: PropTypes.func.isRequired,
|
||||||
|
upgradeToLatest: PropTypes.func.isRequired,
|
||||||
|
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
deviceIcons: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
connectRtty: PropTypes.func.isRequired,
|
||||||
|
deleteDevice: PropTypes.func.isRequired,
|
||||||
|
deleteStatus: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceListTable.defaultProps = {
|
||||||
|
currentPage: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceListTable);
|
||||||
30
src/components/DeviceListTable/Table/index.module.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.firmwareTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipHeader {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-top-left-radius: 1rem !important;
|
||||||
|
border-top-right-radius: 1rem !important;
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import { getItem, setItem } from 'utils/localStorageHelper';
|
|||||||
import DeviceSearchBar from 'components/DeviceSearchBar';
|
import DeviceSearchBar from 'components/DeviceSearchBar';
|
||||||
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
||||||
import FirmwareHistoryModal from 'components/FirmwareHistoryModal';
|
import FirmwareHistoryModal from 'components/FirmwareHistoryModal';
|
||||||
import { DeviceListTable, useAuth, useToast } from 'ucentral-libs';
|
import { useAuth, useToast } from 'ucentral-libs';
|
||||||
|
import Table from './Table';
|
||||||
import meshIcon from '../../assets/icons/Mesh.png';
|
import meshIcon from '../../assets/icons/Mesh.png';
|
||||||
import apIcon from '../../assets/icons/AP.png';
|
import apIcon from '../../assets/icons/AP.png';
|
||||||
import internetSwitch from '../../assets/icons/Switch.png';
|
import internetSwitch from '../../assets/icons/Switch.png';
|
||||||
@@ -377,7 +378,7 @@ const DeviceList = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DeviceListTable
|
<Table
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
t={t}
|
t={t}
|
||||||
searchBar={<DeviceSearchBar />}
|
searchBar={<DeviceSearchBar />}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ const DeviceLogs = () => {
|
|||||||
toggleDetails(index);
|
toggleDetails(index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CIcon name="cilList" size="md" />
|
<CIcon name="cilList" />
|
||||||
</CButton>
|
</CButton>
|
||||||
</td>
|
</td>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs';
|
import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs';
|
||||||
import { checkIfJson } from 'utils/helper';
|
import { checkIfJson } from 'utils/helper';
|
||||||
|
|
||||||
const DeviceSearchBar = () => {
|
const DeviceSearchBar = ({ action }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
@@ -65,7 +66,15 @@ const DeviceSearchBar = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <SearchBar t={t} search={search} results={results} history={history} />;
|
return <SearchBar t={t} search={search} results={results} history={history} action={action} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceSearchBar.propTypes = {
|
||||||
|
action: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceSearchBar.defaultProps = {
|
||||||
|
action: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeviceSearchBar;
|
export default DeviceSearchBar;
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
|
||||||
import { DeviceStatusCard as Card, useDevice, useAuth, useToast } from 'ucentral-libs';
|
|
||||||
|
|
||||||
const DeviceStatusCard = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentToken, endpoints } = useAuth();
|
|
||||||
const { deviceSerialNumber } = useDevice();
|
|
||||||
const { addToast } = useToast();
|
|
||||||
const [lastStats, setLastStats] = useState(null);
|
|
||||||
const [status, setStatus] = useState(null);
|
|
||||||
const [deviceConfig, setDeviceConfig] = useState(null);
|
|
||||||
const [error, setError] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const getDevice = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}`, options)
|
|
||||||
.then((response) => {
|
|
||||||
setDeviceConfig(response.data);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = () => {
|
|
||||||
setLoading(true);
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const lastStatsRequest = axiosInstance.get(
|
|
||||||
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(
|
|
||||||
deviceSerialNumber,
|
|
||||||
)}/statistics?lastOnly=true`,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
const statusRequest = axiosInstance.get(
|
|
||||||
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}/status`,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
Promise.all([lastStatsRequest, statusRequest])
|
|
||||||
.then(([newStats, newStatus]) => {
|
|
||||||
setLastStats(newStats.data);
|
|
||||||
setStatus(newStatus.data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setError(true);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
getData();
|
|
||||||
getDevice();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setError(false);
|
|
||||||
if (deviceSerialNumber) {
|
|
||||||
getDevice();
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
}, [deviceSerialNumber]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
t={t}
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
deviceSerialNumber={deviceSerialNumber}
|
|
||||||
getData={refresh}
|
|
||||||
deviceConfig={deviceConfig}
|
|
||||||
status={status}
|
|
||||||
lastStats={lastStats}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeviceStatusCard;
|
|
||||||
154
src/components/EditBlacklistModal/index.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CLabel,
|
||||||
|
CTextarea,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { useAuth, useToast } from 'ucentral-libs';
|
||||||
|
import { cilSave, cilX } from '@coreui/icons';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
|
||||||
|
const EditBlacklistModal = ({ show, toggle, serialNumber, refresh }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { endpoints, currentToken } = useAuth();
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
|
const getBlacklistInfo = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setReason(response.data.reason);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.put(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, parameters, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('device.success_edit_blacklist'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
toggle();
|
||||||
|
if (refresh) refresh();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_edit_blacklist', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) getBlacklistInfo();
|
||||||
|
}, [show, serialNumber]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('device.edit_blacklist')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.add')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={save}
|
||||||
|
disabled={loading || reason === ''}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<CRow>
|
||||||
|
<CLabel col sm="3">
|
||||||
|
{t('common.reason')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="9" className="pt-2">
|
||||||
|
<CTextarea
|
||||||
|
name="reason"
|
||||||
|
id="reason"
|
||||||
|
rows="3"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditBlacklistModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
serialNumber: PropTypes.string,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditBlacklistModal.defaultProps = {
|
||||||
|
serialNumber: '',
|
||||||
|
refresh: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditBlacklistModal;
|
||||||
163
src/components/EditConfigurationModal/Form.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import {
|
||||||
|
CForm,
|
||||||
|
CInput,
|
||||||
|
CLabel,
|
||||||
|
CCol,
|
||||||
|
CFormGroup,
|
||||||
|
CInvalidFeedback,
|
||||||
|
CFormText,
|
||||||
|
CRow,
|
||||||
|
CTextarea,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CopyToClipboardButton } from 'ucentral-libs';
|
||||||
|
|
||||||
|
const EditDefaultConfigurationForm = ({
|
||||||
|
t,
|
||||||
|
disable,
|
||||||
|
fields,
|
||||||
|
updateField,
|
||||||
|
updateFieldWithKey,
|
||||||
|
deviceTypes,
|
||||||
|
editing,
|
||||||
|
}) => {
|
||||||
|
const [typeOptions, setTypeOptions] = useState([]);
|
||||||
|
const [chosenTypes, setChosenTypes] = useState([]);
|
||||||
|
|
||||||
|
const parseOptions = () => {
|
||||||
|
const options = [{ value: '*', label: 'All' }];
|
||||||
|
const newOptions = deviceTypes.map((option) => ({
|
||||||
|
value: option,
|
||||||
|
label: option,
|
||||||
|
}));
|
||||||
|
options.push(...newOptions);
|
||||||
|
setTypeOptions(options);
|
||||||
|
setChosenTypes([]);
|
||||||
|
|
||||||
|
const newChosenTypes = fields.modelIds.value.map((dType) => ({
|
||||||
|
value: dType,
|
||||||
|
label: dType === '*' ? 'All' : dType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setChosenTypes(newChosenTypes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeOnChange = (chosenArray) => {
|
||||||
|
const allIndex = chosenArray.findIndex((el) => el.value === '*');
|
||||||
|
|
||||||
|
// If the All option was chosen before, we take it out of the array
|
||||||
|
if (allIndex === 0 && chosenTypes.length > 0) {
|
||||||
|
const newResults = chosenArray.slice(1);
|
||||||
|
setChosenTypes(newResults);
|
||||||
|
updateFieldWithKey('modelIds', {
|
||||||
|
value: newResults.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else if (allIndex > 0) {
|
||||||
|
setChosenTypes([{ value: '*', label: 'All' }]);
|
||||||
|
updateFieldWithKey('modelIds', { value: ['*'], error: false, notEmpty: true });
|
||||||
|
} else if (chosenArray.length > 0) {
|
||||||
|
setChosenTypes(chosenArray);
|
||||||
|
updateFieldWithKey('modelIds', {
|
||||||
|
value: chosenArray.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setChosenTypes([]);
|
||||||
|
updateFieldWithKey('modelIds', { value: [], error: false, notEmpty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseOptions();
|
||||||
|
}, [deviceTypes, fields.name.value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CForm>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="name">
|
||||||
|
{t('user.name')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7" className="pt-2">
|
||||||
|
{fields.name.value}
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="description">
|
||||||
|
{t('user.description')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.description.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.description.error}
|
||||||
|
disabled={disable || !editing}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CLabel col htmlFor="deviceTypes">
|
||||||
|
<div>{t('configuration.supported_device_types')}:</div>
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
id="deviceTypes"
|
||||||
|
options={typeOptions}
|
||||||
|
onChange={typeOnChange}
|
||||||
|
value={chosenTypes}
|
||||||
|
className={`basic-multi-select ${fields.modelIds.error ? 'border-danger' : ''}`}
|
||||||
|
classNamePrefix="select"
|
||||||
|
isDisabled={disable || !editing}
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.modelIds.error} color="danger">
|
||||||
|
{t('configuration.need_device_type')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<div className="pb-3">
|
||||||
|
{t('configuration.title')}
|
||||||
|
<CopyToClipboardButton t={t} size="sm" content={fields.configuration.value} />
|
||||||
|
</div>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CCol>
|
||||||
|
<CTextarea
|
||||||
|
style={{ overflowY: 'scroll', height: '500px' }}
|
||||||
|
id="configuration"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.configuration.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.configuration.error}
|
||||||
|
disabled={disable || !editing}
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.configuration.error} color="danger">
|
||||||
|
{t('common.required')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditDefaultConfigurationForm.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
disable: PropTypes.bool.isRequired,
|
||||||
|
fields: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
updateField: PropTypes.func.isRequired,
|
||||||
|
updateFieldWithKey: PropTypes.func.isRequired,
|
||||||
|
deviceTypes: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditDefaultConfigurationForm;
|
||||||
243
src/components/EditConfigurationModal/index.js
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CModal, CModalHeader, CModalTitle, CModalBody, CButton, CPopover } from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX, cilSave, cilPencil } from '@coreui/icons';
|
||||||
|
import { useToast, useFormFields, useAuth } from 'ucentral-libs';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { checkIfJson } from 'utils/helper';
|
||||||
|
import Form from './Form';
|
||||||
|
|
||||||
|
const initialForm = {
|
||||||
|
name: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
modelIds: {
|
||||||
|
value: [],
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditConfigurationModal = ({ show, toggle, refresh, configId }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialForm);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [deviceTypes, setDeviceTypes] = useState([]);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
const getConfig = () => {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/default_configuration/${configId}`, options)
|
||||||
|
.then((response) => {
|
||||||
|
const newConfig = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(response.data)) {
|
||||||
|
if (key in initialForm) {
|
||||||
|
newConfig[key] = {
|
||||||
|
...initialForm[key],
|
||||||
|
value: response.data[key],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newConfig.configuration.value = JSON.stringify(response.data.configuration, null, 2);
|
||||||
|
setFormFields(newConfig);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_fetching_config', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeviceTypes = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owfms}/api/v1/firmwares?deviceSet=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setDeviceTypes([...response.data.deviceTypes]);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = () => {
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
for (const [key, field] of Object.entries(fields)) {
|
||||||
|
if (field.required && field.value === '') {
|
||||||
|
updateField(key, { error: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (field.notEmpty && field.value.length === 0) {
|
||||||
|
updateField(key, { error: true, notEmpty: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkIfJson(fields.configuration.value)) {
|
||||||
|
updateField('configuration', { error: true });
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (validation()) {
|
||||||
|
setLoading(true);
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
name: fields.name.value,
|
||||||
|
description: fields.description.value,
|
||||||
|
modelIds: fields.modelIds.value,
|
||||||
|
configuration: fields.configuration.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.put(`${endpoints.owgw}/api/v1/default_configuration/${configId}`, parameters, options)
|
||||||
|
.then(() => {
|
||||||
|
if (refresh !== null) refresh();
|
||||||
|
toggle();
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('configuration.success_update'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_update', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEditing = () => {
|
||||||
|
if (editing) getConfig();
|
||||||
|
setEditing(!editing);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
setEditing(false);
|
||||||
|
getConfig();
|
||||||
|
getDeviceTypes();
|
||||||
|
}
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('configuration.edit_configuration')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.save')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={save}
|
||||||
|
disabled={!editing}
|
||||||
|
>
|
||||||
|
<CIcon content={cilSave} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.edit')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={toggleEditing}
|
||||||
|
disabled={editing}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPencil} />
|
||||||
|
</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 className="px-5">
|
||||||
|
<Form
|
||||||
|
t={t}
|
||||||
|
disable={loading}
|
||||||
|
fields={fields}
|
||||||
|
editing={editing}
|
||||||
|
updateField={updateFieldWithId}
|
||||||
|
updateFieldWithKey={updateField}
|
||||||
|
deviceTypes={deviceTypes}
|
||||||
|
show={show}
|
||||||
|
/>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditConfigurationModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
configId: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditConfigurationModal.defaultProps = {
|
||||||
|
refresh: null,
|
||||||
|
configId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditConfigurationModal;
|
||||||
54
src/components/EditFirmwareModal/Form.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CCardBody, CCol, CInput, CRow } from '@coreui/react';
|
||||||
|
import { prettyDate, cleanBytesString } from 'utils/helper';
|
||||||
|
|
||||||
|
const FirmwareDetailsForm = ({ t, fields, updateFieldsWithId, editing }) => (
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CRow>
|
||||||
|
<CCol sm="2">{t('firmware.release')}</CCol>
|
||||||
|
<CCol sm="4">{fields.release.value}</CCol>
|
||||||
|
<CCol sm="2">{t('common.created')}</CCol>
|
||||||
|
<CCol sm="4">{prettyDate(fields.created.value)}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-3">
|
||||||
|
<CCol sm="2">{t('firmware.image_date')}</CCol>
|
||||||
|
<CCol sm="4">{prettyDate(fields.imageDate.value)}</CCol>
|
||||||
|
<CCol sm="2">{t('firmware.size')}</CCol>
|
||||||
|
<CCol sm="4">{cleanBytesString(fields.size.value)}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-3">
|
||||||
|
<CCol sm="2">{t('firmware.image')}</CCol>
|
||||||
|
<CCol sm="4">{fields.image.value}</CCol>
|
||||||
|
<CCol sm="2">{t('firmware.revision')}</CCol>
|
||||||
|
<CCol sm="4">{fields.revision.value}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-3">
|
||||||
|
<CCol sm="2">URI</CCol>
|
||||||
|
<CCol sm="4">{fields.uri.value}</CCol>
|
||||||
|
<CCol sm="2" className="mt-2">
|
||||||
|
{t('user.description')}
|
||||||
|
</CCol>
|
||||||
|
<CCol sm="4">
|
||||||
|
{editing ? (
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
value={fields.description.value}
|
||||||
|
onChange={updateFieldsWithId}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 mb-0">{fields.description.value}</p>
|
||||||
|
)}
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
);
|
||||||
|
|
||||||
|
FirmwareDetailsForm.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
fields: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
updateFieldsWithId: PropTypes.func.isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
export default FirmwareDetailsForm;
|
||||||
@@ -16,13 +16,8 @@ import {
|
|||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilPencil, cilSave, cilX } from '@coreui/icons';
|
import { cilPencil, cilSave, cilX } from '@coreui/icons';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import {
|
import { useFormFields, useAuth, useToast, DetailedNotesTable } from 'ucentral-libs';
|
||||||
useFormFields,
|
import Form from './Form';
|
||||||
useAuth,
|
|
||||||
useToast,
|
|
||||||
FirmwareDetailsForm,
|
|
||||||
DetailedNotesTable,
|
|
||||||
} from 'ucentral-libs';
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
created: {
|
created: {
|
||||||
@@ -237,12 +232,7 @@ const EditFirmwareModal = ({ show, toggle, firmwareId, refreshTable }) => {
|
|||||||
<CTabContent>
|
<CTabContent>
|
||||||
<CTabPane active={index === 0} className="pt-2">
|
<CTabPane active={index === 0} className="pt-2">
|
||||||
{index === 0 ? (
|
{index === 0 ? (
|
||||||
<FirmwareDetailsForm
|
<Form t={t} fields={firmware} updateFieldsWithId={updateWithId} editing={editing} />
|
||||||
t={t}
|
|
||||||
fields={firmware}
|
|
||||||
updateFieldsWithId={updateWithId}
|
|
||||||
editing={editing}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</CTabPane>
|
</CTabPane>
|
||||||
<CTabPane active={index === 1}>
|
<CTabPane active={index === 1}>
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
|
||||||
import { useUser, EditUserModal as Modal, useAuth, useToast } from 'ucentral-libs';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
Id: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
changePassword: {
|
|
||||||
value: false,
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
currentPassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
userRole: {
|
|
||||||
value: 'accounting',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
value: [],
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditUserModal = ({ show, toggle, userId, getUsers, policies }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentToken, endpoints } = useAuth();
|
|
||||||
const { addToast } = useToast();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [initialUser, setInitialUser] = useState({});
|
|
||||||
const [editing, setEditing] = useState(false);
|
|
||||||
const [user, updateWithId, updateWithKey, setUser] = useUser(initialState);
|
|
||||||
|
|
||||||
const getUser = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(`${endpoints.owsec}/api/v1/user/${userId}`, options)
|
|
||||||
.then((response) => {
|
|
||||||
const newUser = {};
|
|
||||||
|
|
||||||
for (const key of Object.keys(response.data)) {
|
|
||||||
if (key in initialState && key !== 'currentPassword') {
|
|
||||||
newUser[key] = {
|
|
||||||
...initialState[key],
|
|
||||||
value: response.data[key],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setInitialUser({ ...initialState, ...newUser });
|
|
||||||
setUser({ ...initialState, ...newUser });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('user.error_retrieving'),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
toggle();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleEditing = () => {
|
|
||||||
if (editing) {
|
|
||||||
getUser();
|
|
||||||
}
|
|
||||||
setEditing(!editing);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateUser = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
id: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
let newData = false;
|
|
||||||
|
|
||||||
for (const key of Object.keys(user)) {
|
|
||||||
if (user[key].editable && user[key].value !== initialUser[key].value) {
|
|
||||||
if (key === 'currentPassword' && user[key].length < 8) {
|
|
||||||
updateWithKey('currentPassword', {
|
|
||||||
error: true,
|
|
||||||
});
|
|
||||||
newData = false;
|
|
||||||
break;
|
|
||||||
} else if (key === 'changePassword') {
|
|
||||||
parameters[key] = user[key].value === 'on';
|
|
||||||
newData = true;
|
|
||||||
} else {
|
|
||||||
parameters[key] = user[key].value;
|
|
||||||
newData = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNotes = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < user.notes.value.length; i += 1) {
|
|
||||||
if (user.notes.value[i].new) newNotes.push({ note: user.notes.value[i].note });
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters.notes = newNotes;
|
|
||||||
|
|
||||||
if (newData || newNotes.length > 0) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.put(`${endpoints.owsec}/api/v1/user/${userId}`, parameters, options)
|
|
||||||
.then(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_success_title'),
|
|
||||||
body: t('user.update_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
getUsers();
|
|
||||||
toggle();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_failure_title'),
|
|
||||||
body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
getUser();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_success_title'),
|
|
||||||
body: t('user.update_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
getUsers();
|
|
||||||
toggle();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addNote = (currentNote) => {
|
|
||||||
const newNotes = [...user.notes.value];
|
|
||||||
newNotes.unshift({
|
|
||||||
note: currentNote,
|
|
||||||
new: true,
|
|
||||||
created: new Date().getTime() / 1000,
|
|
||||||
createdBy: '',
|
|
||||||
});
|
|
||||||
updateWithKey('notes', { value: newNotes });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userId) {
|
|
||||||
getUser();
|
|
||||||
}
|
|
||||||
}, [userId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (show) {
|
|
||||||
getUser();
|
|
||||||
setEditing(false);
|
|
||||||
}
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
t={t}
|
|
||||||
user={user}
|
|
||||||
updateUserWithId={updateWithId}
|
|
||||||
saveUser={updateUser}
|
|
||||||
loading={loading}
|
|
||||||
policies={policies}
|
|
||||||
show={show}
|
|
||||||
toggle={toggle}
|
|
||||||
editing={editing}
|
|
||||||
toggleEditing={toggleEditing}
|
|
||||||
addNote={addNote}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EditUserModal.propTypes = {
|
|
||||||
userId: PropTypes.string.isRequired,
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
toggle: PropTypes.func.isRequired,
|
|
||||||
getUsers: PropTypes.func.isRequired,
|
|
||||||
policies: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(EditUserModal);
|
|
||||||
45
src/components/EventQueueModal/Modal.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CModal,
|
||||||
|
CModalBody,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CSpinner,
|
||||||
|
CPopover,
|
||||||
|
CButton,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX } from '@coreui/icons';
|
||||||
|
|
||||||
|
const EventQueueModal = ({ t, show, toggle, loading, result }) => (
|
||||||
|
<CModal size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('commands.event_queue')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody className="text-center">
|
||||||
|
{loading ? (
|
||||||
|
<CSpinner color="primary" size="lg" />
|
||||||
|
) : (
|
||||||
|
<pre className="ignore text-left">{JSON.stringify(result, null, 4)}</pre>
|
||||||
|
)}
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
|
||||||
|
EventQueueModal.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
result: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventQueueModal;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EventQueueModal as Modal, useAuth, useDevice, useToast } from 'ucentral-libs';
|
import { useAuth, useDevice, useToast } from 'ucentral-libs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
const EventQueueModal = ({ show, toggle }) => {
|
const EventQueueModal = ({ show, toggle }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
329
src/components/FirmwareDashboard/Dashboard/index.js
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardHeader,
|
||||||
|
CCol,
|
||||||
|
CDataTable,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CSpinner,
|
||||||
|
CWidgetIcon,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CChartBar, CChartHorizontalBar, CChartPie } from '@coreui/react-chartjs';
|
||||||
|
import { cilClock, cilHappy, cilMeh, cilFrown, cilBirthdayCake, cilInfo } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const getLatestColor = (percent = 0) => {
|
||||||
|
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
|
||||||
|
if (numberPercent >= 90) return 'success';
|
||||||
|
if (numberPercent > 60) return 'warning';
|
||||||
|
return 'danger';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLatestIcon = (percent = 0) => {
|
||||||
|
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
|
||||||
|
if (numberPercent >= 90) return <CIcon width={36} name="cil-happy" content={cilHappy} />;
|
||||||
|
if (numberPercent > 60) return <CIcon width={36} name="cil-meh" content={cilMeh} />;
|
||||||
|
return <CIcon width={36} name="cil-frown" content={cilFrown} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FirmwareDashboard = ({ t, data, loading }) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'endpoint', label: t('common.endpoint'), filter: false, sorter: false },
|
||||||
|
{ key: 'devices', label: t('common.devices') },
|
||||||
|
{ key: 'percent', label: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{ opacity: loading ? '20%' : '100%' }}>
|
||||||
|
<CRow className="mt-3">
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.last_dashboard_refresh')}
|
||||||
|
header={data.snapshot ? <FormattedDate date={data.snapshot} size="lg" /> : <h2>-</h2>}
|
||||||
|
color="info"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-clock" content={cilClock} />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.up_to_date')}
|
||||||
|
header={<h2>{data.latestSoftwareRate}</h2>}
|
||||||
|
color={getLatestColor(data.latestSoftwareRate)}
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
{getLatestIcon(data.latestSoftwareRate)}
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.devices')}
|
||||||
|
header={<h2>{data.numberOfDevices}</h2>}
|
||||||
|
color="primary"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-router" />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('firmware.average_age')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('firmware.age_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header={<h2>{data.averageFirmwareAge}</h2>}
|
||||||
|
color="dark"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} content={cilBirthdayCake} />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.firmware_installed')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.firmwareDistribution.datasets}
|
||||||
|
labels={data.firmwareDistribution.labels}
|
||||||
|
options={{
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.devices_using_latest')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('firmware.latest_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.latest.datasets}
|
||||||
|
labels={data.latest.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">Unknown Firmware</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartHorizontalBar
|
||||||
|
datasets={data.unknownFirmwares.datasets}
|
||||||
|
labels={data.unknownFirmwares.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => value.split(' ')[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.device_status')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.status.datasets}
|
||||||
|
labels={data.status.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) => `${ds.datasets[0].data[item.index]}%`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('firmware.device_types')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.deviceType.datasets}
|
||||||
|
labels={data.deviceType.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">OUIs</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartHorizontalBar
|
||||||
|
datasets={data.ouis.datasets}
|
||||||
|
labels={data.ouis.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => value.split(' ')[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.endpoints')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="table-sm"
|
||||||
|
items={data.endpoints ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol />
|
||||||
|
<CCol />
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div className={styles.centerContainer}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FirmwareDashboard.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(FirmwareDashboard);
|
||||||
10
src/components/FirmwareDashboard/Dashboard/index.module.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.centerContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
@@ -1,7 +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 { FirmwareDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
|
import { useAuth, COLOR_LIST } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
const FirmwareDashboard = () => {
|
const FirmwareDashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
36
src/components/FirmwareHistoryModal/Modal.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CDataTable } from '@coreui/react';
|
||||||
|
import { prettyDate } from 'utils/helper';
|
||||||
|
|
||||||
|
const FirmwareHistoryModal = ({ t, loading, data }) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'date', label: '#', _style: { width: '20%' } },
|
||||||
|
{ key: 'fromRelease', label: t('firmware.from_release'), sorter: false },
|
||||||
|
{ key: 'toRelease', label: t('firmware.to_release'), sorter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
fields={columns}
|
||||||
|
items={data}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
sorter
|
||||||
|
sorterValue={{ column: 'radio', asc: true }}
|
||||||
|
scopedSlots={{
|
||||||
|
date: (item) => <td>{prettyDate(item.upgraded)}</td>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FirmwareHistoryModal.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(FirmwareHistoryModal);
|
||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
CModalFooter,
|
CModalFooter,
|
||||||
CModalTitle,
|
CModalTitle,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import { FirmwareHistoryTable, useAuth } from 'ucentral-libs';
|
import { useAuth } from 'ucentral-libs';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
|
const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -51,7 +52,7 @@ const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
|
|||||||
</CModalTitle>
|
</CModalTitle>
|
||||||
</CModalHeader>
|
</CModalHeader>
|
||||||
<CModalBody>
|
<CModalBody>
|
||||||
<FirmwareHistoryTable t={t} loading={loading} data={data} />
|
<Modal t={t} loading={loading} data={data} />
|
||||||
</CModalBody>
|
</CModalBody>
|
||||||
<CModalFooter>
|
<CModalFooter>
|
||||||
<CButton color="secondary" onClick={toggle}>
|
<CButton color="secondary" onClick={toggle}>
|
||||||
|
|||||||
55
src/components/NetworkDiagram/Graph.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactFlow, { removeElements, MiniMap, Controls, Background } from 'react-flow-renderer';
|
||||||
|
|
||||||
|
const NetworkDiagram = ({ show, elements, setElements }) => {
|
||||||
|
const [reactFlowInstance, setReactFlowInstance] = useState(null);
|
||||||
|
const onElementsRemove = (elementsToRemove) => {
|
||||||
|
setElements((els) => removeElements(elementsToRemove, els));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLoad = (instance) => {
|
||||||
|
setReactFlowInstance(instance);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show && reactFlowInstance !== null && elements.length > 0) {
|
||||||
|
setTimeout(() => reactFlowInstance.fitView(), 100);
|
||||||
|
}
|
||||||
|
}, [show, reactFlowInstance, elements]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '80vh', width: '100%' }}>
|
||||||
|
<ReactFlow
|
||||||
|
elements={elements}
|
||||||
|
onElementsRemove={onElementsRemove}
|
||||||
|
onLoad={onLoad}
|
||||||
|
snapToGrid
|
||||||
|
snapGrid={[20, 20]}
|
||||||
|
>
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(n) => {
|
||||||
|
if (n.style?.background) return n.style.background;
|
||||||
|
|
||||||
|
return '#fff';
|
||||||
|
}}
|
||||||
|
nodeBorderRadius={5}
|
||||||
|
/>
|
||||||
|
<Controls />
|
||||||
|
<Background color="#aaa" gap={20} />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkDiagram.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
elements: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
setElements: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkDiagram.defaultProps = {
|
||||||
|
show: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NetworkDiagram);
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CRow, CCol } from '@coreui/react';
|
import { CRow, CCol } from '@coreui/react';
|
||||||
import { NetworkDiagram as Graph } from 'ucentral-libs';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import createLayoutedElements from './dagreAdapter';
|
import createLayoutedElements from './dagreAdapter';
|
||||||
|
import Graph from './Graph';
|
||||||
|
|
||||||
const associationStyle = {
|
const associationStyle = {
|
||||||
background: '#3399ff',
|
background: '#3399ff',
|
||||||
|
|||||||
42
src/components/WifiAnalysis/RadioAnalysis.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CDataTable } from '@coreui/react';
|
||||||
|
|
||||||
|
const RadioAnalysisTable = ({ data, loading }) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'radio', label: '#', _style: { width: '5%' } },
|
||||||
|
{ key: 'channel', label: 'Ch', _style: { width: '5%' } },
|
||||||
|
{ key: 'channelWidth', label: 'C Width', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'noise', label: 'Noise', _style: { width: '4%' }, sorter: false },
|
||||||
|
{ key: 'txPower', label: 'Tx Power', _style: { width: '9%' }, sorter: false },
|
||||||
|
{ key: 'activeMs', label: 'Active MS', _style: { width: '23%' }, sorter: false },
|
||||||
|
{ key: 'busyMs', label: 'Busy MS', _style: { width: '23%' }, sorter: false },
|
||||||
|
{ key: 'receiveMs', label: 'Receive MS', _style: { width: '23%' }, sorter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const centerIfEmpty = (value) => (
|
||||||
|
<td className={!value || value === '' || value === '-' ? 'text-center' : ''}>{value}</td>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="table-sm"
|
||||||
|
fields={columns}
|
||||||
|
items={data}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
sorter
|
||||||
|
sorterValue={{ column: 'radio', asc: true }}
|
||||||
|
scopedSlots={{
|
||||||
|
noise: (item) => centerIfEmpty(item.noise),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RadioAnalysisTable.propTypes = {
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
export default RadioAnalysisTable;
|
||||||
117
src/components/WifiAnalysis/WifiAnalysis.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CDataTable,
|
||||||
|
CPopover,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX } from '@coreui/icons';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
|
||||||
|
const WifiAnalysisTable = ({ t, data, loading }) => {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [ips, setIps] = useState(null);
|
||||||
|
|
||||||
|
const toggle = (ssid, v4, v6) => {
|
||||||
|
if (v4 && v6) setIps({ ssid, v4, v6 });
|
||||||
|
setShow(!show);
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{ key: 'radio', label: '#', _style: { width: '5%' } },
|
||||||
|
{ key: 'bssid', label: 'BSSID', _style: { width: '14%' } },
|
||||||
|
{ key: 'mode', label: t('wifi_analysis.mode'), _style: { width: '9%' }, sorter: false },
|
||||||
|
{ key: 'ssid', label: 'SSID', _style: { width: '17%' } },
|
||||||
|
{ key: 'rssi', label: 'RSSI', _style: { width: '5%' }, sorter: false },
|
||||||
|
{ key: 'rxRate', label: 'Rx Rate', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'rxBytes', label: 'Rx', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'rxMcs', label: 'Rx MCS', _style: { width: '6%' }, sorter: false },
|
||||||
|
{ key: 'rxNss', label: 'Rx NSS', _style: { width: '6%' }, sorter: false },
|
||||||
|
{ key: 'txRate', label: 'Tx Rate', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'txBytes', label: 'Tx', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'ips', label: 'IP', _style: { width: '6%' }, sorter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const centerIfEmpty = (value) => (
|
||||||
|
<td className={!value || value === '' || value === '-' ? 'text-center' : ''}>{value}</td>
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayIp = (ssid, v4, v6) => {
|
||||||
|
const count = v4.length + v6.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td className="ignore-overflow text-center">
|
||||||
|
{count > 0 ? (
|
||||||
|
<CPopover content="View">
|
||||||
|
<CButton color="primary" size="sm" onClick={() => toggle(ssid, v4, v6)}>
|
||||||
|
{count}
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
) : (
|
||||||
|
<p>{count}</p>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow mb-5 table-sm"
|
||||||
|
fields={columns}
|
||||||
|
items={data}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
sorter
|
||||||
|
sorterValue={{ column: 'radio', asc: true }}
|
||||||
|
scopedSlots={{
|
||||||
|
radio: (item) => <td className="text-center">{item.radio.radio}</td>,
|
||||||
|
rxMcs: (item) => centerIfEmpty(item.rxMcs),
|
||||||
|
rxNss: (item) => centerIfEmpty(item.rxNss),
|
||||||
|
rssi: (item) => centerIfEmpty(item.rssi),
|
||||||
|
ips: (item) => displayIp(item.ssid, item.ipV4, item.ipV6),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CModal size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{ips?.ssid}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody>
|
||||||
|
<div>
|
||||||
|
IpV4
|
||||||
|
<ul>
|
||||||
|
{ips?.v4?.map((ip) => (
|
||||||
|
<li key={createUuid()}>{ip}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
IpV6
|
||||||
|
<ul>
|
||||||
|
{ips?.v6?.map((ip) => (
|
||||||
|
<li key={createUuid()}>{ip}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
WifiAnalysisTable.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
export default WifiAnalysisTable;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { WifiAnalysisTable, RadioAnalysisTable, useAuth } from 'ucentral-libs';
|
import { useAuth } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import NetworkDiagram from 'components/NetworkDiagram';
|
import NetworkDiagram from 'components/NetworkDiagram';
|
||||||
import { cleanBytesString, prettyDate, compactSecondsToDetailed } from 'utils/helper';
|
import { cleanBytesString, prettyDate, compactSecondsToDetailed } from 'utils/helper';
|
||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilX } from '@coreui/icons';
|
import { cilX } from '@coreui/icons';
|
||||||
|
import RadioAnalysisTable from './RadioAnalysis';
|
||||||
|
import WifiAnalysisTable from './WifiAnalysis';
|
||||||
|
|
||||||
const parseDbm = (value) => {
|
const parseDbm = (value) => {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import routes from 'routes';
|
import routes from 'routes';
|
||||||
|
import { CSidebarNavItem } from '@coreui/react';
|
||||||
|
import { cilBarcode, cilRouter, cilSave, cilSettings, cilPeople } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
import { Header, Sidebar, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs';
|
import { Header, Sidebar, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs';
|
||||||
|
|
||||||
const TheLayout = () => {
|
const TheLayout = () => {
|
||||||
@@ -8,40 +11,46 @@ const TheLayout = () => {
|
|||||||
const { endpoints, currentToken, user, avatar, logout } = useAuth();
|
const { endpoints, currentToken, user, avatar, logout } = useAuth();
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
const navigation = [
|
|
||||||
{
|
|
||||||
_tag: 'CSidebarNavItem',
|
|
||||||
name: t('common.devices'),
|
|
||||||
icon: 'cilRouter',
|
|
||||||
to: '/devices',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_tag: 'CSidebarNavItem',
|
|
||||||
name: t('firmware.title'),
|
|
||||||
icon: 'cilSave',
|
|
||||||
to: '/firmware',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_tag: 'CSidebarNavItem',
|
|
||||||
name: t('user.users'),
|
|
||||||
to: '/users',
|
|
||||||
icon: 'cilPeople',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_tag: 'CSidebarNavItem',
|
|
||||||
name: t('common.system'),
|
|
||||||
to: '/system',
|
|
||||||
icon: 'cilSettings',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="c-app c-default-layout">
|
<div className="c-app c-default-layout">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
showSidebar={showSidebar}
|
showSidebar={showSidebar}
|
||||||
setShowSidebar={setShowSidebar}
|
setShowSidebar={setShowSidebar}
|
||||||
logo="assets/OpenWiFi_LogoLockup_WhiteColour.svg"
|
logo="assets/OpenWiFi_LogoLockup_WhiteColour.svg"
|
||||||
options={navigation}
|
options={
|
||||||
|
<>
|
||||||
|
<CSidebarNavItem
|
||||||
|
className="font-weight-bold"
|
||||||
|
name={t('common.devices')}
|
||||||
|
to="/devices"
|
||||||
|
icon={<CIcon content={cilRouter} size="xl" className="mr-3" />}
|
||||||
|
/>
|
||||||
|
<CSidebarNavItem
|
||||||
|
className="font-weight-bold"
|
||||||
|
name={t('firmware.title')}
|
||||||
|
to="/firmware"
|
||||||
|
icon={<CIcon content={cilSave} size="xl" className="mr-3" />}
|
||||||
|
/>
|
||||||
|
<CSidebarNavItem
|
||||||
|
className="font-weight-bold"
|
||||||
|
name={t('configuration.default_configs')}
|
||||||
|
to="/defaultconfigurations"
|
||||||
|
icon={<CIcon content={cilBarcode} size="xl" className="mr-3" />}
|
||||||
|
/>
|
||||||
|
<CSidebarNavItem
|
||||||
|
className="font-weight-bold"
|
||||||
|
name={t('user.users')}
|
||||||
|
to="/users"
|
||||||
|
icon={<CIcon content={cilPeople} size="xl" className="mr-3" />}
|
||||||
|
/>
|
||||||
|
<CSidebarNavItem
|
||||||
|
className="font-weight-bold"
|
||||||
|
name={t('common.system')}
|
||||||
|
to="/system"
|
||||||
|
icon={<CIcon content={cilSettings} size="xl" className="mr-3" />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
redirectTo="/devices"
|
redirectTo="/devices"
|
||||||
/>
|
/>
|
||||||
<div className="c-wrapper">
|
<div className="c-wrapper">
|
||||||
|
|||||||
6
src/pages/DefaultConfigurationsPage/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DefaultConfigurationTable from 'components/DefaultConfigurationTable';
|
||||||
|
|
||||||
|
const DefaultConfigurationsPage = () => <DefaultConfigurationTable />;
|
||||||
|
|
||||||
|
export default DefaultConfigurationsPage;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import DeviceList from 'components/DeviceListTable';
|
import DeviceList from 'components/DeviceListTable';
|
||||||
import DeviceDashboard from 'components/DeviceDashboard';
|
import DeviceDashboard from 'components/DeviceDashboard';
|
||||||
|
import BlacklistTable from 'components/BlacklistTable';
|
||||||
import {
|
import {
|
||||||
CCard,
|
CCard,
|
||||||
CCardBody,
|
CCardBody,
|
||||||
@@ -48,16 +49,21 @@ const DeviceListPage = () => {
|
|||||||
active={index === 1}
|
active={index === 1}
|
||||||
onClick={() => updateNav(1)}
|
onClick={() => updateNav(1)}
|
||||||
>
|
>
|
||||||
{t('common.table')}
|
{t('common.all')}
|
||||||
|
</CNavLink>
|
||||||
|
<CNavLink
|
||||||
|
className="font-weight-bold"
|
||||||
|
href="#"
|
||||||
|
active={index === 2}
|
||||||
|
onClick={() => updateNav(2)}
|
||||||
|
>
|
||||||
|
{t('common.blacklist')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
</CNav>
|
</CNav>
|
||||||
<CTabContent>
|
<CTabContent>
|
||||||
<CTabPane active={index === 0}>
|
<CTabPane active={index === 0}>{index === 0 ? <DeviceDashboard /> : null}</CTabPane>
|
||||||
<DeviceDashboard />
|
<CTabPane active={index === 1}>{index === 1 ? <DeviceList /> : null}</CTabPane>
|
||||||
</CTabPane>
|
<CTabPane active={index === 2}>{index === 2 ? <BlacklistTable /> : null}</CTabPane>
|
||||||
<CTabPane active={index === 1}>
|
|
||||||
<DeviceList />
|
|
||||||
</CTabPane>
|
|
||||||
</CTabContent>
|
</CTabContent>
|
||||||
</CCardBody>
|
</CCardBody>
|
||||||
</CCard>
|
</CCard>
|
||||||
|
|||||||
181
src/pages/DevicePage/Details.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CCardBody,
|
||||||
|
CPopover,
|
||||||
|
CButton,
|
||||||
|
CSpinner,
|
||||||
|
CLabel,
|
||||||
|
CLink,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilSync } from '@coreui/icons';
|
||||||
|
import { prettyDate } from 'utils/helper';
|
||||||
|
import { CopyToClipboardButton, HideTextButton } from 'ucentral-libs';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const DeviceDetails = ({ t, loading, getData, status, deviceConfig, lastStats }) => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const toggleShowPassword = () => {
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPassword = () => {
|
||||||
|
const password =
|
||||||
|
deviceConfig?.devicePassword === '' ? 'openwifi' : deviceConfig?.devicePassword;
|
||||||
|
return showPassword ? password : '******';
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayExtra = (key, value, extraData) => {
|
||||||
|
if (!extraData || !extraData[key]) return value;
|
||||||
|
|
||||||
|
if (!localStorage.getItem('owprov-ui') || key === 'owner') return extraData[key].name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link align-self-center"
|
||||||
|
aria-current="page"
|
||||||
|
href={`${localStorage.getItem('owprov-ui')}/#/${key === 'entity' ? 'entity' : 'venue'}/${
|
||||||
|
extraData[key].id
|
||||||
|
}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{extraData[key].name}
|
||||||
|
</CLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CCard className="m-0">
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.refresh')}>
|
||||||
|
<CButton size="sm" color="info" onClick={getData}>
|
||||||
|
<CIcon content={cilSync} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
{(!lastStats || !status) && loading ? (
|
||||||
|
<div className={styles.centerContainer}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div className={styles.overlayContainer} hidden={!loading}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
<CRow>
|
||||||
|
<CCol lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>{t('common.serial_num')}: </CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol className="border-right" lg="2" xl="3" xxl="3">
|
||||||
|
{deviceConfig?.serialNumber}
|
||||||
|
{' '}
|
||||||
|
<CopyToClipboardButton t={t} size="sm" content={deviceConfig?.serialNumber} />
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel className="align-middle">{t('configuration.device_password')}: </CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="3" xxl="3">
|
||||||
|
{getPassword()}
|
||||||
|
{' '}
|
||||||
|
<HideTextButton t={t} toggle={toggleShowPassword} show={showPassword} />
|
||||||
|
<CopyToClipboardButton
|
||||||
|
t={t}
|
||||||
|
size="sm"
|
||||||
|
content={
|
||||||
|
deviceConfig?.devicePassword === '' ? 'openwifi' : deviceConfig?.devicePassword
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
<CCol className="border-left" lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>{t('configuration.owner')}:</CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="3" xxl="3">
|
||||||
|
{deviceConfig?.owner}
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>{t('common.mac')}:</CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol className="border-right" lg="2" xl="3" xxl="3">
|
||||||
|
{deviceConfig?.macAddress}
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>{t('configuration.type')}: </CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="3" xxl="3">
|
||||||
|
{deviceConfig?.deviceType}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="border-left" lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>
|
||||||
|
{deviceConfig?.venue?.substring(0, 3) === 'ent'
|
||||||
|
? t('entity.entity')
|
||||||
|
: t('inventory.venue')}
|
||||||
|
:
|
||||||
|
</CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="3" xxl="3">
|
||||||
|
{deviceConfig?.venue?.substring(0, 3) === 'ent'
|
||||||
|
? displayExtra(
|
||||||
|
'entity',
|
||||||
|
deviceConfig?.venue?.slice(4),
|
||||||
|
deviceConfig?.extendedInfo,
|
||||||
|
)
|
||||||
|
: displayExtra(
|
||||||
|
'venue',
|
||||||
|
deviceConfig?.venue?.slice(4),
|
||||||
|
deviceConfig?.extendedInfo,
|
||||||
|
)}
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>{t('common.manufacturer')}:</CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol className="border-right" lg="2" xl="3" xxl="3">
|
||||||
|
{deviceConfig?.manufacturer}
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>{t('configuration.created')}: </CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="3" xxl="3">
|
||||||
|
{prettyDate(deviceConfig?.createdTimestamp)}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="border-left" lg="2" xl="1" xxl="1">
|
||||||
|
<CLabel>{t('configuration.location')}:</CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="2" xl="3" xxl="3">
|
||||||
|
{deviceConfig?.location}
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceDetails.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
getData: PropTypes.func.isRequired,
|
||||||
|
status: PropTypes.instanceOf(Object),
|
||||||
|
deviceConfig: PropTypes.instanceOf(Object),
|
||||||
|
lastStats: PropTypes.instanceOf(Object),
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceDetails.defaultProps = {
|
||||||
|
status: null,
|
||||||
|
lastStats: null,
|
||||||
|
deviceConfig: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceDetails);
|
||||||
35
src/pages/DevicePage/DeviceStatusCard/MemoryBar.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CPopover, CProgress, CProgressBar } from '@coreui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { cleanBytesString } from 'utils/helper';
|
||||||
|
|
||||||
|
const MemoryBar = ({ t, usedBytes, totalBytes }) => {
|
||||||
|
const used = cleanBytesString(usedBytes);
|
||||||
|
const total = cleanBytesString(totalBytes);
|
||||||
|
const percentage = Math.floor((usedBytes / totalBytes) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CPopover content={t('status.used_total_memory', { used, total })}>
|
||||||
|
<CProgress>
|
||||||
|
<CProgressBar value={percentage}>
|
||||||
|
{percentage >= 25 ? t('status.percentage_used', { percentage, total }) : ''}
|
||||||
|
</CProgressBar>
|
||||||
|
<CProgressBar value={100 - percentage} color="transparent">
|
||||||
|
<div style={{ color: 'black' }}>
|
||||||
|
{percentage < 25
|
||||||
|
? t('status.percentage_free', { percentage: 100 - percentage, total })
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</CProgressBar>
|
||||||
|
</CProgress>
|
||||||
|
</CPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MemoryBar.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
usedBytes: PropTypes.number.isRequired,
|
||||||
|
totalBytes: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(MemoryBar);
|
||||||
203
src/pages/DevicePage/DeviceStatusCard/index.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/* eslint-disable jsx-a11y/img-redundant-alt */
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CCardBody,
|
||||||
|
CBadge,
|
||||||
|
CAlert,
|
||||||
|
CPopover,
|
||||||
|
CButton,
|
||||||
|
CSpinner,
|
||||||
|
CLabel,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilSync } from '@coreui/icons';
|
||||||
|
import { prettyDate, compactSecondsToDetailed } from 'utils/helper';
|
||||||
|
import MemoryBar from './MemoryBar';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const errorField = (t) => (
|
||||||
|
<CAlert className="py-0" color="danger">
|
||||||
|
{t('status.error')}
|
||||||
|
</CAlert>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeviceStatusCard = ({
|
||||||
|
t,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
deviceSerialNumber,
|
||||||
|
getData,
|
||||||
|
status,
|
||||||
|
deviceConfig,
|
||||||
|
lastStats,
|
||||||
|
}) => (
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.refresh')}>
|
||||||
|
<CButton size="sm" color="info" onClick={getData}>
|
||||||
|
<CIcon content={cilSync} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
<div className="text-value-lg mr-auto">
|
||||||
|
{deviceSerialNumber}, {deviceConfig?.compatible}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
{(!lastStats || !status) && loading ? (
|
||||||
|
<div className={styles.centerContainer}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div className={styles.overlayContainer} hidden={!loading}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
<CRow>
|
||||||
|
<CCol md="5" lg="5" xl="4" className="text-center align-middle bg-light">
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
maxHeight: '250px',
|
||||||
|
maxWidth: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
}}
|
||||||
|
src={`assets/devices/${deviceConfig?.compatible}.png`}
|
||||||
|
alt="Image not found"
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.onerror = null;
|
||||||
|
e.target.src = 'assets/NotFound.png';
|
||||||
|
}}
|
||||||
|
height="auto"
|
||||||
|
width="auto"
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
<CCol md="7" lg="7" xl="8" className="border-left">
|
||||||
|
<CRow>
|
||||||
|
<CCol className="mb-1" md="4" xl="4">
|
||||||
|
{t('status.connection_status')}:
|
||||||
|
</CCol>
|
||||||
|
<CCol className="mb-1" md="8" xl="8">
|
||||||
|
{status?.connected ? (
|
||||||
|
<CBadge color="success">{t('common.connected')}</CBadge>
|
||||||
|
) : (
|
||||||
|
<CBadge color="danger">{t('common.not_connected')}</CBadge>
|
||||||
|
)}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="my-1" md="4" xl="4">
|
||||||
|
{t('status.uptime')}:
|
||||||
|
</CCol>
|
||||||
|
<CCol className="my-1" md="8" xl="8">
|
||||||
|
{error
|
||||||
|
? errorField(t)
|
||||||
|
: compactSecondsToDetailed(
|
||||||
|
lastStats?.unit?.uptime,
|
||||||
|
t('common.day'),
|
||||||
|
t('common.days'),
|
||||||
|
t('common.seconds'),
|
||||||
|
)}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="my-1" md="4" xl="4">
|
||||||
|
{t('status.last_contact')}:
|
||||||
|
</CCol>
|
||||||
|
<CCol className="my-1" md="8" xl="8">
|
||||||
|
{error ? errorField(t) : prettyDate(status?.lastContact)}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="my-1" md="4" xl="4">
|
||||||
|
{t('status.localtime')}:
|
||||||
|
</CCol>
|
||||||
|
<CCol className="my-1" md="8" xl="8">
|
||||||
|
{error ? errorField(t) : prettyDate(lastStats?.unit?.localtime)}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="mt-1" md="4" xl="4">
|
||||||
|
<CLabel>{t('firmware.revision')}: </CLabel>
|
||||||
|
</CCol>
|
||||||
|
<CCol className="mt-1" md="8" xl="8">
|
||||||
|
<CPopover content={deviceConfig?.firmware}>
|
||||||
|
<CLabel>
|
||||||
|
{deviceConfig?.firmware?.split(' / ').length > 1
|
||||||
|
? deviceConfig.firmware.split(' / ')[1]
|
||||||
|
: deviceConfig?.firmware}
|
||||||
|
</CLabel>
|
||||||
|
</CPopover>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol className="mb-1" md="4" xl="4">
|
||||||
|
{t('status.load_averages')}:
|
||||||
|
</CCol>
|
||||||
|
<CCol className="mb-1" md="8" xl="8">
|
||||||
|
{error ? (
|
||||||
|
errorField(t)
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{lastStats?.unit?.load[0] !== undefined
|
||||||
|
? (lastStats?.unit?.load[0] * 100).toFixed(2)
|
||||||
|
: '-'}
|
||||||
|
%{' / '}
|
||||||
|
{lastStats?.unit?.load[1] !== undefined
|
||||||
|
? (lastStats?.unit?.load[1] * 100).toFixed(2)
|
||||||
|
: '-'}
|
||||||
|
%{' / '}
|
||||||
|
{lastStats?.unit?.load[2] !== undefined
|
||||||
|
? (lastStats?.unit?.load[2] * 100).toFixed(2)
|
||||||
|
: '-'}
|
||||||
|
%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="mb-1" md="4" xl="4">
|
||||||
|
{t('status.memory')}:
|
||||||
|
</CCol>
|
||||||
|
<CCol className="mb-1" md="8" xl="8" style={{ paddingTop: '5px' }}>
|
||||||
|
{error ? (
|
||||||
|
errorField(t)
|
||||||
|
) : (
|
||||||
|
<MemoryBar
|
||||||
|
t={t}
|
||||||
|
usedBytes={
|
||||||
|
lastStats?.unit?.memory?.total && lastStats?.unit?.memory?.free
|
||||||
|
? lastStats?.unit?.memory?.total - lastStats?.unit?.memory?.free
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalBytes={lastStats?.unit?.memory?.total ?? 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
DeviceStatusCard.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.bool.isRequired,
|
||||||
|
deviceSerialNumber: PropTypes.string.isRequired,
|
||||||
|
getData: PropTypes.func.isRequired,
|
||||||
|
status: PropTypes.instanceOf(Object),
|
||||||
|
deviceConfig: PropTypes.instanceOf(Object),
|
||||||
|
lastStats: PropTypes.instanceOf(Object),
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceStatusCard.defaultProps = {
|
||||||
|
status: null,
|
||||||
|
lastStats: null,
|
||||||
|
deviceConfig: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceStatusCard);
|
||||||
20
src/pages/DevicePage/DeviceStatusCard/index.module.scss
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.centerContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
138
src/pages/DevicePage/NotesTab.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CCard, CCardHeader, CCardBody, CPopover, CButton } from '@coreui/react';
|
||||||
|
import { cilPencil, cilX, cilSave } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DetailedNotesTable, useAuth, useToast, useToggle } from 'ucentral-libs';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
|
||||||
|
const NotesTab = ({ deviceConfig, refresh }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentToken, endpoints, user } = useAuth();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const [editing, toggleEditing, setEditing] = useToggle(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [currentNotes, setCurrentNotes] = useState(deviceConfig.notes);
|
||||||
|
|
||||||
|
const stopEditing = () => {
|
||||||
|
setEditing(false);
|
||||||
|
refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNote = (currentNote) => {
|
||||||
|
const newNotes = currentNotes;
|
||||||
|
newNotes.unshift({
|
||||||
|
note: currentNote,
|
||||||
|
new: true,
|
||||||
|
created: new Date().getTime() / 1000,
|
||||||
|
createdBy: user?.email ?? '',
|
||||||
|
});
|
||||||
|
setCurrentNotes([...newNotes]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const newNotes = [];
|
||||||
|
for (let i = 0; i < currentNotes.length; i += 1) {
|
||||||
|
if (currentNotes[i].new) newNotes.push({ note: currentNotes[i].note });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
id: deviceConfig.serialNumber,
|
||||||
|
notes: newNotes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.put(`${endpoints.owgw}/api/v1/device/${deviceConfig.serialNumber}`, parameters, options)
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('firmware.update_success_title'),
|
||||||
|
body: t('firmware.update_success'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
toggleEditing();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('firmware.update_failure_title'),
|
||||||
|
body: t('firmware.update_failure', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentNotes(deviceConfig.notes);
|
||||||
|
}, [deviceConfig.notes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CCard className="m-0">
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
|
<div className="pl-2">
|
||||||
|
<CPopover content={t('common.save')}>
|
||||||
|
<CButton className="ml-2" size="sm" color="info" onClick={save} disabled={!editing}>
|
||||||
|
<CIcon content={cilSave} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.edit')}>
|
||||||
|
<CButton
|
||||||
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
color="dark"
|
||||||
|
onClick={toggleEditing}
|
||||||
|
disabled={editing}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPencil} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.stop_editing')}>
|
||||||
|
<CButton
|
||||||
|
className="ml-2"
|
||||||
|
size="sm"
|
||||||
|
color="dark"
|
||||||
|
onClick={stopEditing}
|
||||||
|
disabled={!editing}
|
||||||
|
>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<DetailedNotesTable
|
||||||
|
t={t}
|
||||||
|
notes={currentNotes}
|
||||||
|
addNote={addNote}
|
||||||
|
loading={loading}
|
||||||
|
editable={editing}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NotesTab.propTypes = {
|
||||||
|
deviceConfig: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
refresh: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotesTab;
|
||||||
@@ -7,10 +7,14 @@ import DeviceLogs from 'components/DeviceLogs';
|
|||||||
import DeviceStatisticsCard from 'components/InterfaceStatistics';
|
import DeviceStatisticsCard from 'components/InterfaceStatistics';
|
||||||
import DeviceActionCard from 'components/DeviceActionCard';
|
import DeviceActionCard from 'components/DeviceActionCard';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import { DeviceProvider, DeviceStatusCard, DeviceDetails, useAuth, useToast } from 'ucentral-libs';
|
import { DeviceProvider, useAuth, useToast } from 'ucentral-libs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ConfigurationDisplay from 'components/ConfigurationDisplay';
|
import ConfigurationDisplay from 'components/ConfigurationDisplay';
|
||||||
import WifiAnalysis from 'components/WifiAnalysis';
|
import WifiAnalysis from 'components/WifiAnalysis';
|
||||||
|
import CapabilitiesDisplay from 'components/CapabilitiesDisplay';
|
||||||
|
import NotesTab from './NotesTab';
|
||||||
|
import DeviceDetails from './Details';
|
||||||
|
import DeviceStatusCard from './DeviceStatusCard';
|
||||||
|
|
||||||
const DevicePage = () => {
|
const DevicePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -24,6 +28,11 @@ const DevicePage = () => {
|
|||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const updateNav = (target) => {
|
||||||
|
sessionStorage.setItem('devicePageIndex', target);
|
||||||
|
setIndex(target);
|
||||||
|
};
|
||||||
|
|
||||||
const getDevice = () => {
|
const getDevice = () => {
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -48,13 +57,14 @@ const DevicePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDeviceConfig(deviceInfo);
|
setDeviceConfig({ ...deviceInfo });
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response) setDeviceConfig({ ...deviceInfo, extendedInfo: response.data.extendedInfo });
|
if (response) setDeviceConfig({ ...deviceInfo, extendedInfo: response.data.extendedInfo });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
setDeviceConfig(null);
|
||||||
addToast({
|
addToast({
|
||||||
title: t('common.error'),
|
title: t('common.error'),
|
||||||
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
|
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
|
||||||
@@ -84,10 +94,13 @@ const DevicePage = () => {
|
|||||||
|
|
||||||
Promise.all([lastStatsRequest, statusRequest])
|
Promise.all([lastStatsRequest, statusRequest])
|
||||||
.then(([newStats, newStatus]) => {
|
.then(([newStats, newStatus]) => {
|
||||||
setLastStats(newStats.data);
|
setLastStats({ ...newStats.data });
|
||||||
setStatus(newStatus.data);
|
setStatus({ ...newStatus.data });
|
||||||
|
setError(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
setLastStats(null);
|
||||||
|
setStatus(null);
|
||||||
setError(true);
|
setError(true);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -100,6 +113,12 @@ const DevicePage = () => {
|
|||||||
getDevice();
|
getDevice();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = sessionStorage.getItem('devicePageIndex');
|
||||||
|
|
||||||
|
if (target !== null) setIndex(parseInt(target, 10));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(false);
|
setError(false);
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
@@ -125,7 +144,7 @@ const DevicePage = () => {
|
|||||||
/>
|
/>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol lg="12" xl="6">
|
<CCol lg="12" xl="6">
|
||||||
<DeviceActionCard />
|
<DeviceActionCard device={deviceConfig} />
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
<CRow>
|
<CRow>
|
||||||
@@ -137,7 +156,7 @@ const DevicePage = () => {
|
|||||||
className="font-weight-bold"
|
className="font-weight-bold"
|
||||||
href="#"
|
href="#"
|
||||||
active={index === 0}
|
active={index === 0}
|
||||||
onClick={() => setIndex(0)}
|
onClick={() => updateNav(0)}
|
||||||
>
|
>
|
||||||
{t('statistics.title')}
|
{t('statistics.title')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
@@ -145,7 +164,7 @@ const DevicePage = () => {
|
|||||||
className="font-weight-bold"
|
className="font-weight-bold"
|
||||||
href="#"
|
href="#"
|
||||||
active={index === 1}
|
active={index === 1}
|
||||||
onClick={() => setIndex(1)}
|
onClick={() => updateNav(1)}
|
||||||
>
|
>
|
||||||
{t('common.details')}
|
{t('common.details')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
@@ -153,15 +172,31 @@ const DevicePage = () => {
|
|||||||
className="font-weight-bold"
|
className="font-weight-bold"
|
||||||
href="#"
|
href="#"
|
||||||
active={index === 5}
|
active={index === 5}
|
||||||
onClick={() => setIndex(5)}
|
onClick={() => updateNav(5)}
|
||||||
>
|
>
|
||||||
{t('configuration.title')}
|
{t('configuration.title')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
|
<CNavLink
|
||||||
|
className="font-weight-bold"
|
||||||
|
href="#"
|
||||||
|
active={index === 8}
|
||||||
|
onClick={() => updateNav(8)}
|
||||||
|
>
|
||||||
|
{t('device.capabilities')}
|
||||||
|
</CNavLink>
|
||||||
|
<CNavLink
|
||||||
|
className="font-weight-bold"
|
||||||
|
href="#"
|
||||||
|
active={index === 7}
|
||||||
|
onClick={() => updateNav(7)}
|
||||||
|
>
|
||||||
|
{t('configuration.notes')}
|
||||||
|
</CNavLink>
|
||||||
<CNavLink
|
<CNavLink
|
||||||
className="font-weight-bold"
|
className="font-weight-bold"
|
||||||
href="#"
|
href="#"
|
||||||
active={index === 6}
|
active={index === 6}
|
||||||
onClick={() => setIndex(6)}
|
onClick={() => updateNav(6)}
|
||||||
>
|
>
|
||||||
{t('wifi_analysis.title')}
|
{t('wifi_analysis.title')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
@@ -169,7 +204,7 @@ const DevicePage = () => {
|
|||||||
className="font-weight-bold"
|
className="font-weight-bold"
|
||||||
href="#"
|
href="#"
|
||||||
active={index === 2}
|
active={index === 2}
|
||||||
onClick={() => setIndex(2)}
|
onClick={() => updateNav(2)}
|
||||||
>
|
>
|
||||||
{t('commands.title')}
|
{t('commands.title')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
@@ -177,7 +212,7 @@ const DevicePage = () => {
|
|||||||
className="font-weight-bold"
|
className="font-weight-bold"
|
||||||
href="#"
|
href="#"
|
||||||
active={index === 3}
|
active={index === 3}
|
||||||
onClick={() => setIndex(3)}
|
onClick={() => updateNav(3)}
|
||||||
>
|
>
|
||||||
{t('health.title')}
|
{t('health.title')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
@@ -185,11 +220,12 @@ const DevicePage = () => {
|
|||||||
className="font-weight-bold"
|
className="font-weight-bold"
|
||||||
href="#"
|
href="#"
|
||||||
active={index === 4}
|
active={index === 4}
|
||||||
onClick={() => setIndex(4)}
|
onClick={() => updateNav(4)}
|
||||||
>
|
>
|
||||||
{t('device_logs.title')}
|
{t('device_logs.title')}
|
||||||
</CNavLink>
|
</CNavLink>
|
||||||
</CNav>
|
</CNav>
|
||||||
|
{deviceConfig ? (
|
||||||
<CTabContent>
|
<CTabContent>
|
||||||
<CTabPane active={index === 0}>
|
<CTabPane active={index === 0}>
|
||||||
{index === 0 ? <DeviceStatisticsCard /> : null}
|
{index === 0 ? <DeviceStatisticsCard /> : null}
|
||||||
@@ -211,13 +247,26 @@ const DevicePage = () => {
|
|||||||
<ConfigurationDisplay deviceConfig={deviceConfig} getData={refresh} />
|
<ConfigurationDisplay deviceConfig={deviceConfig} getData={refresh} />
|
||||||
) : null}
|
) : null}
|
||||||
</CTabPane>
|
</CTabPane>
|
||||||
<CTabPane active={index === 6}>{index === 6 ? <WifiAnalysis /> : null}</CTabPane>
|
<CTabPane active={index === 8}>
|
||||||
|
{index === 8 ? <CapabilitiesDisplay serialNumber={deviceId} /> : null}
|
||||||
|
</CTabPane>
|
||||||
|
<CTabPane active={index === 6}>
|
||||||
|
{index === 6 ? <WifiAnalysis /> : null}
|
||||||
|
</CTabPane>
|
||||||
|
<CTabPane active={index === 7}>
|
||||||
|
{index === 7 ? (
|
||||||
|
<NotesTab deviceConfig={deviceConfig} refresh={refresh} />
|
||||||
|
) : null}
|
||||||
|
</CTabPane>
|
||||||
<CTabPane active={index === 2}>
|
<CTabPane active={index === 2}>
|
||||||
{index === 2 ? <CommandHistory /> : null}
|
{index === 2 ? <CommandHistory /> : null}
|
||||||
</CTabPane>
|
</CTabPane>
|
||||||
<CTabPane active={index === 3}>{index === 3 ? <DeviceHealth /> : null}</CTabPane>
|
<CTabPane active={index === 3}>
|
||||||
|
{index === 3 ? <DeviceHealth /> : null}
|
||||||
|
</CTabPane>
|
||||||
<CTabPane active={index === 4}>{index === 4 ? <DeviceLogs /> : null}</CTabPane>
|
<CTabPane active={index === 4}>{index === 4 ? <DeviceLogs /> : null}</CTabPane>
|
||||||
</CTabContent>
|
</CTabContent>
|
||||||
|
) : null}
|
||||||
</CCardBody>
|
</CCardBody>
|
||||||
</CCard>
|
</CCard>
|
||||||
</CCol>
|
</CCol>
|
||||||
|
|||||||
20
src/pages/DevicePage/index.module.scss
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.centerContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayContainer {
|
||||||
|
display: flex;
|
||||||
|
top: 0%;
|
||||||
|
left: 50%;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
198
src/pages/FirmwareListPage/Table.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardHeader,
|
||||||
|
CDataTable,
|
||||||
|
CPopover,
|
||||||
|
CSelect,
|
||||||
|
CSwitch,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilSearch } from '@coreui/icons';
|
||||||
|
import { CopyToClipboardButton } from 'ucentral-libs';
|
||||||
|
import { prettyDate, cleanBytesString } from 'utils/helper';
|
||||||
|
|
||||||
|
const FirmwareList = ({
|
||||||
|
t,
|
||||||
|
loading,
|
||||||
|
page,
|
||||||
|
pageCount,
|
||||||
|
setPage,
|
||||||
|
data,
|
||||||
|
toggleEditModal,
|
||||||
|
firmwarePerPage,
|
||||||
|
setFirmwarePerPage,
|
||||||
|
selectedDeviceType,
|
||||||
|
deviceTypes,
|
||||||
|
setSelectedDeviceType,
|
||||||
|
displayDev,
|
||||||
|
toggleDevDisplay,
|
||||||
|
}) => {
|
||||||
|
const fields = [
|
||||||
|
{ key: 'imageDate', label: t('firmware.image_date'), _style: { width: '1%' } },
|
||||||
|
{ key: 'size', label: t('firmware.size'), _style: { width: '1%' } },
|
||||||
|
{ key: 'revision', label: t('firmware.revision'), _style: { width: '1%' } },
|
||||||
|
{ key: 'uri', label: 'URI' },
|
||||||
|
{ key: 'show_details', label: '', _style: { width: '1%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getShortRevision = (revision) => {
|
||||||
|
if (revision.includes(' / ')) {
|
||||||
|
return revision.split(' / ')[1];
|
||||||
|
}
|
||||||
|
return revision;
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePage = (newValue) => {
|
||||||
|
setPage(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CCard className="m-0">
|
||||||
|
<CCardHeader className="p-1">
|
||||||
|
<div className="d-flex flex-row-reverse">
|
||||||
|
<div className="px-3">
|
||||||
|
<CSwitch
|
||||||
|
id="showDev"
|
||||||
|
color="primary"
|
||||||
|
defaultChecked={displayDev}
|
||||||
|
onClick={toggleDevDisplay}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="pr-2 pt-1">{t('firmware.show_dev')}</div>
|
||||||
|
<div className="px-3">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
value={selectedDeviceType}
|
||||||
|
onChange={(e) => setSelectedDeviceType(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{deviceTypes.map((deviceType) => (
|
||||||
|
<option key={createUuid()} value={deviceType}>
|
||||||
|
{deviceType}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
<div className="pr-2 pt-1">{t('firmware.device_type')}</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-0">
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="table-sm"
|
||||||
|
items={data}
|
||||||
|
fields={fields}
|
||||||
|
loading={loading}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
scopedSlots={{
|
||||||
|
imageDate: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<div style={{ width: '150px' }}>{prettyDate(item.imageDate)}</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
size: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<div style={{ width: '100px' }}>{cleanBytesString(item.size)}</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
revision: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<CPopover content={item.revision}>
|
||||||
|
<div style={{ width: 'calc(10vw)' }} className="text-truncate align-middle">
|
||||||
|
{item.revision ? getShortRevision(item.revision) : 'N/A'}
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
uri: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<div style={{ width: 'calc(50vw)' }}>
|
||||||
|
<div className="text-truncate align-middle">
|
||||||
|
<CopyToClipboardButton key={item.uri} t={t} size="sm" content={item.uri} />
|
||||||
|
<CPopover content={item.uri}>
|
||||||
|
<span>{item.uri}</span>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
show_details: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CPopover content={t('common.details')}>
|
||||||
|
<CButton
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => toggleEditModal(item.id)}
|
||||||
|
>
|
||||||
|
<CIcon content={cilSearch} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row pl-3">
|
||||||
|
<div className="pr-3">
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={changePage}
|
||||||
|
forcePage={page.selected}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
|
||||||
|
<div style={{ width: '100px' }} className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={firmwarePerPage}
|
||||||
|
onChange={(e) => setFirmwarePerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FirmwareList.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
page: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
setPage: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
firmwarePerPage: PropTypes.string.isRequired,
|
||||||
|
setFirmwarePerPage: PropTypes.func.isRequired,
|
||||||
|
selectedDeviceType: PropTypes.string.isRequired,
|
||||||
|
deviceTypes: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
setSelectedDeviceType: PropTypes.func.isRequired,
|
||||||
|
displayDev: PropTypes.bool.isRequired,
|
||||||
|
toggleDevDisplay: PropTypes.func.isRequired,
|
||||||
|
toggleEditModal: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(FirmwareList);
|
||||||
@@ -11,9 +11,10 @@ import {
|
|||||||
CTabContent,
|
CTabContent,
|
||||||
CCardHeader,
|
CCardHeader,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import { FirmwareList, useAuth, useToast } from 'ucentral-libs';
|
import { useAuth, useToast } from 'ucentral-libs';
|
||||||
import FirmwareDashboard from 'components/FirmwareDashboard';
|
import FirmwareDashboard from 'components/FirmwareDashboard';
|
||||||
import EditFirmwareModal from 'components/EditFirmwareModal';
|
import EditFirmwareModal from 'components/EditFirmwareModal';
|
||||||
|
import Table from './Table';
|
||||||
|
|
||||||
const FirmwareListPage = () => {
|
const FirmwareListPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -196,7 +197,7 @@ const FirmwareListPage = () => {
|
|||||||
<FirmwareDashboard />
|
<FirmwareDashboard />
|
||||||
</CTabPane>
|
</CTabPane>
|
||||||
<CTabPane active={index === 1}>
|
<CTabPane active={index === 1}>
|
||||||
<FirmwareList
|
<Table
|
||||||
t={t}
|
t={t}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
page={page}
|
page={page}
|
||||||
|
|||||||
@@ -1,559 +1,22 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import * as axios from 'axios';
|
import * as axios from 'axios';
|
||||||
import { LoginPage, useFormFields, useAuth, useToast } from 'ucentral-libs';
|
import { LoginPage, useAuth, useToast } from 'ucentral-libs';
|
||||||
import { setItem } from 'utils/localStorageHelper';
|
|
||||||
|
|
||||||
const initialFormState = {
|
|
||||||
username: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
placeholder: 'login.username',
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
placeholder: 'login.password',
|
|
||||||
},
|
|
||||||
ucentralsecurl: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
hidden: true,
|
|
||||||
placeholder: 'login.url',
|
|
||||||
},
|
|
||||||
forgotusername: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
placeholder: 'login.username',
|
|
||||||
},
|
|
||||||
newpassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
placeholder: 'login.new_password',
|
|
||||||
},
|
|
||||||
confirmpassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
placeholder: 'login.confirm_new_password',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialResponseState = {
|
|
||||||
text: '',
|
|
||||||
error: false,
|
|
||||||
tried: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { setCurrentToken, setEndpoints } = useAuth();
|
const { setCurrentToken, setEndpoints } = useAuth();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const [defaultConfig, setDefaultConfig] = useState({
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
hidden: true,
|
|
||||||
placeholder: 'login.url',
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [loginResponse, setLoginResponse] = useState(initialResponseState);
|
|
||||||
const [forgotResponse, setForgotResponse] = useState(initialResponseState);
|
|
||||||
const [changePasswordResponse, setChangeResponse] = useState(initialResponseState);
|
|
||||||
const [policies, setPolicies] = useState({
|
|
||||||
passwordPolicy: '',
|
|
||||||
passwordPattern: '',
|
|
||||||
accessPolicy: '',
|
|
||||||
});
|
|
||||||
const [formType, setFormType] = useState('login');
|
|
||||||
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialFormState);
|
|
||||||
const axiosInstance = axios.create();
|
|
||||||
axiosInstance.defaults.timeout = 5000;
|
|
||||||
|
|
||||||
const toggleForgotPassword = () => {
|
|
||||||
setFormFields({
|
|
||||||
...initialFormState,
|
|
||||||
...{
|
|
||||||
ucentralsecurl: defaultConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setLoginResponse(initialResponseState);
|
|
||||||
setForgotResponse(initialResponseState);
|
|
||||||
if (formType === 'login') setFormType('forgot-password');
|
|
||||||
else setFormType('login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelPasswordChange = () => {
|
|
||||||
setFormFields({
|
|
||||||
...initialFormState,
|
|
||||||
...{
|
|
||||||
ucentralsecurl: defaultConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setLoginResponse(initialResponseState);
|
|
||||||
setForgotResponse(initialResponseState);
|
|
||||||
setFormType('login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const signInValidation = () => {
|
|
||||||
let valid = true;
|
|
||||||
if (fields.ucentralsecurl.value === '') {
|
|
||||||
updateField('ucentralsecurl', { error: true });
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
if (fields.password.value === '') {
|
|
||||||
updateField('password', { error: true });
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
if (fields.username.value === '') {
|
|
||||||
updateField('username', { error: true });
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
formType === 'change-password' &&
|
|
||||||
fields.newpassword.value !== fields.confirmpassword.value
|
|
||||||
) {
|
|
||||||
updateField('confirmpassword', { error: true });
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
return valid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const forgotValidation = () => {
|
|
||||||
let valid = true;
|
|
||||||
|
|
||||||
if (fields.ucentralsecurl.value === '') {
|
|
||||||
updateField('ucentralsecurl', { error: true });
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
if (fields.forgotusername.value === '') {
|
|
||||||
updateField('forgotusername', { error: true });
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
return valid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event, action) => {
|
|
||||||
if (event.code === 'Enter') {
|
|
||||||
action(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDefaultConfig = async () => {
|
|
||||||
let uCentralSecUrl = '';
|
|
||||||
|
|
||||||
fetch('./config.json', {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((json) => {
|
|
||||||
const newUcentralSecConfig = {
|
|
||||||
value: json.DEFAULT_UCENTRALSEC_URL,
|
|
||||||
error: false,
|
|
||||||
hidden: !json.ALLOW_UCENTRALSEC_CHANGE,
|
|
||||||
placeholder: json.DEFAULT_UCENTRALSEC_URL,
|
|
||||||
};
|
|
||||||
uCentralSecUrl = newUcentralSecConfig.value;
|
|
||||||
setDefaultConfig(newUcentralSecConfig);
|
|
||||||
setFormFields({
|
|
||||||
...fields,
|
|
||||||
...{
|
|
||||||
ucentralsecurl: newUcentralSecConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return axiosInstance.post(
|
|
||||||
`${newUcentralSecConfig.value}/api/v1/oauth2?requirements=true`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
const newPolicies = response.data;
|
|
||||||
newPolicies.accessPolicy = `${uCentralSecUrl}${newPolicies.accessPolicy}`;
|
|
||||||
newPolicies.passwordPolicy = `${uCentralSecUrl}${newPolicies.passwordPolicy}`;
|
|
||||||
setPolicies(newPolicies);
|
|
||||||
})
|
|
||||||
.catch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGatewayUIUrl = (token, gwUrl) => {
|
|
||||||
axiosInstance
|
|
||||||
.get(`${gwUrl}/api/v1/system?command=info`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.data.UI) setItem('owgw-ui', response.data.UI);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProvUIUrl = (token, provUrl) => {
|
|
||||||
axiosInstance
|
|
||||||
.get(`${provUrl}/api/v1/system?command=info`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.data.UI) setItem('owprov-ui', response.data.UI);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const SignIn = () => {
|
|
||||||
setLoginResponse(initialResponseState);
|
|
||||||
if (signInValidation()) {
|
|
||||||
setLoading(true);
|
|
||||||
let token = '';
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
userId: fields.username.value,
|
|
||||||
password: fields.password.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (formType === 'change-password') {
|
|
||||||
parameters.newPassword = fields.newpassword.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.post(`${fields.ucentralsecurl.value}/api/v1/oauth2`, parameters)
|
|
||||||
.then((response) => {
|
|
||||||
// If there's MFA to do
|
|
||||||
if (response.data.method && response.data.created) {
|
|
||||||
setFormType(`validation-${response.data.method}-${response.data.uuid}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (response.data.userMustChangePassword) {
|
|
||||||
setFormType('change-password');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
setItem('access_token', response.data.access_token);
|
|
||||||
token = response.data.access_token;
|
|
||||||
return axiosInstance.get(`${fields.ucentralsecurl.value}/api/v1/systemEndpoints`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${response.data.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response) {
|
|
||||||
const endpoints = {
|
|
||||||
owsec: fields.ucentralsecurl.value,
|
|
||||||
};
|
|
||||||
for (const endpoint of response.data.endpoints) {
|
|
||||||
endpoints[endpoint.type] = endpoint.uri;
|
|
||||||
}
|
|
||||||
if (endpoints.owgw) getGatewayUIUrl(token, endpoints.owgw);
|
|
||||||
if (endpoints.owprov) getProvUIUrl(token, endpoints.owprov);
|
|
||||||
setItem('gateway_endpoints', JSON.stringify(endpoints));
|
|
||||||
setEndpoints(endpoints);
|
|
||||||
setCurrentToken(token);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (formType === 'change-password') {
|
|
||||||
if (error.response?.data?.ErrorCode === 3) {
|
|
||||||
setChangeResponse({
|
|
||||||
text: t('login.previously_used'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
} else if (error.response?.data?.ErrorCode === 5) {
|
|
||||||
setChangeResponse({
|
|
||||||
text: t('common.invalid_password'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setChangeResponse({
|
|
||||||
text: t('login.change_password_error'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (error.response.status === 403) {
|
|
||||||
if (error.response?.data?.ErrorCode === 1) setFormType('change-password');
|
|
||||||
else if (error.response?.data?.ErrorCode === 2) {
|
|
||||||
setLoginResponse({
|
|
||||||
text: t('common.invalid_credentials'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoginResponse({
|
|
||||||
text: t('login.login_error'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setLoginResponse({
|
|
||||||
text: t('login.login_error'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoginResponse(initialResponseState);
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
let token = '';
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
userId: event.target?.username?.value,
|
|
||||||
password: event.target?.password?.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (formType === 'change-password') {
|
|
||||||
parameters.newPassword = fields.newpassword.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.post(`${fields.ucentralsecurl.value}/api/v1/oauth2`, parameters)
|
|
||||||
.then((response) => {
|
|
||||||
// If there's MFA to do
|
|
||||||
if (response.data.method && response.data.created) {
|
|
||||||
setFormType(`validation-${response.data.method}-${response.data.uuid}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (response.data.userMustChangePassword) {
|
|
||||||
setFormType('change-password');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
setItem('access_token', response.data.access_token);
|
|
||||||
token = response.data.access_token;
|
|
||||||
return axiosInstance.get(`${fields.ucentralsecurl.value}/api/v1/systemEndpoints`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${response.data.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response) {
|
|
||||||
const endpoints = {
|
|
||||||
owsec: fields.ucentralsecurl.value,
|
|
||||||
};
|
|
||||||
for (const endpoint of response.data.endpoints) {
|
|
||||||
endpoints[endpoint.type] = endpoint.uri;
|
|
||||||
}
|
|
||||||
if (endpoints.owgw) getGatewayUIUrl(token, endpoints.owgw);
|
|
||||||
if (endpoints.owprov) getProvUIUrl(token, endpoints.owprov);
|
|
||||||
setItem('gateway_endpoints', JSON.stringify(endpoints));
|
|
||||||
setEndpoints(endpoints);
|
|
||||||
setCurrentToken(token);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (formType === 'change-password') {
|
|
||||||
if (error.response?.data?.ErrorCode === 3) {
|
|
||||||
setChangeResponse({
|
|
||||||
text: t('login.previously_used'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
} else if (error.response?.data?.ErrorCode === 5) {
|
|
||||||
setChangeResponse({
|
|
||||||
text: t('common.invalid_password'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setChangeResponse({
|
|
||||||
text: t('login.change_password_error'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (error.response.status === 403) {
|
|
||||||
if (error.response?.data?.ErrorCode === 1) setFormType('change-password');
|
|
||||||
else if (error.response?.data?.ErrorCode === 2) {
|
|
||||||
setLoginResponse({
|
|
||||||
text: t('common.invalid_credentials'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoginResponse({
|
|
||||||
text: t('login.login_error'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setLoginResponse({
|
|
||||||
text: t('login.login_error'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendForgotPasswordEmail = () => {
|
|
||||||
setForgotResponse(initialResponseState);
|
|
||||||
|
|
||||||
if (forgotValidation()) {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.post(`${fields.ucentralsecurl.value}/api/v1/oauth2?forgotPassword=true`, {
|
|
||||||
userId: fields.forgotusername.value,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
updateField('forgotusername', {
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
setForgotResponse({
|
|
||||||
text: t('login.forgot_password_success'),
|
|
||||||
error: false,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setForgotResponse({
|
|
||||||
text: t('login.forgot_password_error'),
|
|
||||||
error: true,
|
|
||||||
tried: true,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateCode = (code) => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
uuid: formType.split('-').slice(2).join('-'),
|
|
||||||
answer: code,
|
|
||||||
};
|
|
||||||
|
|
||||||
let token = '';
|
|
||||||
|
|
||||||
return axiosInstance
|
|
||||||
.post(
|
|
||||||
`${fields.ucentralsecurl.value}/api/v1/oauth2?completeMFAChallenge=true`,
|
|
||||||
parameters,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.data.userMustChangePassword) {
|
|
||||||
setFormType('change-password');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
setItem('access_token', response.data.access_token);
|
|
||||||
token = response.data.access_token;
|
|
||||||
return axiosInstance.get(`${fields.ucentralsecurl.value}/api/v1/systemEndpoints`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${response.data.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response) {
|
|
||||||
const endpoints = {
|
|
||||||
owsec: fields.ucentralsecurl.value,
|
|
||||||
};
|
|
||||||
for (const endpoint of response.data.endpoints) {
|
|
||||||
endpoints[endpoint.type] = endpoint.uri;
|
|
||||||
}
|
|
||||||
if (endpoints.owgw) getGatewayUIUrl(token, endpoints.owgw);
|
|
||||||
if (endpoints.owprov) getProvUIUrl(token, endpoints.owprov);
|
|
||||||
setItem('gateway_endpoints', JSON.stringify(endpoints));
|
|
||||||
setEndpoints(endpoints);
|
|
||||||
setCurrentToken(token);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => false)
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const resendValidationCode = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
uuid: formType.split('-').slice(2).join('-'),
|
|
||||||
};
|
|
||||||
|
|
||||||
return axiosInstance
|
|
||||||
.post(`${fields.ucentralsecurl.value}/api/v1/oauth2?resendMFACode=true`, parameters, options)
|
|
||||||
.then(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.success'),
|
|
||||||
body: t('user.new_code_sent'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('login.authentication_expired'),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
if (e.response?.data?.ErrorCode === 403) setFormType('login');
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getDefaultConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginPage
|
<LoginPage
|
||||||
t={t}
|
t={t}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
signIn={SignIn}
|
setCurrentToken={setCurrentToken}
|
||||||
loading={loading}
|
setEndpoints={setEndpoints}
|
||||||
|
addToast={addToast}
|
||||||
|
axios={axios}
|
||||||
logo="assets/OpenWiFi_LogoLockup_DarkGreyColour.svg"
|
logo="assets/OpenWiFi_LogoLockup_DarkGreyColour.svg"
|
||||||
loginResponse={loginResponse}
|
|
||||||
forgotResponse={forgotResponse}
|
|
||||||
fields={fields}
|
|
||||||
updateField={updateFieldWithId}
|
|
||||||
toggleForgotPassword={toggleForgotPassword}
|
|
||||||
formType={formType}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
submitForm={submitForm}
|
|
||||||
sendForgotPasswordEmail={sendForgotPasswordEmail}
|
|
||||||
changePasswordResponse={changePasswordResponse}
|
|
||||||
cancelPasswordChange={cancelPasswordChange}
|
|
||||||
policies={policies}
|
|
||||||
validateCode={validateCode}
|
|
||||||
resendValidationCode={resendValidationCode}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
.logo {
|
|
||||||
padding-left: 17%;
|
|
||||||
width: 85%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.languageSwitcher {
|
|
||||||
float: right;
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
@@ -1,392 +1,11 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CCard, CCardBody, CCardHeader, CButton, CPopover, CButtonToolbar } from '@coreui/react';
|
|
||||||
import { cilPencil, cilSave, cilSync, cilX } from '@coreui/icons';
|
|
||||||
import CIcon from '@coreui/icons-react';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import { testRegex } from 'utils/helper';
|
import { ProfilePage as Page } from 'ucentral-libs';
|
||||||
import { useUser, EditMyProfile, useAuth, useToast } from 'ucentral-libs';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
Id: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
newPassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
ignore: true,
|
|
||||||
},
|
|
||||||
confirmNewPassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
ignore: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
value: [],
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
userTypeProprietaryInfo: {
|
|
||||||
value: {},
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
mfaMethod: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentToken, endpoints, user, getAvatar, avatar } = useAuth();
|
return <Page t={t} axiosInstance={axiosInstance} />;
|
||||||
const { addToast } = useToast();
|
|
||||||
const [editing, setEditing] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [userForm, updateWithId, updateWithKey, setUser] = useUser(initialState);
|
|
||||||
const [newAvatar, setNewAvatar] = useState('');
|
|
||||||
const [newAvatarFile, setNewAvatarFile] = useState(null);
|
|
||||||
const [avatarDeleted, setAvatarDeleted] = useState(false);
|
|
||||||
const [fileInputKey, setFileInputKey] = useState(0);
|
|
||||||
const [policies, setPolicies] = useState({
|
|
||||||
passwordPolicy: '',
|
|
||||||
passwordPattern: '',
|
|
||||||
accessPolicy: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const getPasswordPolicy = () => {
|
|
||||||
axiosInstance
|
|
||||||
.post(`${endpoints.owsec}/api/v1/oauth2?requirements=true`, {})
|
|
||||||
.then((response) => {
|
|
||||||
const newPolicies = response.data;
|
|
||||||
newPolicies.accessPolicy = `${endpoints.owsec}${newPolicies.accessPolicy}`;
|
|
||||||
newPolicies.passwordPolicy = `${endpoints.owsec}${newPolicies.passwordPolicy}`;
|
|
||||||
setPolicies(response.data);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUser = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(`${endpoints.owsec}/api/v1/user/${user.Id}`, options)
|
|
||||||
.then((response) => {
|
|
||||||
const newUser = {};
|
|
||||||
|
|
||||||
for (const key of Object.keys(response.data)) {
|
|
||||||
if (key in initialState && key !== 'currentPassword') {
|
|
||||||
newUser[key] = {
|
|
||||||
...initialState[key],
|
|
||||||
value: response.data[key],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newUser.mfaMethod = {
|
|
||||||
value: response.data.userTypeProprietaryInfo.mfa.enabled
|
|
||||||
? response.data.userTypeProprietaryInfo.mfa.method
|
|
||||||
: '',
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
setUser({ ...initialState, ...newUser });
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('user.error_fetching_users', { error: e }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadAvatar = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = new FormData();
|
|
||||||
data.append('file', newAvatarFile);
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.post(`${endpoints.owsec}/api/v1/avatar/${user.Id}`, data, options)
|
|
||||||
.then(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_success_title'),
|
|
||||||
body: t('user.update_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
getAvatar();
|
|
||||||
setNewAvatar('');
|
|
||||||
setNewAvatarFile(null);
|
|
||||||
setFileInputKey(fileInputKey + 1);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_failure_title'),
|
|
||||||
body: t('user.update_failure'),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateUser = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (newAvatar !== '' && newAvatarFile !== null) {
|
|
||||||
uploadAvatar();
|
|
||||||
} else if (avatarDeleted) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.delete(`${endpoints.owsec}/api/v1/avatar/${user.Id}`, options)
|
|
||||||
.then(() => {
|
|
||||||
getAvatar();
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
userForm.newPassword.value !== '' &&
|
|
||||||
(!testRegex(userForm.newPassword.value, policies.passwordPattern) ||
|
|
||||||
userForm.newPassword.value !== userForm.confirmNewPassword.value)
|
|
||||||
) {
|
|
||||||
updateWithKey('newPassword', {
|
|
||||||
error: true,
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
} else {
|
|
||||||
const newNotes = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < userForm.notes.value.length; i += 1) {
|
|
||||||
if (userForm.notes.value[i].new) newNotes.push({ note: userForm.notes.value[i].note });
|
|
||||||
}
|
|
||||||
|
|
||||||
const propInfo = { ...userForm.userTypeProprietaryInfo.value };
|
|
||||||
propInfo.mfa.method = userForm.mfaMethod.value === '' ? undefined : userForm.mfaMethod.value;
|
|
||||||
propInfo.mfa.enabled = userForm.mfaMethod.value !== '';
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
id: user.Id,
|
|
||||||
description: userForm.description.value,
|
|
||||||
name: userForm.name.value,
|
|
||||||
notes: newNotes,
|
|
||||||
userTypeProprietaryInfo: propInfo,
|
|
||||||
currentPassword: userForm.newPassword.value !== '' ? userForm.newPassword.value : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.put(`${endpoints.owsec}/api/v1/user/${user.Id}`, parameters, options)
|
|
||||||
.then(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_success_title'),
|
|
||||||
body: t('user.update_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
toggleEditing();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_failure_title'),
|
|
||||||
body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
getUser();
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addNote = (currentNote) => {
|
|
||||||
const newNotes = [...userForm.notes.value];
|
|
||||||
newNotes.unshift({
|
|
||||||
note: currentNote,
|
|
||||||
new: true,
|
|
||||||
created: new Date().getTime() / 1000,
|
|
||||||
createdBy: '',
|
|
||||||
});
|
|
||||||
updateWithKey('notes', { value: newNotes });
|
|
||||||
};
|
|
||||||
|
|
||||||
const showPreview = (e) => {
|
|
||||||
setAvatarDeleted(false);
|
|
||||||
const imageFile = e.target.files[0];
|
|
||||||
setNewAvatar(URL.createObjectURL(imageFile));
|
|
||||||
setNewAvatarFile(imageFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAvatar = () => {
|
|
||||||
setNewAvatar('');
|
|
||||||
setAvatarDeleted(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendPhoneNumberTest = async (phoneNumber) => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return axiosInstance
|
|
||||||
.post(`${endpoints.owsec}/api/v1/sms?validateNumber=true`, { to: phoneNumber }, options)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('user.error_sending_code'),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const testVerificationCode = async (phoneNumber, code) => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return axiosInstance
|
|
||||||
.post(
|
|
||||||
`${endpoints.owsec}/api/v1/sms?completeValidation=true&validationCode=${code}`,
|
|
||||||
{ to: phoneNumber },
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('user.wrong_validation_code'),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleEditing = () => {
|
|
||||||
if (editing) {
|
|
||||||
setAvatarDeleted(false);
|
|
||||||
setNewAvatar('');
|
|
||||||
getUser();
|
|
||||||
getAvatar();
|
|
||||||
}
|
|
||||||
setEditing(!editing);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user.Id) {
|
|
||||||
getAvatar();
|
|
||||||
getUser();
|
|
||||||
}
|
|
||||||
if (policies.passwordPattern.length === 0) {
|
|
||||||
getPasswordPolicy();
|
|
||||||
}
|
|
||||||
}, [user.Id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CCard className="my-0 py-0">
|
|
||||||
<CCardHeader className="dark-header">
|
|
||||||
<div style={{ fontWeight: '600' }} className=" text-value-lg float-left">
|
|
||||||
{t('user.my_profile')}
|
|
||||||
</div>
|
|
||||||
<div className="text-right float-right">
|
|
||||||
<CButtonToolbar role="group" className="justify-content-end">
|
|
||||||
<CPopover content={t('common.save')}>
|
|
||||||
<CButton disabled={!editing} color="info" onClick={updateUser} className="mx-1">
|
|
||||||
<CIcon name="cil-save" content={cilSave} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
<CPopover content={t('common.edit')}>
|
|
||||||
<CButton disabled={editing} color="dark" onClick={toggleEditing} className="mx-1">
|
|
||||||
<CIcon name="cil-pencil" content={cilPencil} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
<CPopover content={t('common.stop_editing')}>
|
|
||||||
<CButton disabled={!editing} color="dark" onClick={toggleEditing} className="mx-1">
|
|
||||||
<CIcon name="cil-x" content={cilX} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
<CPopover content={t('common.refresh')}>
|
|
||||||
<CButton disabled={editing} color="info" onClick={getUser} className="mx-1">
|
|
||||||
<CIcon content={cilSync} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
</CButtonToolbar>
|
|
||||||
</div>
|
|
||||||
</CCardHeader>
|
|
||||||
<CCardBody>
|
|
||||||
<EditMyProfile
|
|
||||||
t={t}
|
|
||||||
user={userForm}
|
|
||||||
updateUserWithId={updateWithId}
|
|
||||||
updateWithKey={updateWithKey}
|
|
||||||
loading={loading}
|
|
||||||
policies={policies}
|
|
||||||
addNote={addNote}
|
|
||||||
avatar={avatar}
|
|
||||||
newAvatar={newAvatar}
|
|
||||||
showPreview={showPreview}
|
|
||||||
deleteAvatar={deleteAvatar}
|
|
||||||
fileInputKey={fileInputKey}
|
|
||||||
sendPhoneNumberTest={sendPhoneNumberTest}
|
|
||||||
testVerificationCode={testVerificationCode}
|
|
||||||
editing={editing}
|
|
||||||
avatarDeleted={avatarDeleted}
|
|
||||||
/>
|
|
||||||
</CCardBody>
|
|
||||||
</CCard>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfilePage;
|
export default ProfilePage;
|
||||||
|
|||||||