Version 2.5.18
597
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.4.1",
|
||||
"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.31",
|
||||
"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 = {
|
||||
@@ -48,7 +50,7 @@ const DeviceFirmwareModal = ({
|
||||
|
||||
const allFirmwares = [];
|
||||
let continueFirmware = true;
|
||||
let i = 1;
|
||||
let i = 0;
|
||||
while (continueFirmware) {
|
||||
const newFirmwares = await getPartialFirmware(i);
|
||||
if (newFirmwares === null || newFirmwares.length === 0) continueFirmware = false;
|
||||
@@ -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';
|
||||
@@ -68,7 +69,7 @@ const DeviceList = () => {
|
||||
axiosInstance
|
||||
.get(
|
||||
`${endpoints.owgw}/api/v1/devices?deviceWithStatus=true&limit=${devicePerPage}&offset=${
|
||||
devicePerPage * selectedPage + 1
|
||||
devicePerPage * selectedPage
|
||||
}`,
|
||||
options,
|
||||
)
|
||||
@@ -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,39 +220,53 @@ const DevicePage = () => {
|
||||
className="font-weight-bold"
|
||||
href="#"
|
||||
active={index === 4}
|
||||
onClick={() => setIndex(4)}
|
||||
onClick={() => updateNav(4)}
|
||||
>
|
||||
{t('device_logs.title')}
|
||||
</CNavLink>
|
||||
</CNav>
|
||||
<CTabContent>
|
||||
<CTabPane active={index === 0}>
|
||||
{index === 0 ? <DeviceStatisticsCard /> : null}
|
||||
</CTabPane>
|
||||
<CTabPane active={index === 1}>
|
||||
{index === 1 ? (
|
||||
<DeviceDetails
|
||||
t={t}
|
||||
loading={loading}
|
||||
getData={refresh}
|
||||
deviceConfig={deviceConfig}
|
||||
status={status}
|
||||
lastStats={lastStats}
|
||||
/>
|
||||
) : null}
|
||||
</CTabPane>
|
||||
<CTabPane active={index === 5}>
|
||||
{index === 5 ? (
|
||||
<ConfigurationDisplay deviceConfig={deviceConfig} getData={refresh} />
|
||||
) : null}
|
||||
</CTabPane>
|
||||
<CTabPane active={index === 6}>{index === 6 ? <WifiAnalysis /> : null}</CTabPane>
|
||||
<CTabPane active={index === 2}>
|
||||
{index === 2 ? <CommandHistory /> : null}
|
||||
</CTabPane>
|
||||
<CTabPane active={index === 3}>{index === 3 ? <DeviceHealth /> : null}</CTabPane>
|
||||
<CTabPane active={index === 4}>{index === 4 ? <DeviceLogs /> : null}</CTabPane>
|
||||
</CTabContent>
|
||||
{deviceConfig ? (
|
||||
<CTabContent>
|
||||
<CTabPane active={index === 0}>
|
||||
{index === 0 ? <DeviceStatisticsCard /> : null}
|
||||
</CTabPane>
|
||||
<CTabPane active={index === 1}>
|
||||
{index === 1 ? (
|
||||
<DeviceDetails
|
||||
t={t}
|
||||
loading={loading}
|
||||
getData={refresh}
|
||||
deviceConfig={deviceConfig}
|
||||
status={status}
|
||||
lastStats={lastStats}
|
||||
/>
|
||||
) : null}
|
||||
</CTabPane>
|
||||
<CTabPane active={index === 5}>
|
||||
{index === 5 ? (
|
||||
<ConfigurationDisplay deviceConfig={deviceConfig} getData={refresh} />
|
||||
) : 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 === 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();
|
||||
@@ -96,7 +97,7 @@ const FirmwareListPage = () => {
|
||||
|
||||
const allFirmwares = [];
|
||||
let continueFirmware = true;
|
||||
let i = 1;
|
||||
let i = 0;
|
||||
while (continueFirmware) {
|
||||
const newFirmwares = await getPartialFirmware(deviceType ?? selectedDeviceType, i);
|
||||
if (newFirmwares === null || newFirmwares.length === 0) continueFirmware = false;
|
||||
@@ -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;
|
||||
|
||||