Version 2.5.18
							
								
								
									
										597
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "ucentral-client", | ||||
|   "version": "2.4.3", | ||||
|   "version": "2.5.18", | ||||
|   "dependencies": { | ||||
|     "@coreui/coreui": "^3.4.0", | ||||
|     "@coreui/icons": "^2.0.1", | ||||
| @@ -26,7 +26,7 @@ | ||||
|     "react-tooltip": "^4.2.21", | ||||
|     "react-widgets": "^5.1.1", | ||||
|     "sass": "^1.35.1", | ||||
|     "ucentral-libs": "^1.0.37", | ||||
|     "ucentral-libs": "^1.0.57", | ||||
|     "uuid": "^8.3.2" | ||||
|   }, | ||||
|   "main": "index.js", | ||||
| @@ -82,7 +82,6 @@ | ||||
|     "husky": "^4.3.8", | ||||
|     "lint-staged": "^11.0.0", | ||||
|     "mini-css-extract-plugin": "^1.6.1", | ||||
|     "node-sass": "^5.0.0", | ||||
|     "path": "^0.12.7", | ||||
|     "prettier": "^2.3.2", | ||||
|     "react-refresh": "^0.9.0", | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| 		"blink": "LEDs Blinken", | ||||
| 		"device_leds": "LEDs", | ||||
| 		"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:", | ||||
| 		"set_leds": "LEDs einstellen", | ||||
| 		"when_blink_leds": "Wann möchten Sie die LEDs blinken lassen?" | ||||
| @@ -37,9 +38,11 @@ | ||||
| 		"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", | ||||
| 		"adding_ellipsis": "Hinzufügen ...", | ||||
| 		"all": "Alles", | ||||
| 		"are_you_sure": "Bist du sicher?", | ||||
| 		"back_to_login": "Zurück zur Anmeldung", | ||||
| 		"back_to_start": "Zurück zum Start", | ||||
| 		"blacklist": "Schwarze Liste", | ||||
| 		"by": "Durch", | ||||
| 		"cancel": "Abbrechen", | ||||
| 		"certificate": "Zertifikat", | ||||
| @@ -62,12 +65,14 @@ | ||||
| 		"create": "Erstellen", | ||||
| 		"created": "Erstellt", | ||||
| 		"created_by": "Erstellt von", | ||||
| 		"creator": "Schöpfer", | ||||
| 		"current": "Aktuell", | ||||
| 		"custom_date": "Benutzerdefiniertes Datum", | ||||
| 		"dashboard": "Instrumententafel", | ||||
| 		"date": "Datum", | ||||
| 		"day": "tag", | ||||
| 		"days": "tage", | ||||
| 		"default_map": "Standardkarte", | ||||
| 		"delete": "Löschen", | ||||
| 		"delete_device": "Gerät löschen", | ||||
| 		"details": "Einzelheiten", | ||||
| @@ -85,6 +90,7 @@ | ||||
| 		"dismiss": "entlassen", | ||||
| 		"do_now": "Sofort", | ||||
| 		"download": "Herunterladen", | ||||
| 		"duplicate": "Duplikat", | ||||
| 		"duration": "Dauer", | ||||
| 		"edit": "Bearbeiten", | ||||
| 		"edit_user": "Bearbeiten", | ||||
| @@ -94,6 +100,7 @@ | ||||
| 		"error": "Fehler", | ||||
| 		"error_adding_note": "Fehler beim Hinzufügen einer Notiz", | ||||
| 		"error_code": "Fehlercode", | ||||
| 		"errors": "Fehler", | ||||
| 		"execute_now": "Möchten Sie diesen Befehl jetzt ausführen?", | ||||
| 		"executed": "Ausgeführt", | ||||
| 		"exit": "Ausgang", | ||||
| @@ -142,14 +149,16 @@ | ||||
| 		"no_items": "Keine Gegenstände", | ||||
| 		"none": "Keiner", | ||||
| 		"not_connected": "Nicht verbunden", | ||||
| 		"of_connected": "% der Geräte", | ||||
| 		"of_connected": "% der verbundenen Geräte", | ||||
| 		"off": "Aus", | ||||
| 		"on": "An", | ||||
| 		"optional": "Wahlweise", | ||||
| 		"overall_health": "Allgemeine Gesundheit", | ||||
| 		"password_policy": "Kennwortrichtlinie", | ||||
| 		"preferences": "Einstellungen", | ||||
| 		"preview": "Vorschau", | ||||
| 		"program": "Programm", | ||||
| 		"reason": "Grund", | ||||
| 		"recorded": "Verzeichnet", | ||||
| 		"refresh": "Aktualisierung", | ||||
| 		"refresh_device": "Gerät aktualisieren", | ||||
| @@ -170,12 +179,14 @@ | ||||
| 		"show_all": "Zeige alles", | ||||
| 		"socket_connection_closed": "Verbindung geschlossen!", | ||||
| 		"start": "Start", | ||||
| 		"status": "Status", | ||||
| 		"stop_editing": "Stoppen Sie die Bearbeitung", | ||||
| 		"submit": "Absenden", | ||||
| 		"submitted": "Eingereicht", | ||||
| 		"success": "Erfolg", | ||||
| 		"system": "System", | ||||
| 		"table": "Tabelle", | ||||
| 		"time_per_device": "Gerät/Sekunde", | ||||
| 		"timestamp": "Zeit", | ||||
| 		"to": "zu", | ||||
| 		"type": "Art", | ||||
| @@ -190,6 +201,7 @@ | ||||
| 		"uuid": "UUID", | ||||
| 		"vendors": "Anbieter", | ||||
| 		"view_more": "Mehr anzeigen", | ||||
| 		"visibility": "Sichtweite", | ||||
| 		"yes": "Ja" | ||||
| 	}, | ||||
| 	"configuration": { | ||||
| @@ -210,6 +222,8 @@ | ||||
| 		"creation_success": "Konfiguration erfolgreich erstellt!", | ||||
| 		"currently_associated": "Aktuell zugeordnete Konfiguration: {{config}}", | ||||
| 		"currently_selected_config": "Derzeit ausgewählte Konfiguration: {{config}}", | ||||
| 		"default_configs": "Standardkonfigurationen", | ||||
| 		"default_configurations": "Standardkonfigurationen", | ||||
| 		"delete_config": "Konfiguration löschen", | ||||
| 		"details": "Gerätedetails", | ||||
| 		"device_password": "Passwort", | ||||
| @@ -218,6 +232,7 @@ | ||||
| 		"devices_affected": "Von dieser Konfiguration betroffene Geräte:", | ||||
| 		"edit_configuration": "Konfiguration bearbeiten", | ||||
| 		"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_trying_delete": "Fehler beim Versuch zu löschen: {{error}}", | ||||
| 		"error_update": "Fehler: {{error}}", | ||||
| @@ -261,6 +276,7 @@ | ||||
| 	"contact": { | ||||
| 		"access_pin": "Zugangs-PIN", | ||||
| 		"add_contact": "Kontakt hinzufügen", | ||||
| 		"contact": "Kontakt", | ||||
| 		"create_contact": "Kontakt erstellen", | ||||
| 		"currently_selected_contact": "Aktuell ausgewählter Kontakt: {{contact}}", | ||||
| 		"delete": "Kontakt löschen?", | ||||
| @@ -300,12 +316,23 @@ | ||||
| 		"healthchecks_title": "Healthchecks löschen" | ||||
| 	}, | ||||
| 	"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", | ||||
| 		"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_devices": "Fehler beim Abrufen von Geräten: {{error}}", | ||||
| 		"health_explanation": "Zustand der angeschlossenen Geräte", | ||||
| 		"memory_explanation": "Von angeschlossenen Geräten belegter Speicher", | ||||
| 		"uptimes_explanation": "Zeit, zu der verbundene Geräte aktiv und verbunden waren" | ||||
| 		"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": "Anzahl verbundener Geräte mit entsprechendem belegtem Speicher %", | ||||
| 		"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": { | ||||
| 		"log": "Protokoll", | ||||
| @@ -320,23 +347,32 @@ | ||||
| 		"add_success": "Entität erfolgreich erstellt!", | ||||
| 		"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.", | ||||
| 		"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_venue": "Aktuell ausgewählter Veranstaltungsort: {{config}}", | ||||
| 		"delete_success": "Entität erfolgreich gelöscht", | ||||
| 		"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}}", | ||||
| 		"enter_here": "Geben Sie hier die IP(s) ein, die Sie hinzufügen möchten", | ||||
| 		"entire_tree": "Seitenverzeichnis", | ||||
| 		"entire_tree": "Netzwerkkarte", | ||||
| 		"entities": "Entitäten", | ||||
| 		"entity": "Entität", | ||||
| 		"error_deleting_map": "Fehler beim Löschen der Karte: {{error}}", | ||||
| 		"error_fetch_entity": "Fehler beim Abrufen von Entitätsinformationen", | ||||
| 		"error_fetching": "Fehler beim Abrufen von Entitäten", | ||||
| 		"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_map": "Fehler beim Speichern der Karte: {{error}}", | ||||
| 		"higher_priority": "Stellen Sie eine höhere Priorität ein", | ||||
| 		"ip_detection": "IP-Erkennung", | ||||
| 		"ip_formats": "Sie können IPv4- oder IPv6-Adressen in den folgenden Formaten hinzufügen:", | ||||
| 		"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", | ||||
| 		"no_ips": "Keine IPs ausgewählt", | ||||
| 		"not_assigned": "Nicht zugeordnet", | ||||
| @@ -344,6 +380,7 @@ | ||||
| 		"select_entity": "Wählen Sie diese Entität aus", | ||||
| 		"selected_entity": "Ausgewählte Einheit", | ||||
| 		"selected_map": "Ausgewählte Karte", | ||||
| 		"tree_saved": "Karte erfolgreich gespeichert!", | ||||
| 		"update_failure_error": "Fehler beim Versuch, die Entität zu aktualisieren: {{error}}", | ||||
| 		"valid_serial": "Muss eine gültige Seriennummer sein (12 HEX-Zeichen)", | ||||
| 		"venues": "Veranstaltungsorte" | ||||
| @@ -541,6 +578,9 @@ | ||||
| 		"verification_code": "Geben Sie hier Ihre Bestätigung ein", | ||||
| 		"wrong_code": "Der eingegebene Bestätigungscode ist ungültig." | ||||
| 	}, | ||||
| 	"preferences": { | ||||
| 		"provisioning": "Bereitstellung" | ||||
| 	}, | ||||
| 	"reboot": { | ||||
| 		"directions": "Wann möchten Sie dieses Gerät neu starten?", | ||||
| 		"now": "Möchten Sie dieses Gerät jetzt neu starten?", | ||||
| @@ -584,7 +624,7 @@ | ||||
| 		"mac_prefix": "MAC-Präfix", | ||||
| 		"max_associations": "max. Verbände", | ||||
| 		"max_clients": "Max. Kunden", | ||||
| 		"messages_transmitted": "Gesendete Nachrichten", | ||||
| 		"messages_transmitted": "Nachricht TX", | ||||
| 		"min_associations": "Mindest. Verbände", | ||||
| 		"min_clients": "Mindest. Kunden", | ||||
| 		"pause": "Pause", | ||||
| @@ -592,7 +632,7 @@ | ||||
| 		"prefix_length": "Erforderlich, muss eine Länge von 6 Zeichen haben", | ||||
| 		"previous_runs": "Vorherige Läufe", | ||||
| 		"received": "empfangen", | ||||
| 		"received_messages": "Erhaltene Nachrichten", | ||||
| 		"received_messages": "Nachricht RX", | ||||
| 		"reconnect_interval": "Wiederverbindungsintervall", | ||||
| 		"resume": "Fortsetzen", | ||||
| 		"resume_success": "Lauf wieder aufgenommen!", | ||||
| @@ -634,6 +674,19 @@ | ||||
| 		"uptime": "Betriebszeit", | ||||
| 		"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": { | ||||
| 		"error_fetching": "Fehler beim Abrufen von Systeminformationen", | ||||
| 		"error_reloading": "Fehler beim Neuladen: {{error}}", | ||||
| @@ -723,6 +776,7 @@ | ||||
| 		"send_code_again": "Code nochmal senden", | ||||
| 		"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", | ||||
| 		"table_title": "Admin-Benutzer", | ||||
| 		"update_failure": "Fehler beim Aktualisieren: {{error}}", | ||||
| 		"update_failure_title": "Update fehlgeschlagen", | ||||
| 		"update_success": "Benutzer erfolgreich aktualisiert", | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| 		"blink": "Blink", | ||||
| 		"device_leds": "Device LEDs", | ||||
| 		"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: ", | ||||
| 		"set_leds": "Set LEDs", | ||||
| 		"when_blink_leds": "When would you like to make the device LEDs blink?" | ||||
| @@ -37,9 +38,11 @@ | ||||
| 		"add_note": "Add Note", | ||||
| 		"add_note_explanation": "Write your new note below and click the '+' button where you are done", | ||||
| 		"adding_ellipsis": "Adding...", | ||||
| 		"all": "All", | ||||
| 		"are_you_sure": "Are you sure?", | ||||
| 		"back_to_login": "Back to Login", | ||||
| 		"back_to_start": "Back to start", | ||||
| 		"blacklist": "Blacklist", | ||||
| 		"by": "By", | ||||
| 		"cancel": "Cancel", | ||||
| 		"certificate": "Certificate", | ||||
| @@ -62,12 +65,14 @@ | ||||
| 		"create": "Create", | ||||
| 		"created": "Created", | ||||
| 		"created_by": "Created By", | ||||
| 		"creator": "Creator", | ||||
| 		"current": "Current ", | ||||
| 		"custom_date": "Custom Date", | ||||
| 		"dashboard": "Dashboard", | ||||
| 		"date": "Date", | ||||
| 		"day": "day", | ||||
| 		"days": "days", | ||||
| 		"default_map": "Default Map", | ||||
| 		"delete": "Delete", | ||||
| 		"delete_device": "Delete Device", | ||||
| 		"details": "Details", | ||||
| @@ -85,6 +90,7 @@ | ||||
| 		"dismiss": "Dismiss", | ||||
| 		"do_now": "Do Now!", | ||||
| 		"download": "Download", | ||||
| 		"duplicate": "Duplicate", | ||||
| 		"duration": "Duration", | ||||
| 		"edit": "Edit", | ||||
| 		"edit_user": "Edit", | ||||
| @@ -94,6 +100,7 @@ | ||||
| 		"error": "Error", | ||||
| 		"error_adding_note": "Error while adding note", | ||||
| 		"error_code": "Error Code", | ||||
| 		"errors": "Errors", | ||||
| 		"execute_now": "Would you like to execute this command now?", | ||||
| 		"executed": "Executed", | ||||
| 		"exit": "Exit", | ||||
| @@ -142,14 +149,16 @@ | ||||
| 		"no_items": "No Items", | ||||
| 		"none": "None", | ||||
| 		"not_connected": "Not Connected", | ||||
| 		"of_connected": "% of devices", | ||||
| 		"of_connected": "% of connected devices", | ||||
| 		"off": "Off", | ||||
| 		"on": "On", | ||||
| 		"optional": "Optional", | ||||
| 		"overall_health": "Overall Health", | ||||
| 		"password_policy": "Password Policy", | ||||
| 		"preferences": "Preferences", | ||||
| 		"preview": "Preview", | ||||
| 		"program": "Program", | ||||
| 		"reason": "Reason", | ||||
| 		"recorded": "Recorded", | ||||
| 		"refresh": "Refresh", | ||||
| 		"refresh_device": "Refresh Device", | ||||
| @@ -170,12 +179,14 @@ | ||||
| 		"show_all": "Show All", | ||||
| 		"socket_connection_closed": "Connection closed!", | ||||
| 		"start": "Start", | ||||
| 		"status": "Status", | ||||
| 		"stop_editing": "Stop Editing", | ||||
| 		"submit": "Submit", | ||||
| 		"submitted": "Submitted", | ||||
| 		"success": "Success", | ||||
| 		"system": "System", | ||||
| 		"table": "Table", | ||||
| 		"time_per_device": "Devices/Second", | ||||
| 		"timestamp": "Time", | ||||
| 		"to": "To", | ||||
| 		"type": "Type", | ||||
| @@ -190,6 +201,7 @@ | ||||
| 		"uuid": "UUID", | ||||
| 		"vendors": "Vendors", | ||||
| 		"view_more": "View more", | ||||
| 		"visibility": "Visibility", | ||||
| 		"yes": "Yes" | ||||
| 	}, | ||||
| 	"configuration": { | ||||
| @@ -210,6 +222,8 @@ | ||||
| 		"creation_success": "Configuration successfully created!", | ||||
| 		"currently_associated": "Currently Associated Configuration: {{config}}", | ||||
| 		"currently_selected_config": "Currently Selected Configuration: {{config}}", | ||||
| 		"default_configs": "Default Configs", | ||||
| 		"default_configurations": "Default Configurations", | ||||
| 		"delete_config": "Delete Config", | ||||
| 		"details": "Details", | ||||
| 		"device_password": "Password", | ||||
| @@ -218,6 +232,7 @@ | ||||
| 		"devices_affected": "Devices affected by this configuration: ", | ||||
| 		"edit_configuration": "Edit Configuration", | ||||
| 		"error_delete": "Error while trying to delete: {{error}}", | ||||
| 		"error_delete_blacklist": "Error deleting from blacklist: {{error}}", | ||||
| 		"error_fetching_config": "Error while fetching configuration", | ||||
| 		"error_trying_delete": "Error while trying to delete: {{error}}", | ||||
| 		"error_update": "Error: {{error}}", | ||||
| @@ -261,6 +276,7 @@ | ||||
| 	"contact": { | ||||
| 		"access_pin": "Access PIN", | ||||
| 		"add_contact": "Add Contact", | ||||
| 		"contact": "Contact", | ||||
| 		"create_contact": "Create Contact", | ||||
| 		"currently_selected_contact": "Currently Selected Contact: {{contact}}", | ||||
| 		"delete": "Delete Contact?", | ||||
| @@ -300,12 +316,23 @@ | ||||
| 		"healthchecks_title": "Delete Healthchecks" | ||||
| 	}, | ||||
| 	"device": { | ||||
| 		"add_to_blacklist": "Add Device To Blacklist", | ||||
| 		"all_devices": "All Devices", | ||||
| 		"blacklisted_on": "Date", | ||||
| 		"capabilities": "Capabilities", | ||||
| 		"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_devices": "Error while fetching devices: {{error}}", | ||||
| 		"health_explanation": "Health of connected devices", | ||||
| 		"memory_explanation": "Memory used by connected devices", | ||||
| 		"uptimes_explanation": "Time connected devices have been up and connected" | ||||
| 		"health_explanation": "Health of connected devices ((Devices=100% * 100 + Devices>90% * 95 + Devices>60% * 75 + Devices<60% * 35) / ConnectedDevices)", | ||||
| 		"memory_explanation": "Amount of connected devices with corresponding memory used percentage", | ||||
| 		"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": { | ||||
| 		"log": "Log", | ||||
| @@ -320,23 +347,32 @@ | ||||
| 		"add_success": "Entity Successfully Created!", | ||||
| 		"assigned_inventory": "Assigned Inventory", | ||||
| 		"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_venue": "Currently Selected Venue: {{config}}", | ||||
| 		"delete_success": "Entity Successfully Deleted", | ||||
| 		"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}}", | ||||
| 		"enter_here": "Enter the IP(s) you'd like to add here", | ||||
| 		"entire_tree": "Site Map", | ||||
| 		"entire_tree": "Network Map", | ||||
| 		"entities": "Entities", | ||||
| 		"entity": "Entity", | ||||
| 		"error_deleting_map": "Error deleting map: {{error}}", | ||||
| 		"error_fetch_entity": "Error while fetching entity information", | ||||
| 		"error_fetching": "Error while fetching entities", | ||||
| 		"error_fetching_map": "Error fetching map: {{error}}", | ||||
| 		"error_fetching_tree": "Error while fetching tree: {{error}}", | ||||
| 		"error_saving": "Error while saving entity", | ||||
| 		"error_saving_map": "Error saving map: {{error}}", | ||||
| 		"higher_priority": "Make Higher Priority", | ||||
| 		"ip_detection": "IP Detection", | ||||
| 		"ip_formats": "You can add IPv4 or IPv6 addresses in the following formats:", | ||||
| 		"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", | ||||
| 		"no_ips": "No IPs selected", | ||||
| 		"not_assigned": "Not Assigned", | ||||
| @@ -344,6 +380,7 @@ | ||||
| 		"select_entity": "Select this Entity", | ||||
| 		"selected_entity": "Selected Entity", | ||||
| 		"selected_map": "Selected Map", | ||||
| 		"tree_saved": "Map Successfully Saved!", | ||||
| 		"update_failure_error": "Error while trying to update entity: {{error}}", | ||||
| 		"valid_serial": "Needs to be a valid serial number (12 HEX characters)", | ||||
| 		"venues": "Venues" | ||||
| @@ -541,6 +578,9 @@ | ||||
| 		"verification_code": "Enter your verification here", | ||||
| 		"wrong_code": "The verification code that was entered is not valid. " | ||||
| 	}, | ||||
| 	"preferences": { | ||||
| 		"provisioning": "Provisioning" | ||||
| 	}, | ||||
| 	"reboot": { | ||||
| 		"directions": "When would you like to reboot this device?", | ||||
| 		"now": "Would you like to reboot this device now?", | ||||
| @@ -584,7 +624,7 @@ | ||||
| 		"mac_prefix": "MAC Prefix", | ||||
| 		"max_associations": "Max. Associations", | ||||
| 		"max_clients": "Max. Clients", | ||||
| 		"messages_transmitted": "Messages Transmitted", | ||||
| 		"messages_transmitted": "Msgs TX", | ||||
| 		"min_associations": "Min. Associations", | ||||
| 		"min_clients": "Min. Clients", | ||||
| 		"pause": "Pause", | ||||
| @@ -592,7 +632,7 @@ | ||||
| 		"prefix_length": "Required, needs to be of a length of 6 characters", | ||||
| 		"previous_runs": "Previous Runs", | ||||
| 		"received": "Received", | ||||
| 		"received_messages": "Messages Received", | ||||
| 		"received_messages": "Msgs RX", | ||||
| 		"reconnect_interval": "Reconnect Interval", | ||||
| 		"resume": "Resume", | ||||
| 		"resume_success": "Run Resumed!", | ||||
| @@ -634,6 +674,19 @@ | ||||
| 		"uptime": "Uptime", | ||||
| 		"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": { | ||||
| 		"error_fetching": "Error while fetching system information", | ||||
| 		"error_reloading": "Error while reloading: {{error}}", | ||||
| @@ -723,6 +776,7 @@ | ||||
| 		"send_code_again": "Send Code Again", | ||||
| 		"show_hide_password": "Show/Hide Password", | ||||
| 		"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_title": "Update Failed", | ||||
| 		"update_success": "User Updated Successfully", | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| 		"blink": "Parpadeo", | ||||
| 		"device_leds": "LED de dispositivo", | ||||
| 		"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:", | ||||
| 		"set_leds": "Establecer LED", | ||||
| 		"when_blink_leds": "¿Cuándo desea que los LED del dispositivo parpadeen?" | ||||
| @@ -37,9 +38,11 @@ | ||||
| 		"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", | ||||
| 		"adding_ellipsis": "Añadiendo ...", | ||||
| 		"all": "TODOS", | ||||
| 		"are_you_sure": "¿Estás seguro?", | ||||
| 		"back_to_login": "Atrás para iniciar sesión", | ||||
| 		"back_to_start": "volver a empezar", | ||||
| 		"blacklist": "Lista negra", | ||||
| 		"by": "Por", | ||||
| 		"cancel": "Cancelar", | ||||
| 		"certificate": "Certificado", | ||||
| @@ -62,12 +65,14 @@ | ||||
| 		"create": "Crear", | ||||
| 		"created": "creado", | ||||
| 		"created_by": "Creado por", | ||||
| 		"creator": "Creador", | ||||
| 		"current": "Corriente", | ||||
| 		"custom_date": "Fecha personalizada", | ||||
| 		"dashboard": "Tablero", | ||||
| 		"date": "Fecha", | ||||
| 		"day": "día", | ||||
| 		"days": "días", | ||||
| 		"default_map": "Mapa predeterminado", | ||||
| 		"delete": "Borrar", | ||||
| 		"delete_device": "Eliminar dispositivo", | ||||
| 		"details": "Detalles", | ||||
| @@ -85,6 +90,7 @@ | ||||
| 		"dismiss": "Despedir", | ||||
| 		"do_now": "¡Hagan ahora!", | ||||
| 		"download": "Descargar", | ||||
| 		"duplicate": "Duplicar", | ||||
| 		"duration": "Duración", | ||||
| 		"edit": "Editar", | ||||
| 		"edit_user": "Editar", | ||||
| @@ -94,6 +100,7 @@ | ||||
| 		"error": "Error", | ||||
| 		"error_adding_note": "Error al agregar una nota", | ||||
| 		"error_code": "código de error", | ||||
| 		"errors": "Los errores", | ||||
| 		"execute_now": "¿Le gustaría ejecutar este comando ahora?", | ||||
| 		"executed": "ejecutado", | ||||
| 		"exit": "salida", | ||||
| @@ -142,14 +149,16 @@ | ||||
| 		"no_items": "No hay articulos", | ||||
| 		"none": "Ninguna", | ||||
| 		"not_connected": "No conectado", | ||||
| 		"of_connected": "% de dispositivos", | ||||
| 		"of_connected": "% de dispositivos conectados", | ||||
| 		"off": "Apagado", | ||||
| 		"on": "en", | ||||
| 		"optional": "Opcional", | ||||
| 		"overall_health": "Salud en general", | ||||
| 		"password_policy": "Política de contraseñas", | ||||
| 		"preferences": "Preferencias", | ||||
| 		"preview": "Avance", | ||||
| 		"program": "Programa", | ||||
| 		"reason": "Razón", | ||||
| 		"recorded": "Grabado", | ||||
| 		"refresh": "Refrescar", | ||||
| 		"refresh_device": "Actualizar dispositivo", | ||||
| @@ -170,12 +179,14 @@ | ||||
| 		"show_all": "Mostrar todo", | ||||
| 		"socket_connection_closed": "¡Conexión cerrada!", | ||||
| 		"start": "comienzo", | ||||
| 		"status": "Estado", | ||||
| 		"stop_editing": "Dejar de editar", | ||||
| 		"submit": "Enviar", | ||||
| 		"submitted": "Presentado", | ||||
| 		"success": "Éxito", | ||||
| 		"system": "Sistema", | ||||
| 		"table": "Mesa", | ||||
| 		"time_per_device": "Dispositivo / segundo", | ||||
| 		"timestamp": "hora", | ||||
| 		"to": "a", | ||||
| 		"type": "Tipo", | ||||
| @@ -190,6 +201,7 @@ | ||||
| 		"uuid": "UUID", | ||||
| 		"vendors": "Vendedores", | ||||
| 		"view_more": "Ver más", | ||||
| 		"visibility": "Visibilidad", | ||||
| 		"yes": "Sí" | ||||
| 	}, | ||||
| 	"configuration": { | ||||
| @@ -210,6 +222,8 @@ | ||||
| 		"creation_success": "¡Configuración creada con éxito!", | ||||
| 		"currently_associated": "Configuración asociada actual: {{config}}", | ||||
| 		"currently_selected_config": "Configuración seleccionada actualmente: {{config}}", | ||||
| 		"default_configs": "Configuraciones predeterminadas", | ||||
| 		"default_configurations": "Configuraciones predeterminadas", | ||||
| 		"delete_config": "Eliminar Configuración", | ||||
| 		"details": "Detalles", | ||||
| 		"device_password": "Contraseña", | ||||
| @@ -218,6 +232,7 @@ | ||||
| 		"devices_affected": "Dispositivos afectados por esta configuración:", | ||||
| 		"edit_configuration": "Editar configuración", | ||||
| 		"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_trying_delete": "Error al intentar eliminar: {{error}}", | ||||
| 		"error_update": "Error: {{error}}", | ||||
| @@ -261,6 +276,7 @@ | ||||
| 	"contact": { | ||||
| 		"access_pin": "PIN de acceso", | ||||
| 		"add_contact": "Agregar contacto", | ||||
| 		"contact": "Contacto", | ||||
| 		"create_contact": "Crear contacto", | ||||
| 		"currently_selected_contact": "Contacto seleccionado actualmente: {{contact}}", | ||||
| 		"delete": "¿Borrar contacto?", | ||||
| @@ -300,12 +316,23 @@ | ||||
| 		"healthchecks_title": "Eliminar comprobaciones de estado" | ||||
| 	}, | ||||
| 	"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", | ||||
| 		"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_devices": "Error al recuperar dispositivos: {{error}}", | ||||
| 		"health_explanation": "Salud de los dispositivos conectados", | ||||
| 		"memory_explanation": "Memoria utilizada por dispositivos conectados", | ||||
| 		"uptimes_explanation": "Tiempo que los dispositivos conectados han estado en funcionamiento y conectados" | ||||
| 		"health_explanation": "Estado de los dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos conectados)", | ||||
| 		"memory_explanation": "Cantidad de dispositivos conectados con la memoria correspondiente utilizada%", | ||||
| 		"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": { | ||||
| 		"log": "Iniciar sesión", | ||||
| @@ -320,23 +347,32 @@ | ||||
| 		"add_success": "¡Entidad creada con éxito!", | ||||
| 		"assigned_inventory": "Inventario asignado", | ||||
| 		"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_venue": "Lugar seleccionado actualmente: {{config}}", | ||||
| 		"delete_success": "Entidad eliminada correctamente", | ||||
| 		"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}}", | ||||
| 		"enter_here": "Ingrese las IP que desea agregar aquí", | ||||
| 		"entire_tree": "Site MAp", | ||||
| 		"entire_tree": "Mapa de red", | ||||
| 		"entities": "entidades", | ||||
| 		"entity": "Entidad", | ||||
| 		"error_deleting_map": "Error al eliminar el mapa: {{error}}", | ||||
| 		"error_fetch_entity": "Error al obtener la información de la entidad", | ||||
| 		"error_fetching": "Error al recuperar entidades", | ||||
| 		"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_map": "Error al guardar el mapa: {{error}}", | ||||
| 		"higher_priority": "Dar mayor prioridad", | ||||
| 		"ip_detection": "Detección de IP", | ||||
| 		"ip_formats": "Puede agregar direcciones IPv4 o IPv6 en los siguientes formatos:", | ||||
| 		"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", | ||||
| 		"no_ips": "No se seleccionaron direcciones IP", | ||||
| 		"not_assigned": "No asignado", | ||||
| @@ -344,6 +380,7 @@ | ||||
| 		"select_entity": "Seleccione esta entidad", | ||||
| 		"selected_entity": "Entidad seleccionada", | ||||
| 		"selected_map": "Mapa seleccionado", | ||||
| 		"tree_saved": "¡Mapa guardado con éxito!", | ||||
| 		"update_failure_error": "Error al intentar actualizar la entidad: {{error}}", | ||||
| 		"valid_serial": "Debe ser un número de serie válido (12 caracteres HEX)", | ||||
| 		"venues": "Sedes" | ||||
| @@ -541,6 +578,9 @@ | ||||
| 		"verification_code": "Ingrese su verificación aquí", | ||||
| 		"wrong_code": "El código de verificación que se ingresó no es válido." | ||||
| 	}, | ||||
| 	"preferences": { | ||||
| 		"provisioning": "Aprovisionamiento" | ||||
| 	}, | ||||
| 	"reboot": { | ||||
| 		"directions": "¿Cuándo le gustaría reiniciar este dispositivo?", | ||||
| 		"now": "¿Le gustaría reiniciar este dispositivo ahora?", | ||||
| @@ -584,7 +624,7 @@ | ||||
| 		"mac_prefix": "Prefijo MAC", | ||||
| 		"max_associations": "Max. Asociaciones", | ||||
| 		"max_clients": "Max. Clientela", | ||||
| 		"messages_transmitted": "Mensajes transmitidos", | ||||
| 		"messages_transmitted": "Mensajes TX", | ||||
| 		"min_associations": "Min. Asociaciones", | ||||
| 		"min_clients": "Min. Clientela", | ||||
| 		"pause": "pausa", | ||||
| @@ -592,7 +632,7 @@ | ||||
| 		"prefix_length": "Obligatorio, debe tener una longitud de 6 caracteres", | ||||
| 		"previous_runs": "Ejecuciones anteriores", | ||||
| 		"received": "recibido", | ||||
| 		"received_messages": "Mensajes recibidos", | ||||
| 		"received_messages": "Msgs RX", | ||||
| 		"reconnect_interval": "Intervalo de reconexión", | ||||
| 		"resume": "Currículum", | ||||
| 		"resume_success": "¡Ejecutar reanudado!", | ||||
| @@ -634,6 +674,19 @@ | ||||
| 		"uptime": "Tiempo de actividad", | ||||
| 		"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": { | ||||
| 		"error_fetching": "Error al obtener información del sistema", | ||||
| 		"error_reloading": "Error al recargar: {{error}}", | ||||
| @@ -723,6 +776,7 @@ | ||||
| 		"send_code_again": "Enviar Código De nuevo", | ||||
| 		"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", | ||||
| 		"table_title": "Usuarios administrativos", | ||||
| 		"update_failure": "Error al intentar actualizar: {{error}}", | ||||
| 		"update_failure_title": "Actualización fallida", | ||||
| 		"update_success": "Usuario actualizado con éxito", | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| 		"blink": "Cligner", | ||||
| 		"device_leds": "LED de l'appareil", | ||||
| 		"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 :", | ||||
| 		"set_leds": "Définir les LED", | ||||
| 		"when_blink_leds": "Quand souhaitez-vous faire clignoter les LED de l'appareil ?" | ||||
| @@ -37,9 +38,11 @@ | ||||
| 		"add_note": "Ajouter une note", | ||||
| 		"add_note_explanation": "Écrivez votre nouvelle note ci-dessous et cliquez sur le bouton '+' où vous avez terminé", | ||||
| 		"adding_ellipsis": "Ajouter...", | ||||
| 		"all": "Tout", | ||||
| 		"are_you_sure": "Êtes-vous sûr?", | ||||
| 		"back_to_login": "Retour connexion", | ||||
| 		"back_to_start": "Retour au début", | ||||
| 		"blacklist": "Liste noire", | ||||
| 		"by": "Par", | ||||
| 		"cancel": "annuler", | ||||
| 		"certificate": "Certificat", | ||||
| @@ -62,12 +65,14 @@ | ||||
| 		"create": "Créer", | ||||
| 		"created": "Créé", | ||||
| 		"created_by": "Créé par", | ||||
| 		"creator": "Créateur", | ||||
| 		"current": "Actuel", | ||||
| 		"custom_date": "Date personnalisée", | ||||
| 		"dashboard": "Tableau de bord", | ||||
| 		"date": "Rendez-vous amoureux", | ||||
| 		"day": "journée", | ||||
| 		"days": "journées", | ||||
| 		"default_map": "Carte par défaut", | ||||
| 		"delete": "Effacer", | ||||
| 		"delete_device": "Supprimer le périphérique", | ||||
| 		"details": "Détails", | ||||
| @@ -85,6 +90,7 @@ | ||||
| 		"dismiss": "Rejeter", | ||||
| 		"do_now": "Faire maintenant!", | ||||
| 		"download": "Télécharger", | ||||
| 		"duplicate": "Dupliquer", | ||||
| 		"duration": "Durée", | ||||
| 		"edit": "modifier", | ||||
| 		"edit_user": "Modifier", | ||||
| @@ -94,6 +100,7 @@ | ||||
| 		"error": "Erreur", | ||||
| 		"error_adding_note": "Erreur lors de l'ajout de la note", | ||||
| 		"error_code": "Code d'erreur", | ||||
| 		"errors": "les erreurs", | ||||
| 		"execute_now": "Souhaitez-vous exécuter cette commande maintenant ?", | ||||
| 		"executed": "réalisé", | ||||
| 		"exit": "Sortie", | ||||
| @@ -142,14 +149,16 @@ | ||||
| 		"no_items": "Pas d'objet", | ||||
| 		"none": "Aucun", | ||||
| 		"not_connected": "Pas connecté", | ||||
| 		"of_connected": "% d'appareils", | ||||
| 		"of_connected": "% d'appareils connectés", | ||||
| 		"off": "De", | ||||
| 		"on": "sur", | ||||
| 		"optional": "Optionnel", | ||||
| 		"overall_health": "Santé globale", | ||||
| 		"password_policy": "Politique de mot de passe", | ||||
| 		"preferences": "Préférences", | ||||
| 		"preview": "Aperçu", | ||||
| 		"program": "Programme", | ||||
| 		"reason": "raison", | ||||
| 		"recorded": "Enregistré", | ||||
| 		"refresh": "Rafraîchir", | ||||
| 		"refresh_device": "Actualiser l'appareil", | ||||
| @@ -170,12 +179,14 @@ | ||||
| 		"show_all": "Montre tout", | ||||
| 		"socket_connection_closed": "Connexion fermée !", | ||||
| 		"start": "Début", | ||||
| 		"status": "Statut", | ||||
| 		"stop_editing": "Arrêter la modification", | ||||
| 		"submit": "Soumettre", | ||||
| 		"submitted": "Soumis", | ||||
| 		"success": "Succès", | ||||
| 		"system": "Système", | ||||
| 		"table": "Table", | ||||
| 		"time_per_device": "Appareils/Seconde", | ||||
| 		"timestamp": "Temps", | ||||
| 		"to": "à", | ||||
| 		"type": "Type", | ||||
| @@ -190,6 +201,7 @@ | ||||
| 		"uuid": "UUID", | ||||
| 		"vendors": "Vendeurs", | ||||
| 		"view_more": "Afficher plus", | ||||
| 		"visibility": "Visibilité", | ||||
| 		"yes": "Oui" | ||||
| 	}, | ||||
| 	"configuration": { | ||||
| @@ -210,6 +222,8 @@ | ||||
| 		"creation_success": "Configuration créée avec succès !", | ||||
| 		"currently_associated": "Configuration associée actuelle : {{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", | ||||
| 		"details": "Détails", | ||||
| 		"device_password": "Mot de passe", | ||||
| @@ -218,6 +232,7 @@ | ||||
| 		"devices_affected": "Appareils concernés par cette configuration :", | ||||
| 		"edit_configuration": "Modifier la configuration", | ||||
| 		"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_trying_delete": "Erreur lors de la tentative de suppression : {{error}}", | ||||
| 		"error_update": "Erreur: {{error}}", | ||||
| @@ -261,6 +276,7 @@ | ||||
| 	"contact": { | ||||
| 		"access_pin": "NIP d'accès", | ||||
| 		"add_contact": "Ajouter le contact", | ||||
| 		"contact": "Contact", | ||||
| 		"create_contact": "Créer un contact", | ||||
| 		"currently_selected_contact": "Contact actuellement sélectionné : {{contact}}", | ||||
| 		"delete": "Effacer le contact?", | ||||
| @@ -300,12 +316,23 @@ | ||||
| 		"healthchecks_title": "Supprimer les vérifications d'état" | ||||
| 	}, | ||||
| 	"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", | ||||
| 		"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_devices": "Erreur lors de la récupération des appareils : {{error}}", | ||||
| 		"health_explanation": "Santé des appareils connectés", | ||||
| 		"memory_explanation": "Mémoire utilisée par les appareils connectés", | ||||
| 		"uptimes_explanation": "Heure à laquelle les appareils connectés ont été activés et 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": "Nombre d'appareils connectés avec la mémoire correspondante utilisée %", | ||||
| 		"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": { | ||||
| 		"log": "Bûche", | ||||
| @@ -320,23 +347,32 @@ | ||||
| 		"add_success": "Entité créée avec succès !", | ||||
| 		"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.", | ||||
| 		"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_venue": "Lieu actuellement sélectionné : {{config}}", | ||||
| 		"delete_success": "Entité supprimée avec succès", | ||||
| 		"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}}", | ||||
| 		"enter_here": "Entrez les IP que vous souhaitez ajouter ici", | ||||
| 		"entire_tree": "Site MAp", | ||||
| 		"entire_tree": "Carte du réseau", | ||||
| 		"entities": "Entités", | ||||
| 		"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_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_tree": "Erreur lors de la récupération de l'arborescence : {{error}}", | ||||
| 		"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", | ||||
| 		"ip_detection": "Détection IP", | ||||
| 		"ip_formats": "Vous pouvez ajouter des adresses IPv4 ou IPv6 aux formats suivants :", | ||||
| 		"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", | ||||
| 		"no_ips": "Aucune adresse IP sélectionnée", | ||||
| 		"not_assigned": "Non attribué", | ||||
| @@ -344,6 +380,7 @@ | ||||
| 		"select_entity": "Sélectionnez cette entité", | ||||
| 		"selected_entity": "Entité 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}}", | ||||
| 		"valid_serial": "Doit être un numéro de série valide (12 caractères HEX)", | ||||
| 		"venues": "Les lieux" | ||||
| @@ -541,6 +578,9 @@ | ||||
| 		"verification_code": "Entrez votre vérification ici", | ||||
| 		"wrong_code": "Le code de vérification saisi n'est pas valide." | ||||
| 	}, | ||||
| 	"preferences": { | ||||
| 		"provisioning": "Provisioning" | ||||
| 	}, | ||||
| 	"reboot": { | ||||
| 		"directions": "Quand souhaitez-vous redémarrer cet appareil ?", | ||||
| 		"now": "Souhaitez-vous redémarrer cet appareil maintenant ?", | ||||
| @@ -584,7 +624,7 @@ | ||||
| 		"mac_prefix": "Préfixe MAC", | ||||
| 		"max_associations": "Max. Les associations", | ||||
| 		"max_clients": "Max. Clients", | ||||
| 		"messages_transmitted": "Messages transmis", | ||||
| 		"messages_transmitted": "Émission de messages", | ||||
| 		"min_associations": "Min. Les associations", | ||||
| 		"min_clients": "Min. Clients", | ||||
| 		"pause": "Pause", | ||||
| @@ -592,7 +632,7 @@ | ||||
| 		"prefix_length": "Obligatoire, doit être d'une longueur de 6 caractères", | ||||
| 		"previous_runs": "Courses précédentes", | ||||
| 		"received": "reçu", | ||||
| 		"received_messages": "Messages reçus", | ||||
| 		"received_messages": "Réception des messages", | ||||
| 		"reconnect_interval": "Intervalle de reconnexion", | ||||
| 		"resume": "CV", | ||||
| 		"resume_success": "Exécution reprise !", | ||||
| @@ -634,6 +674,19 @@ | ||||
| 		"uptime": "La disponibilité", | ||||
| 		"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": { | ||||
| 		"error_fetching": "Erreur lors de la récupération des informations système", | ||||
| 		"error_reloading": "Erreur lors du rechargement : {{error}}", | ||||
| @@ -723,6 +776,7 @@ | ||||
| 		"send_code_again": "Envoyer code à nouveau", | ||||
| 		"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", | ||||
| 		"table_title": "Utilisateurs administrateurs", | ||||
| 		"update_failure": "Erreur lors de la tentative de mise à jour : {{error}}", | ||||
| 		"update_failure_title": "mise à jour a échoué", | ||||
| 		"update_success": "L'utilisateur a bien été mis à jour", | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| 		"blink": "Piscar", | ||||
| 		"device_leds": "LEDs do dispositivo", | ||||
| 		"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:", | ||||
| 		"set_leds": "Definir LEDs", | ||||
| 		"when_blink_leds": "Quando você gostaria de fazer os LEDs do dispositivo piscarem?" | ||||
| @@ -37,9 +38,11 @@ | ||||
| 		"add_note": "Adicionar nota", | ||||
| 		"add_note_explanation": "Escreva sua nova nota abaixo e clique no botão '+' quando terminar", | ||||
| 		"adding_ellipsis": "Adicionando ...", | ||||
| 		"all": "Todos", | ||||
| 		"are_you_sure": "Você tem certeza?", | ||||
| 		"back_to_login": "Volte ao login", | ||||
| 		"back_to_start": "Voltar ao Início", | ||||
| 		"blacklist": "Lista negra", | ||||
| 		"by": "Por", | ||||
| 		"cancel": "Cancelar", | ||||
| 		"certificate": "Certificado", | ||||
| @@ -62,12 +65,14 @@ | ||||
| 		"create": "Crio", | ||||
| 		"created": "Criado", | ||||
| 		"created_by": "Criado Por", | ||||
| 		"creator": "O Criador", | ||||
| 		"current": "Atual", | ||||
| 		"custom_date": "Data personalizada", | ||||
| 		"dashboard": "painel de controle", | ||||
| 		"date": "Encontro", | ||||
| 		"day": "dia", | ||||
| 		"days": "dias", | ||||
| 		"default_map": "Mapa Padrão", | ||||
| 		"delete": "Excluir", | ||||
| 		"delete_device": "Apagar dispositivo", | ||||
| 		"details": "Detalhes", | ||||
| @@ -85,6 +90,7 @@ | ||||
| 		"dismiss": "Dispensar", | ||||
| 		"do_now": "Faça agora!", | ||||
| 		"download": "Baixar", | ||||
| 		"duplicate": "Duplicado", | ||||
| 		"duration": "Duração", | ||||
| 		"edit": "Editar", | ||||
| 		"edit_user": "Editar", | ||||
| @@ -94,6 +100,7 @@ | ||||
| 		"error": "Erro", | ||||
| 		"error_adding_note": "Erro ao adicionar nota", | ||||
| 		"error_code": "Erro de código", | ||||
| 		"errors": "Erros", | ||||
| 		"execute_now": "Você gostaria de executar este comando agora?", | ||||
| 		"executed": "Executado", | ||||
| 		"exit": "Saída", | ||||
| @@ -142,14 +149,16 @@ | ||||
| 		"no_items": "Nenhum item", | ||||
| 		"none": "Nenhum", | ||||
| 		"not_connected": "Não conectado", | ||||
| 		"of_connected": "% de dispositivos", | ||||
| 		"of_connected": "% de dispositivos conectados", | ||||
| 		"off": "Fora", | ||||
| 		"on": "em", | ||||
| 		"optional": "Opcional", | ||||
| 		"overall_health": "Saúde geral", | ||||
| 		"password_policy": "Política de Senha", | ||||
| 		"preferences": "Preferências", | ||||
| 		"preview": "Visualizar", | ||||
| 		"program": "Programa", | ||||
| 		"reason": "RAZÃO", | ||||
| 		"recorded": "Gravado", | ||||
| 		"refresh": "REFRESH", | ||||
| 		"refresh_device": "Atualizar dispositivo", | ||||
| @@ -170,12 +179,14 @@ | ||||
| 		"show_all": "mostre tudo", | ||||
| 		"socket_connection_closed": "Conexão fechada!", | ||||
| 		"start": "Começar", | ||||
| 		"status": "Status", | ||||
| 		"stop_editing": "Pare de editar", | ||||
| 		"submit": "Enviar", | ||||
| 		"submitted": "Submetido", | ||||
| 		"success": "Sucesso", | ||||
| 		"system": "Sistema", | ||||
| 		"table": "Mesa", | ||||
| 		"time_per_device": "Dispositivo / segundo", | ||||
| 		"timestamp": "tempo", | ||||
| 		"to": "Para", | ||||
| 		"type": "Tipo", | ||||
| @@ -190,6 +201,7 @@ | ||||
| 		"uuid": "UUID", | ||||
| 		"vendors": "Vendedores", | ||||
| 		"view_more": "Veja mais", | ||||
| 		"visibility": "visibilidade", | ||||
| 		"yes": "sim" | ||||
| 	}, | ||||
| 	"configuration": { | ||||
| @@ -210,6 +222,8 @@ | ||||
| 		"creation_success": "Configuração criada com sucesso!", | ||||
| 		"currently_associated": "Configuração atual associada: {{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", | ||||
| 		"details": "Detalhes", | ||||
| 		"device_password": "Senha", | ||||
| @@ -218,6 +232,7 @@ | ||||
| 		"devices_affected": "Dispositivos afetados por esta configuração:", | ||||
| 		"edit_configuration": "Editar configuração", | ||||
| 		"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_trying_delete": "Erro ao tentar excluir: {{error}}", | ||||
| 		"error_update": "Erro: {{error}}", | ||||
| @@ -261,6 +276,7 @@ | ||||
| 	"contact": { | ||||
| 		"access_pin": "PIN de acesso", | ||||
| 		"add_contact": "Adicionar contato", | ||||
| 		"contact": "Contato", | ||||
| 		"create_contact": "Criar Contato", | ||||
| 		"currently_selected_contact": "Contato atualmente selecionado: {{contact}}", | ||||
| 		"delete": "Excluir contato?", | ||||
| @@ -300,12 +316,23 @@ | ||||
| 		"healthchecks_title": "Excluir verificações de saúde" | ||||
| 	}, | ||||
| 	"device": { | ||||
| 		"add_to_blacklist": "Adicionar dispositivo à lista negra", | ||||
| 		"all_devices": "Todos os dispositivos", | ||||
| 		"blacklisted_on": "Encontro", | ||||
| 		"capabilities": "Recursos", | ||||
| 		"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_devices": "Erro ao buscar dispositivos: {{error}}", | ||||
| 		"health_explanation": "Saúde de dispositivos conectados", | ||||
| 		"memory_explanation": "Memória usada por dispositivos conectados", | ||||
| 		"uptimes_explanation": "Há tempo em que os dispositivos conectados estão ativados e conectados" | ||||
| 		"health_explanation": "Integridade dos dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos Conectados)", | ||||
| 		"memory_explanation": "Quantidade de dispositivos conectados com a memória correspondente usada%", | ||||
| 		"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": { | ||||
| 		"log": "Registro", | ||||
| @@ -320,23 +347,32 @@ | ||||
| 		"add_success": "Entidade criada com sucesso!", | ||||
| 		"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.", | ||||
| 		"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_venue": "Local selecionado atualmente: {{config}}", | ||||
| 		"delete_success": "Entidade excluída com sucesso", | ||||
| 		"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}}", | ||||
| 		"enter_here": "Digite o (s) IP (s) que deseja adicionar aqui", | ||||
| 		"entire_tree": "Mapa do Site", | ||||
| 		"entire_tree": "Mapa de Rede", | ||||
| 		"entities": "Entidades", | ||||
| 		"entity": "Entidade", | ||||
| 		"error_deleting_map": "Erro ao excluir mapa: {{error}}", | ||||
| 		"error_fetch_entity": "Erro ao buscar informações da entidade", | ||||
| 		"error_fetching": "Erro ao buscar entidades", | ||||
| 		"error_fetching_map": "Erro ao buscar mapa: {{error}}", | ||||
| 		"error_fetching_tree": "Erro ao buscar árvore: {{error}}", | ||||
| 		"error_saving": "Erro ao salvar entidade", | ||||
| 		"error_saving_map": "Erro ao salvar o mapa: {{error}}", | ||||
| 		"higher_priority": "Dê maior prioridade", | ||||
| 		"ip_detection": "Detecção de IP", | ||||
| 		"ip_formats": "Você pode adicionar endereços IPv4 ou IPv6 nos seguintes formatos:", | ||||
| 		"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", | ||||
| 		"no_ips": "Nenhum IP selecionado", | ||||
| 		"not_assigned": "Não atribuído", | ||||
| @@ -344,6 +380,7 @@ | ||||
| 		"select_entity": "Selecione esta Entidade", | ||||
| 		"selected_entity": "Entidade Selecionada", | ||||
| 		"selected_map": "Mapa Selecionado", | ||||
| 		"tree_saved": "Mapa salvo com sucesso!", | ||||
| 		"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)", | ||||
| 		"venues": "Locais" | ||||
| @@ -541,6 +578,9 @@ | ||||
| 		"verification_code": "Insira sua verificação aqui", | ||||
| 		"wrong_code": "O código de verificação inserido não é válido." | ||||
| 	}, | ||||
| 	"preferences": { | ||||
| 		"provisioning": "Provisioning" | ||||
| 	}, | ||||
| 	"reboot": { | ||||
| 		"directions": "Quando você gostaria de reinicializar este dispositivo?", | ||||
| 		"now": "Você gostaria de reiniciar este dispositivo agora?", | ||||
| @@ -584,7 +624,7 @@ | ||||
| 		"mac_prefix": "Prefixo MAC", | ||||
| 		"max_associations": "Máx. Associações", | ||||
| 		"max_clients": "Máx. Clientes", | ||||
| 		"messages_transmitted": "Mensagens Transmitidas", | ||||
| 		"messages_transmitted": "Msgs TX", | ||||
| 		"min_associations": "Min. Associações", | ||||
| 		"min_clients": "Min. Clientes", | ||||
| 		"pause": "pausa", | ||||
| @@ -592,7 +632,7 @@ | ||||
| 		"prefix_length": "Obrigatório, deve ter 6 caracteres", | ||||
| 		"previous_runs": "Execuções anteriores", | ||||
| 		"received": "recebido", | ||||
| 		"received_messages": "Mensagens recebidas", | ||||
| 		"received_messages": "Msgs RX", | ||||
| 		"reconnect_interval": "Intervalo de reconexão", | ||||
| 		"resume": "Currículo", | ||||
| 		"resume_success": "Executar retomado!", | ||||
| @@ -634,6 +674,19 @@ | ||||
| 		"uptime": "Tempo de atividade", | ||||
| 		"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": { | ||||
| 		"error_fetching": "Erro ao buscar informações do sistema", | ||||
| 		"error_reloading": "Erro ao recarregar: {{error}}", | ||||
| @@ -723,6 +776,7 @@ | ||||
| 		"send_code_again": "Envie o Código Novamente", | ||||
| 		"show_hide_password": "Mostrar / ocultar senha", | ||||
| 		"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_title": "Atualização falhou", | ||||
| 		"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, | ||||
|   cilAsterisk, | ||||
|   cilBan, | ||||
|   cilBarcode, | ||||
|   cilBasket, | ||||
|   cilBell, | ||||
|   cilBold, | ||||
| @@ -108,6 +109,7 @@ export const icons = { | ||||
|   cilArrowTop, | ||||
|   cilAsterisk, | ||||
|   cilBan, | ||||
|   cilBarcode, | ||||
|   cilBasket, | ||||
|   cilBell, | ||||
|   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, | ||||
|   CModalBody, | ||||
|   CModalFooter, | ||||
|   CSwitch, | ||||
|   CCol, | ||||
|   CRow, | ||||
|   CFormGroup, | ||||
|   CInputRadio, | ||||
|   CLabel, | ||||
|   CPopover, | ||||
|   CRow, | ||||
| } from '@coreui/react'; | ||||
| import CIcon from '@coreui/icons-react'; | ||||
| import { cilX } from '@coreui/icons'; | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import DatePicker from 'react-widgets/DatePicker'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { dateToUnix } from 'utils/helper'; | ||||
| import 'react-widgets/styles.css'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import eventBus from 'utils/eventBus'; | ||||
| @@ -31,38 +28,24 @@ const BlinkModal = ({ show, toggleModal }) => { | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const { deviceSerialNumber } = useDevice(); | ||||
|   const { addToast } = useToast(); | ||||
|   const [isNow, setIsNow] = useState(false); | ||||
|   const [waiting, setWaiting] = useState(false); | ||||
|   const [chosenDate, setChosenDate] = useState(new Date().toString()); | ||||
|   const [chosenPattern, setPattern] = useState('on'); | ||||
|   const [chosenPattern, setPattern] = useState('blink'); | ||||
|   const [result, setResult] = useState(null); | ||||
|  | ||||
|   const toggleNow = () => { | ||||
|     setIsNow(!isNow); | ||||
|   }; | ||||
|  | ||||
|   const setDate = (date) => { | ||||
|     if (date) { | ||||
|       setChosenDate(date.toString()); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (show) { | ||||
|       setWaiting(false); | ||||
|       setChosenDate(new Date().toString()); | ||||
|       setPattern('on'); | ||||
|       setPattern('blink'); | ||||
|       setResult(null); | ||||
|     } | ||||
|   }, [show]); | ||||
|  | ||||
|   const doAction = () => { | ||||
|     setWaiting(true); | ||||
|     const utcDate = new Date(chosenDate); | ||||
|  | ||||
|     const parameters = { | ||||
|       serialNumber: deviceSerialNumber, | ||||
|       when: isNow ? 0 : dateToUnix(utcDate), | ||||
|       when: 0, | ||||
|       pattern: chosenPattern, | ||||
|       duration: 30, | ||||
|     }; | ||||
| @@ -113,11 +96,26 @@ const BlinkModal = ({ show, toggleModal }) => { | ||||
|       ) : ( | ||||
|         <div> | ||||
|           <CModalBody> | ||||
|             <CFormGroup row> | ||||
|             <CRow className="mb-3"> | ||||
|               <CCol>{t('blink.explanation')}</CCol> | ||||
|             </CRow> | ||||
|             <CFormGroup row className="mb-0"> | ||||
|               <CCol md="3"> | ||||
|                 <CLabel>{t('blink.pattern')}</CLabel> | ||||
|               </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> | ||||
|                   <CInputRadio | ||||
|                     custom | ||||
| @@ -142,55 +140,12 @@ const BlinkModal = ({ show, toggleModal }) => { | ||||
|                     {t('common.off')} | ||||
|                   </CLabel> | ||||
|                 </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> | ||||
|             </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> | ||||
|           <CModalFooter> | ||||
|             <LoadingButton | ||||
|               label={isNow ? t('blink.set_leds') : t('common.schedule')} | ||||
|               label={t('blink.set_leds')} | ||||
|               isLoadingLabel={t('common.loading_ellipsis')} | ||||
|               isLoading={waiting} | ||||
|               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' ? ( | ||||
|                                 <CIcon | ||||
|                                   name="cil-cloud-download" | ||||
|                                   content={cilCloudDownload} | ||||
|                                   size="md" | ||||
|                                 /> | ||||
|                                 <CIcon name="cil-cloud-download" content={cilCloudDownload} /> | ||||
|                               ) : ( | ||||
|                                 <CIcon | ||||
|                                   name="cil-calendar-check" | ||||
|                                   content={cilCalendarCheck} | ||||
|                                   size="md" | ||||
|                                 /> | ||||
|                                 <CIcon name="cil-calendar-check" content={cilCalendarCheck} /> | ||||
|                               )} | ||||
|                             </CButton> | ||||
|                           </CPopover> | ||||
| @@ -350,7 +342,7 @@ const DeviceCommands = () => { | ||||
|                                 toggleResponse(item); | ||||
|                               }} | ||||
|                             > | ||||
|                               <CIcon name="cilList" size="md" /> | ||||
|                               <CIcon name="cilList" /> | ||||
|                             </CButton> | ||||
|                           </CPopover> | ||||
|                           <CPopover content={t('common.delete')}> | ||||
| @@ -364,7 +356,7 @@ const DeviceCommands = () => { | ||||
|                                 toggleConfirmModal(item.UUID, index); | ||||
|                               }} | ||||
|                             > | ||||
|                               <CIcon name="cilTrash" size="mdå" /> | ||||
|                               <CIcon name="cilTrash" /> | ||||
|                             </CButton> | ||||
|                           </CPopover> | ||||
|                         </CButtonToolbar> | ||||
|   | ||||
| @@ -41,10 +41,12 @@ const ConfigurationDisplay = ({ getData, deviceConfig }) => { | ||||
|           /> | ||||
|         </h5> | ||||
|         <CRow> | ||||
|           <CCol md="2" xl="2" xxl="1"> | ||||
|             <CLabel>{t('configuration.last_configuration_change')}: </CLabel> | ||||
|           <CCol> | ||||
|             <CLabel> | ||||
|               {t('configuration.last_configuration_change')}:{' '} | ||||
|               {prettyDate(deviceConfig?.lastConfigurationChange)} | ||||
|             </CLabel> | ||||
|           </CCol> | ||||
|           <CCol>{prettyDate(deviceConfig?.lastConfigurationChange)}</CCol> | ||||
|         </CRow> | ||||
|         <pre className="ignore">{JSON.stringify(deviceConfig?.configuration ?? {}, null, 4)}</pre> | ||||
|       </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 PropTypes from 'prop-types'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { CButton, CCard, CCardHeader, CCardBody, CRow, CCol } from '@coreui/react'; | ||||
| 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 DeviceFirmwareModal from 'components/DeviceFirmwareModal'; | ||||
| import ConfigureModal from 'components/ConfigureModal'; | ||||
| @@ -13,7 +14,7 @@ import FactoryResetModal from 'components/FactoryResetModal'; | ||||
| import EventQueueModal from 'components/EventQueueModal'; | ||||
| import TelemetryModal from 'components/TelemetryModal'; | ||||
|  | ||||
| const DeviceActions = () => { | ||||
| const DeviceActions = ({ device }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const { addToast } = useToast(); | ||||
| @@ -21,35 +22,16 @@ const DeviceActions = () => { | ||||
|   const [upgradeStatus, setUpgradeStatus] = useState({ | ||||
|     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 [showConfigModal, setConfigModal] = useState(false); | ||||
|   const [showFactoryModal, setShowFactoryModal] = useState(false); | ||||
|   const [showQueueModal, setShowQueueModal] = useState(false); | ||||
|   const [showTelemetryModal, setShowTelemetryModal] = useState(false); | ||||
|  | ||||
|   const toggleRebootModal = () => setShowRebootModal(!showRebootModal); | ||||
|  | ||||
|   const toggleBlinkModal = () => setShowBlinkModal(!showBlinkModal); | ||||
|  | ||||
|   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 [showRebootModal, toggleRebootModal] = useToggle(false); | ||||
|   const [showBlinkModal, toggleBlinkModal] = useToggle(false); | ||||
|   const [showUpgradeModal, toggleUpgradeModal, setShowUpgradeModal] = useToggle(false); | ||||
|   const [showTraceModal, toggleTraceModal] = useToggle(false); | ||||
|   const [showScanModal, toggleScanModal] = useToggle(false); | ||||
|   const [showConfigModal, toggleConfigModal] = useToggle(false); | ||||
|   const [showFactoryModal, toggleFactoryResetModal] = useToggle(false); | ||||
|   const [showQueueModal, toggleQueueModal] = useToggle(false); | ||||
|   const [showTelemetryModal, toggleTelemetryModal] = useToggle(false); | ||||
|  | ||||
|   const getRttysInfo = () => { | ||||
|     setConnectLoading(true); | ||||
| @@ -67,6 +49,7 @@ const DeviceActions = () => { | ||||
|       ) | ||||
|       .then((response) => { | ||||
|         const url = `https://${response.data.server}:${response.data.viewport}/connect/${response.data.connectionId}`; | ||||
|  | ||||
|         const newWindow = window.open(url, '_blank', 'noopener,noreferrer'); | ||||
|         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(() => { | ||||
|     if (upgradeStatus.result !== undefined) { | ||||
|       addToast({ | ||||
| @@ -116,10 +83,6 @@ const DeviceActions = () => { | ||||
|     } | ||||
|   }, [upgradeStatus]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     getDeviceInformation(); | ||||
|   }, [deviceSerialNumber]); | ||||
|  | ||||
|   return ( | ||||
|     <CCard> | ||||
|       <CCardHeader className="dark-header"> | ||||
| @@ -128,36 +91,41 @@ const DeviceActions = () => { | ||||
|       <CCardBody> | ||||
|         <CRow> | ||||
|           <CCol> | ||||
|             <CButton block onClick={toggleRebootModal} color="primary"> | ||||
|             <CButton block disabled={device === null} onClick={toggleRebootModal} color="primary"> | ||||
|               {t('actions.reboot')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <CButton block onClick={toggleBlinkModal} color="primary"> | ||||
|             <CButton block disabled={device === null} onClick={toggleBlinkModal} color="primary"> | ||||
|               {t('actions.blink')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow className="my-1"> | ||||
|           <CCol> | ||||
|             <CButton block color="primary" onClick={toggleUpgradeModal}> | ||||
|             <CButton block disabled={device === null} color="primary" onClick={toggleUpgradeModal}> | ||||
|               {t('actions.firmware_upgrade')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <CButton block color="primary" onClick={toggleTraceModal}> | ||||
|             <CButton block disabled={device === null} color="primary" onClick={toggleTraceModal}> | ||||
|               {t('actions.trace')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow className="my-1"> | ||||
|           <CCol> | ||||
|             <CButton block color="primary" onClick={toggleScanModal}> | ||||
|             <CButton block disabled={device === null} color="primary" onClick={toggleScanModal}> | ||||
|               {t('actions.wifi_scan')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <CButton block color="primary" onClick={toggleFactoryResetModal}> | ||||
|             <CButton | ||||
|               block | ||||
|               disabled={device === null} | ||||
|               color="primary" | ||||
|               onClick={toggleFactoryResetModal} | ||||
|             > | ||||
|               {t('actions.factory_reset')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
| @@ -165,6 +133,7 @@ const DeviceActions = () => { | ||||
|         <CRow className="my-1"> | ||||
|           <CCol> | ||||
|             <LoadingButton | ||||
|               disabled={device === null} | ||||
|               isLoading={connectLoading} | ||||
|               label={t('actions.connect')} | ||||
|               isLoadingLabel={t('actions.connecting')} | ||||
| @@ -172,19 +141,24 @@ const DeviceActions = () => { | ||||
|             /> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <CButton block color="primary" onClick={toggleConfigModal}> | ||||
|             <CButton block disabled={device === null} color="primary" onClick={toggleConfigModal}> | ||||
|               {t('actions.configure')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow className="my-1"> | ||||
|           <CCol> | ||||
|             <CButton block color="primary" onClick={toggleQueueModal}> | ||||
|             <CButton block disabled={device === null} color="primary" onClick={toggleQueueModal}> | ||||
|               {t('commands.event_queue')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <CButton block color="primary" onClick={toggleTelemetryModal}> | ||||
|             <CButton | ||||
|               block | ||||
|               disabled={device === null} | ||||
|               color="primary" | ||||
|               onClick={toggleTelemetryModal} | ||||
|             > | ||||
|               {t('actions.telemetry')} | ||||
|             </CButton> | ||||
|           </CCol> | ||||
| @@ -212,4 +186,12 @@ const DeviceActions = () => { | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| DeviceActions.propTypes = { | ||||
|   device: PropTypes.instanceOf(Object), | ||||
| }; | ||||
|  | ||||
| DeviceActions.defaultProps = { | ||||
|   device: null, | ||||
| }; | ||||
|  | ||||
| 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 { 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 Dashboard from './Dashboard'; | ||||
|  | ||||
| const DeviceDashboard = () => { | ||||
|   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 */ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| 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 { useTranslation } from 'react-i18next'; | ||||
| import Modal from './Modal'; | ||||
|  | ||||
| const DeviceFirmwareModal = ({ | ||||
|   device, | ||||
| @@ -17,6 +18,7 @@ const DeviceFirmwareModal = ({ | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [firmwareVersions, setFirmwareVersions] = useState([]); | ||||
|   const [keepRedirector, toggleKeepRedirector, setKeepRedirector] = useToggle(true); | ||||
|  | ||||
|   const getPartialFirmware = async (offset) => { | ||||
|     const headers = { | ||||
| @@ -78,6 +80,7 @@ const DeviceFirmwareModal = ({ | ||||
|  | ||||
|     const parameters = { | ||||
|       serialNumber: device.serialNumber, | ||||
|       keepRedirector, | ||||
|       when: 0, | ||||
|       uri, | ||||
|     }; | ||||
| @@ -108,6 +111,7 @@ const DeviceFirmwareModal = ({ | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (show && device.compatible) getFirmwareList(); | ||||
|     if (show) setKeepRedirector(true); | ||||
|   }, [device, show]); | ||||
|  | ||||
|   return ( | ||||
| @@ -120,6 +124,8 @@ const DeviceFirmwareModal = ({ | ||||
|       upgradeToVersion={upgradeToVersion} | ||||
|       loading={loading} | ||||
|       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 DeviceFirmwareModal from 'components/DeviceFirmwareModal'; | ||||
| 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 apIcon from '../../assets/icons/AP.png'; | ||||
| import internetSwitch from '../../assets/icons/Switch.png'; | ||||
| @@ -377,7 +378,7 @@ const DeviceList = () => { | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       <DeviceListTable | ||||
|       <Table | ||||
|         currentPage={page} | ||||
|         t={t} | ||||
|         searchBar={<DeviceSearchBar />} | ||||
|   | ||||
| @@ -214,7 +214,7 @@ const DeviceLogs = () => { | ||||
|                             toggleDetails(index); | ||||
|                           }} | ||||
|                         > | ||||
|                           <CIcon name="cilList" size="md" /> | ||||
|                           <CIcon name="cilList" /> | ||||
|                         </CButton> | ||||
|                       </td> | ||||
|                     ), | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { useHistory } from 'react-router-dom'; | ||||
| import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs'; | ||||
| import { checkIfJson } from 'utils/helper'; | ||||
|  | ||||
| const DeviceSearchBar = () => { | ||||
| const DeviceSearchBar = ({ action }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const history = useHistory(); | ||||
|   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; | ||||
|   | ||||
| @@ -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 { cilPencil, cilSave, cilX } from '@coreui/icons'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import { | ||||
|   useFormFields, | ||||
|   useAuth, | ||||
|   useToast, | ||||
|   FirmwareDetailsForm, | ||||
|   DetailedNotesTable, | ||||
| } from 'ucentral-libs'; | ||||
| import { useFormFields, useAuth, useToast, DetailedNotesTable } from 'ucentral-libs'; | ||||
| import Form from './Form'; | ||||
|  | ||||
| const initialState = { | ||||
|   created: { | ||||
| @@ -237,12 +232,7 @@ const EditFirmwareModal = ({ show, toggle, firmwareId, refreshTable }) => { | ||||
|         <CTabContent> | ||||
|           <CTabPane active={index === 0} className="pt-2"> | ||||
|             {index === 0 ? ( | ||||
|               <FirmwareDetailsForm | ||||
|                 t={t} | ||||
|                 fields={firmware} | ||||
|                 updateFieldsWithId={updateWithId} | ||||
|                 editing={editing} | ||||
|               /> | ||||
|               <Form t={t} fields={firmware} updateFieldsWithId={updateWithId} editing={editing} /> | ||||
|             ) : null} | ||||
|           </CTabPane> | ||||
|           <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 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 axiosInstance from 'utils/axiosInstance'; | ||||
| import Modal from './Modal'; | ||||
|  | ||||
| const EventQueueModal = ({ show, toggle }) => { | ||||
|   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 { 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 Dashboard from './Dashboard'; | ||||
|  | ||||
| const FirmwareDashboard = () => { | ||||
|   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, | ||||
|   CModalTitle, | ||||
| } from '@coreui/react'; | ||||
| import { FirmwareHistoryTable, useAuth } from 'ucentral-libs'; | ||||
| import { useAuth } from 'ucentral-libs'; | ||||
| import Modal from './Modal'; | ||||
|  | ||||
| const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => { | ||||
|   const { t } = useTranslation(); | ||||
| @@ -51,7 +52,7 @@ const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => { | ||||
|         </CModalTitle> | ||||
|       </CModalHeader> | ||||
|       <CModalBody> | ||||
|         <FirmwareHistoryTable t={t} loading={loading} data={data} /> | ||||
|         <Modal t={t} loading={loading} data={data} /> | ||||
|       </CModalBody> | ||||
|       <CModalFooter> | ||||
|         <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 PropTypes from 'prop-types'; | ||||
| import { CRow, CCol } from '@coreui/react'; | ||||
| import { NetworkDiagram as Graph } from 'ucentral-libs'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import createLayoutedElements from './dagreAdapter'; | ||||
| import Graph from './Graph'; | ||||
|  | ||||
| const associationStyle = { | ||||
|   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 { useTranslation } from 'react-i18next'; | ||||
| import { useParams } from 'react-router-dom'; | ||||
| import { WifiAnalysisTable, RadioAnalysisTable, useAuth } from 'ucentral-libs'; | ||||
| import { useAuth } from 'ucentral-libs'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import NetworkDiagram from 'components/NetworkDiagram'; | ||||
| import { cleanBytesString, prettyDate, compactSecondsToDetailed } from 'utils/helper'; | ||||
| @@ -20,6 +20,8 @@ import { | ||||
| } from '@coreui/react'; | ||||
| import CIcon from '@coreui/icons-react'; | ||||
| import { cilX } from '@coreui/icons'; | ||||
| import RadioAnalysisTable from './RadioAnalysis'; | ||||
| import WifiAnalysisTable from './WifiAnalysis'; | ||||
|  | ||||
| const parseDbm = (value) => { | ||||
|   if (!value) return '-'; | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| 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'; | ||||
|  | ||||
| const TheLayout = () => { | ||||
| @@ -8,40 +11,46 @@ const TheLayout = () => { | ||||
|   const { endpoints, currentToken, user, avatar, logout } = useAuth(); | ||||
|   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 ( | ||||
|     <div className="c-app c-default-layout"> | ||||
|       <Sidebar | ||||
|         showSidebar={showSidebar} | ||||
|         setShowSidebar={setShowSidebar} | ||||
|         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" | ||||
|       /> | ||||
|       <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 DeviceList from 'components/DeviceListTable'; | ||||
| import DeviceDashboard from 'components/DeviceDashboard'; | ||||
| import BlacklistTable from 'components/BlacklistTable'; | ||||
| import { | ||||
|   CCard, | ||||
|   CCardBody, | ||||
| @@ -48,16 +49,21 @@ const DeviceListPage = () => { | ||||
|             active={index === 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> | ||||
|         </CNav> | ||||
|         <CTabContent> | ||||
|           <CTabPane active={index === 0}> | ||||
|             <DeviceDashboard /> | ||||
|           </CTabPane> | ||||
|           <CTabPane active={index === 1}> | ||||
|             <DeviceList /> | ||||
|           </CTabPane> | ||||
|           <CTabPane active={index === 0}>{index === 0 ? <DeviceDashboard /> : null}</CTabPane> | ||||
|           <CTabPane active={index === 1}>{index === 1 ? <DeviceList /> : null}</CTabPane> | ||||
|           <CTabPane active={index === 2}>{index === 2 ? <BlacklistTable /> : null}</CTabPane> | ||||
|         </CTabContent> | ||||
|       </CCardBody> | ||||
|     </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 DeviceActionCard from 'components/DeviceActionCard'; | ||||
| 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 ConfigurationDisplay from 'components/ConfigurationDisplay'; | ||||
| 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 { t } = useTranslation(); | ||||
| @@ -24,6 +28,11 @@ const DevicePage = () => { | ||||
|   const [error, setError] = useState(false); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|  | ||||
|   const updateNav = (target) => { | ||||
|     sessionStorage.setItem('devicePageIndex', target); | ||||
|     setIndex(target); | ||||
|   }; | ||||
|  | ||||
|   const getDevice = () => { | ||||
|     const options = { | ||||
|       headers: { | ||||
| @@ -48,13 +57,14 @@ const DevicePage = () => { | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         setDeviceConfig(deviceInfo); | ||||
|         setDeviceConfig({ ...deviceInfo }); | ||||
|         return null; | ||||
|       }) | ||||
|       .then((response) => { | ||||
|         if (response) setDeviceConfig({ ...deviceInfo, extendedInfo: response.data.extendedInfo }); | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         setDeviceConfig(null); | ||||
|         addToast({ | ||||
|           title: t('common.error'), | ||||
|           body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }), | ||||
| @@ -84,10 +94,13 @@ const DevicePage = () => { | ||||
|  | ||||
|     Promise.all([lastStatsRequest, statusRequest]) | ||||
|       .then(([newStats, newStatus]) => { | ||||
|         setLastStats(newStats.data); | ||||
|         setStatus(newStatus.data); | ||||
|         setLastStats({ ...newStats.data }); | ||||
|         setStatus({ ...newStatus.data }); | ||||
|         setError(false); | ||||
|       }) | ||||
|       .catch(() => { | ||||
|         setLastStats(null); | ||||
|         setStatus(null); | ||||
|         setError(true); | ||||
|       }) | ||||
|       .finally(() => { | ||||
| @@ -100,6 +113,12 @@ const DevicePage = () => { | ||||
|     getDevice(); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const target = sessionStorage.getItem('devicePageIndex'); | ||||
|  | ||||
|     if (target !== null) setIndex(parseInt(target, 10)); | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setError(false); | ||||
|     if (deviceId) { | ||||
| @@ -125,7 +144,7 @@ const DevicePage = () => { | ||||
|             /> | ||||
|           </CCol> | ||||
|           <CCol lg="12" xl="6"> | ||||
|             <DeviceActionCard /> | ||||
|             <DeviceActionCard device={deviceConfig} /> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
| @@ -137,7 +156,7 @@ const DevicePage = () => { | ||||
|                     className="font-weight-bold" | ||||
|                     href="#" | ||||
|                     active={index === 0} | ||||
|                     onClick={() => setIndex(0)} | ||||
|                     onClick={() => updateNav(0)} | ||||
|                   > | ||||
|                     {t('statistics.title')} | ||||
|                   </CNavLink> | ||||
| @@ -145,7 +164,7 @@ const DevicePage = () => { | ||||
|                     className="font-weight-bold" | ||||
|                     href="#" | ||||
|                     active={index === 1} | ||||
|                     onClick={() => setIndex(1)} | ||||
|                     onClick={() => updateNav(1)} | ||||
|                   > | ||||
|                     {t('common.details')} | ||||
|                   </CNavLink> | ||||
| @@ -153,15 +172,31 @@ const DevicePage = () => { | ||||
|                     className="font-weight-bold" | ||||
|                     href="#" | ||||
|                     active={index === 5} | ||||
|                     onClick={() => setIndex(5)} | ||||
|                     onClick={() => updateNav(5)} | ||||
|                   > | ||||
|                     {t('configuration.title')} | ||||
|                   </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 | ||||
|                     className="font-weight-bold" | ||||
|                     href="#" | ||||
|                     active={index === 6} | ||||
|                     onClick={() => setIndex(6)} | ||||
|                     onClick={() => updateNav(6)} | ||||
|                   > | ||||
|                     {t('wifi_analysis.title')} | ||||
|                   </CNavLink> | ||||
| @@ -169,7 +204,7 @@ const DevicePage = () => { | ||||
|                     className="font-weight-bold" | ||||
|                     href="#" | ||||
|                     active={index === 2} | ||||
|                     onClick={() => setIndex(2)} | ||||
|                     onClick={() => updateNav(2)} | ||||
|                   > | ||||
|                     {t('commands.title')} | ||||
|                   </CNavLink> | ||||
| @@ -177,7 +212,7 @@ const DevicePage = () => { | ||||
|                     className="font-weight-bold" | ||||
|                     href="#" | ||||
|                     active={index === 3} | ||||
|                     onClick={() => setIndex(3)} | ||||
|                     onClick={() => updateNav(3)} | ||||
|                   > | ||||
|                     {t('health.title')} | ||||
|                   </CNavLink> | ||||
| @@ -185,11 +220,12 @@ const DevicePage = () => { | ||||
|                     className="font-weight-bold" | ||||
|                     href="#" | ||||
|                     active={index === 4} | ||||
|                     onClick={() => setIndex(4)} | ||||
|                     onClick={() => updateNav(4)} | ||||
|                   > | ||||
|                     {t('device_logs.title')} | ||||
|                   </CNavLink> | ||||
|                 </CNav> | ||||
|                 {deviceConfig ? ( | ||||
|                   <CTabContent> | ||||
|                     <CTabPane active={index === 0}> | ||||
|                       {index === 0 ? <DeviceStatisticsCard /> : null} | ||||
| @@ -211,13 +247,26 @@ const DevicePage = () => { | ||||
|                         <ConfigurationDisplay deviceConfig={deviceConfig} getData={refresh} /> | ||||
|                       ) : null} | ||||
|                     </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}> | ||||
|                       {index === 2 ? <CommandHistory /> : null} | ||||
|                     </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> | ||||
|                   </CTabContent> | ||||
|                 ) : null} | ||||
|               </CCardBody> | ||||
|             </CCard> | ||||
|           </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, | ||||
|   CCardHeader, | ||||
| } from '@coreui/react'; | ||||
| import { FirmwareList, useAuth, useToast } from 'ucentral-libs'; | ||||
| import { useAuth, useToast } from 'ucentral-libs'; | ||||
| import FirmwareDashboard from 'components/FirmwareDashboard'; | ||||
| import EditFirmwareModal from 'components/EditFirmwareModal'; | ||||
| import Table from './Table'; | ||||
|  | ||||
| const FirmwareListPage = () => { | ||||
|   const { t } = useTranslation(); | ||||
| @@ -196,7 +197,7 @@ const FirmwareListPage = () => { | ||||
|             <FirmwareDashboard /> | ||||
|           </CTabPane> | ||||
|           <CTabPane active={index === 1}> | ||||
|             <FirmwareList | ||||
|             <Table | ||||
|               t={t} | ||||
|               loading={loading} | ||||
|               page={page} | ||||
|   | ||||
| @@ -1,559 +1,22 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import React from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import * as axios from 'axios'; | ||||
| import { LoginPage, useFormFields, 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, | ||||
| }; | ||||
| import { LoginPage, useAuth, useToast } from 'ucentral-libs'; | ||||
|  | ||||
| const Login = () => { | ||||
|   const { t, i18n } = useTranslation(); | ||||
|   const { setCurrentToken, setEndpoints } = useAuth(); | ||||
|   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 ( | ||||
|     <LoginPage | ||||
|       t={t} | ||||
|       i18n={i18n} | ||||
|       signIn={SignIn} | ||||
|       loading={loading} | ||||
|       setCurrentToken={setCurrentToken} | ||||
|       setEndpoints={setEndpoints} | ||||
|       addToast={addToast} | ||||
|       axios={axios} | ||||
|       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 { 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 { testRegex } from 'utils/helper'; | ||||
| 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, | ||||
|   }, | ||||
| }; | ||||
| import { ProfilePage as Page } from 'ucentral-libs'; | ||||
|  | ||||
| const ProfilePage = () => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { currentToken, endpoints, user, getAvatar, avatar } = useAuth(); | ||||
|   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> | ||||
|   ); | ||||
|   return <Page t={t} axiosInstance={axiosInstance} />; | ||||
| }; | ||||
|  | ||||
| export default ProfilePage; | ||||
|   | ||||
 bourquecharles
					bourquecharles