Compare commits

..

1 Commits

Author SHA1 Message Date
TIP Automation User
7f0897d189 Chg: update image tag in helm values to v2.8.0-RC1 2022-12-13 23:08:47 +00:00
61 changed files with 351 additions and 2073 deletions

View File

@@ -8,7 +8,7 @@ fullnameOverride: ""
images:
owgwui:
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
tag: v2.9.0-RC1
tag: v2.8.0-RC1
pullPolicy: Always
services:

97
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.9.0(13)",
"version": "2.8.0(44)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.9.0(13)",
"version": "2.8.0(44)",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.11",
@@ -16,8 +16,6 @@
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fontsource/inter": "^4.5.14",
"@googlemaps/react-wrapper": "^1.1.35",
"@googlemaps/typescript-guards": "^2.0.3",
"@react-spring/web": "^9.5.5",
"@tanstack/react-query": "^4.12.0",
"@textea/json-viewer": "^2.10.0",
@@ -26,7 +24,6 @@
"chakra-react-select": "^4.3.0",
"chart.js": "^3.9.1",
"dagre": "^0.8.5",
"fast-equals": "^4.0.3",
"formik": "^2.2.9",
"framer-motion": "^7.6.1",
"i18next": "^22.0.0",
@@ -57,7 +54,6 @@
"zustand": "^4.1.2"
},
"devDependencies": {
"@types/google.maps": "^3.51.0",
"@types/node": "^18.11.2",
"@types/react": "^18.0.21",
"@types/react-csv": "^1.1.3",
@@ -2852,30 +2848,6 @@
"version": "4.5.14",
"license": "MIT"
},
"node_modules/@googlemaps/js-api-loader": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.15.1.tgz",
"integrity": "sha512-AsnEgNsB7S/VdrHGEQUaUM2e5tmjFGKBAfzR/AqO8O7TPq/jQGvoRw5liPBw4EMF38RDsHmKDV89q/X+qiUREQ==",
"dependencies": {
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/@googlemaps/react-wrapper": {
"version": "1.1.35",
"resolved": "https://registry.npmjs.org/@googlemaps/react-wrapper/-/react-wrapper-1.1.35.tgz",
"integrity": "sha512-vK+BDQMHN0Oqr66cW3ZPWVK43BUmJJBu6P8T74tc6/fKpUJUlFEaZsupgIIRRRDW9ejB8uGagUmwOnA2gdcvbw==",
"dependencies": {
"@googlemaps/js-api-loader": "^1.13.2"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@googlemaps/typescript-guards": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@googlemaps/typescript-guards/-/typescript-guards-2.0.3.tgz",
"integrity": "sha512-3iHuO8H0jPehftsMK0kgyJzPYU/g/oiTRw+wu/yltqSZ7wJPt3vfsJHkPiuRpQjbnnWygX+T3mkRGyK/eyZ/lw=="
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.10.7",
"dev": true,
@@ -3529,12 +3501,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/google.maps": {
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.51.0.tgz",
"integrity": "sha512-44/oQYjc5D6kxBcI3Qk9rk3IIOMwnlEMWDV7pwPJ2YI89s5Q1OzDrFvR7QJ3LFrpVXEhig+gyagFg54+foinFg==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.11",
"dev": true,
@@ -5563,6 +5529,7 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
@@ -5570,11 +5537,6 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-equals": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
},
"node_modules/fast-glob": {
"version": "3.2.12",
"dev": true,
@@ -6680,9 +6642,8 @@
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"version": "2.2.1",
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
@@ -8892,10 +8853,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"version": "1.0.1",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.0"
},
@@ -11465,27 +11425,6 @@
"@fontsource/inter": {
"version": "4.5.14"
},
"@googlemaps/js-api-loader": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.15.1.tgz",
"integrity": "sha512-AsnEgNsB7S/VdrHGEQUaUM2e5tmjFGKBAfzR/AqO8O7TPq/jQGvoRw5liPBw4EMF38RDsHmKDV89q/X+qiUREQ==",
"requires": {
"fast-deep-equal": "^3.1.3"
}
},
"@googlemaps/react-wrapper": {
"version": "1.1.35",
"resolved": "https://registry.npmjs.org/@googlemaps/react-wrapper/-/react-wrapper-1.1.35.tgz",
"integrity": "sha512-vK+BDQMHN0Oqr66cW3ZPWVK43BUmJJBu6P8T74tc6/fKpUJUlFEaZsupgIIRRRDW9ejB8uGagUmwOnA2gdcvbw==",
"requires": {
"@googlemaps/js-api-loader": "^1.13.2"
}
},
"@googlemaps/typescript-guards": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@googlemaps/typescript-guards/-/typescript-guards-2.0.3.tgz",
"integrity": "sha512-3iHuO8H0jPehftsMK0kgyJzPYU/g/oiTRw+wu/yltqSZ7wJPt3vfsJHkPiuRpQjbnnWygX+T3mkRGyK/eyZ/lw=="
},
"@humanwhocodes/config-array": {
"version": "0.10.7",
"dev": true,
@@ -11847,12 +11786,6 @@
"version": "0.0.39",
"dev": true
},
"@types/google.maps": {
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.51.0.tgz",
"integrity": "sha512-44/oQYjc5D6kxBcI3Qk9rk3IIOMwnlEMWDV7pwPJ2YI89s5Q1OzDrFvR7QJ3LFrpVXEhig+gyagFg54+foinFg==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.11",
"dev": true
@@ -13092,17 +13025,13 @@
}
},
"fast-deep-equal": {
"version": "3.1.3"
"version": "3.1.3",
"dev": true
},
"fast-diff": {
"version": "1.2.0",
"dev": true
},
"fast-equals": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
},
"fast-glob": {
"version": "3.2.12",
"dev": true,
@@ -13759,9 +13688,7 @@
"dev": true
},
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
"version": "2.2.1"
},
"jsonfile": {
"version": "6.1.0",
@@ -15054,9 +14981,7 @@
},
"dependencies": {
"json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"version": "1.0.1",
"dev": true,
"requires": {
"minimist": "^1.2.0"

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.9.0(13)",
"version": "2.8.0(44)",
"description": "",
"private": true,
"main": "index.tsx",
@@ -22,15 +22,12 @@
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fontsource/inter": "^4.5.14",
"@googlemaps/react-wrapper": "^1.1.35",
"@googlemaps/typescript-guards": "^2.0.3",
"@react-spring/web": "^9.5.5",
"axios": "^1.1.3",
"buffer": "^6.0.3",
"chakra-react-select": "^4.3.0",
"dagre": "^0.8.5",
"formik": "^2.2.9",
"fast-equals": "^4.0.3",
"framer-motion": "^7.6.1",
"i18next": "^22.0.0",
"i18next-browser-languagedetector": "^6.1.8",
@@ -63,7 +60,6 @@
"zustand": "^4.1.2"
},
"devDependencies": {
"@types/google.maps": "^3.51.0",
"@types/node": "^18.11.2",
"@types/react": "^18.0.21",
"@types/react-csv": "^1.1.3",

View File

@@ -79,11 +79,8 @@
"live_view_help": "Hilfe zur Live-Ansicht",
"memory": "Erinnerung",
"memory_used": "Verwendeter Speicher",
"missing_board": "Analytics-Überwachung an diesem Ort ist nicht mehr aktiv. Klicken Sie hier, um die Überwachung neu zu starten",
"missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu",
"mode": "Modus",
"monitoring": "Überwachung",
"no_board": "Keine Überwachung",
"no_board_description": "Sie überwachen diesen Veranstaltungsort derzeit nicht, klicken Sie hier, um zu beginnen",
"noise": "Lärm",
"packets": "Pakete",
"radio": "RADIO",
@@ -94,8 +91,6 @@
"retries": "Wiederholungen",
"search_serials": "Zeitschriften suchen",
"stop_monitoring": "Beenden Sie die Überwachung",
"stop_monitoring_success": "Überwachungsort gestoppt!",
"stop_monitoring_warning": "Bist du sicher? Dadurch werden alle aufgezeichneten Überwachungsdaten für diesen Veranstaltungsort gelöscht",
"temperature": "Temperatur",
"title": "ANALYTICS",
"total_data": "Gesamtdaten",
@@ -180,7 +175,6 @@
"other": "Befehle",
"override_dfs": "DFS überschreiben",
"reboot": "Starten Sie neu",
"reboot_description": "Möchten Sie dieses Gerät neu starten?",
"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}",
"reboot_success": "Neustartbefehl erfolgreich gesendet!",
"revision": "Revision",
@@ -397,7 +391,6 @@
"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}",
"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}",
"weight": "Gewicht",
"wifi_bands_max": "Es können nicht mehr als 8 SSIDs dieses WLAN-Band verwenden",
"wifi_frames": "WiFi-Frames"
},
"contacts": {
@@ -607,7 +600,6 @@
"certificate_expires_in": "Zertifikat läuft ab in",
"certificate_expiry": "Zert. Läuft ab in",
"connected": "In Verbindung gebracht",
"crash_logs": "Absturzprotokolle",
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
"create_success": " Geräte erfolgreich erstellt",
"current_firmware": "Aktuelle Firmware",
@@ -621,7 +613,6 @@
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
"logs_one": "Log",
"new_devices": "Neue Geräte",
"no_model_image": "Kein Modellbild gefunden",
"not_connected": "Nicht verbunden",
@@ -630,8 +621,6 @@
"one": "Gerät",
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
"restricted": "Beschränkt",
"restricted_overriden": "Dies ist ein eingeschränktes Gerät, aber es befindet sich im Entwicklungsmodus. Alle Einschränkungen werden derzeit ignoriert",
"restrictions_overriden_title": "Dev-Modus",
"sanity": "Gesundheit",
"start_import": "Geräteimport starten",
"test_batch": "Testen Sie Importdaten",
@@ -682,15 +671,7 @@
"test_digicert_creds": "Anmeldeinformationen testen",
"title": "Entitäten",
"tree": "Entitätsbaum",
"update_success": "Entität aktualisiert!",
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
},
"firmware": {
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
"last_db_update_modal": "Firmware-Datenbank",
"last_db_update_title": "Datenbank",
"start_db_update": "Datenbankaktualisierung starten",
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden. Bitte erstellen Sie neue Entitäten und erstellen Sie Veranstaltungsorte unter diesen."
},
"footer": {
"powered_by": "Unterstützt von",
@@ -784,17 +765,13 @@
"city": "Stadt",
"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden",
"country": "Land",
"elevation": "Elevation",
"geocode": "Geo-Code",
"lat": "Breite",
"longitude": "Längengrad",
"one": "Ort",
"other": "Standorte",
"postal": "Postleitzahl",
"state": "Bundesstaat / Provinz",
"title": "Standorte",
"to_claim": "Standorte zu beanspruchen",
"view_gps": ""
"to_claim": "Standorte zu beanspruchen"
},
"login": {
"access_policy": "Zugangsrichtlinien",
@@ -820,7 +797,6 @@
"reset_password": "Passwort zurücksetzen",
"sign_in": "Einloggen",
"sms_instructions": "Sie sollten bald einen 6-stelligen Code auf Ihrem Telefon erhalten. Bitte geben Sie es unten ein, um sich anzumelden",
"suspended_error": "Konto gesperrt, wenden Sie sich bitte an Ihren Administrator",
"verification": "Bestätigen Sie Ihre Anmeldung",
"waiting_for_email_verification": "Konto noch nicht per E-Mail validiert. Bitte sehen Sie in Ihrem Posteingang nach oder bitten Sie Ihren Administrator, eine Bestätigung erneut zu senden",
"welcome_back": "Willkommen zurück!",
@@ -926,7 +902,7 @@
"dfs": "DFS-Überschreibung",
"gw_commands": "Gateway-Befehle",
"identifier": "Identifikator",
"key_verification": "Signieren von Schlüsselinformationen",
"key_verification": "Überprüfung des Signaturschlüssels",
"restricted": "Beschränkt",
"signed_upgrade": "Nur signiertes Upgrade",
"title": "Beschränkungen",
@@ -1046,7 +1022,6 @@
},
"system": {
"backend_logs": "Back-End-Protokolle",
"configuration": "Aufbau",
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
"endpoint": "Endpunkt",
"hostname": "Hostname",
@@ -1057,10 +1032,6 @@
"os": "Betriebssystem",
"processors": "Prozessoren",
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
"secrets": "Geheimnisse",
"secrets_create": "Geheimnis erstellen",
"secrets_one": "Geheimnis",
"services": "dienstleistungen",
"start": "Start",
"subsystems": "Subsysteme",
"success_reload": "Reload-Befehl erfolgreich gesendet!",
@@ -1082,11 +1053,9 @@
"previous_page": "Vorherige Seite"
},
"user": {
"email_not_validated": "E-Mail nicht validiert",
"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}",
"password": "Passwort",
"role": "Rolle",
"suspended": "Suspendiert",
"title": "Nutzer"
},
"users": {
@@ -1131,11 +1100,9 @@
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
"title": "Veranstaltungsorte",
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
"update_success": "Veranstaltungsort aktualisiert!",
"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte",
"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware",
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",
"upgrade_options_available": "Hier sind alle verfügbaren Revisionen, bitte wählen Sie diejenige aus, auf die ALLE Geräte dieses Veranstaltungsortes aktualisiert werden sollen",
"use_existing": "Benutze existierendes",
"use_existing_contacts": "Verwenden Sie vorhandene Kontakte",
"use_this_contact": "Verwenden Sie diesen Kontakt"

View File

@@ -79,11 +79,8 @@
"live_view_help": "Live View Help",
"memory": "Memory",
"memory_used": "Memory Used",
"missing_board": "Analytics monitoring on this venue is no longer active. Click here to restart monitoring",
"missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu",
"mode": "Mode",
"monitoring": "Monitoring",
"no_board": "No Monitoring",
"no_board_description": "You are not monitoring this Venue at the moment, click here to start",
"noise": "Noise",
"packets": "Packets",
"radio": "Radio",
@@ -94,8 +91,6 @@
"retries": "Retries",
"search_serials": "Search Serials",
"stop_monitoring": "Stop Monitoring",
"stop_monitoring_success": "Stopped Monitoring Venue!",
"stop_monitoring_warning": "Are you sure? This will erase all recorded monitoring data for this venue",
"temperature": "Temperature",
"title": "Analytics",
"total_data": "Total Data",
@@ -180,7 +175,6 @@
"other": "Commands",
"override_dfs": "Override DFS",
"reboot": "Reboot",
"reboot_description": "Do you want to reboot this device?",
"reboot_error": "Error while sending reboot command: {{e}}",
"reboot_success": "Successfully sent reboot command!",
"revision": "Revision",
@@ -397,7 +391,6 @@
"warning_pushes_one": "Waiting for devices to connect: {{count}}",
"warning_pushes_other": "Waiting for devices to connect: {{count}}",
"weight": "Weight",
"wifi_bands_max": "There cannot be more than 8 SSIDs using this wifi-band",
"wifi_frames": "WiFi Frames"
},
"contacts": {
@@ -607,7 +600,6 @@
"certificate_expires_in": "Certificate Expiry",
"certificate_expiry": "Cert. Expires In",
"connected": "Connected",
"crash_logs": "Crash Logs",
"create_errors": "errors while trying to create devices",
"create_success": " devices successfully created",
"current_firmware": "Current Firmware",
@@ -621,7 +613,6 @@
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
"logs_one": "Log",
"new_devices": "new devices",
"no_model_image": "No Model Image Found",
"not_connected": "Not Connected",
@@ -630,8 +621,6 @@
"one": "Device",
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
"restricted": "Restricted",
"restricted_overriden": "This is a restricted device, but it is in development mode. All restrictions are currently ignored",
"restrictions_overriden_title": "Dev Mode",
"sanity": "Sanity",
"start_import": "Start Device Importation",
"test_batch": "Test Import Data",
@@ -682,15 +671,7 @@
"test_digicert_creds": "Test Credentials",
"title": "Entities",
"tree": "Entity Tree",
"update_success": "Entity updated!",
"venues_under_root": "Venues cannot be created directly under the root entity"
},
"firmware": {
"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes",
"last_db_update_modal": "Firmware Database",
"last_db_update_title": "Database",
"start_db_update": "Start Database Update",
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete"
"venues_under_root": "Venues cannot be created directly under the root entity. Please create new entities and create venues under these."
},
"footer": {
"powered_by": "Powered By",
@@ -784,17 +765,13 @@
"city": "City",
"claim_explanation": "To claim locations you can use the table below",
"country": "Country",
"elevation": "Elevation",
"geocode": "Geo Code",
"lat": "Latitude",
"longitude": "Longitude",
"one": "Location",
"other": "Locations",
"postal": "ZIP/Postal Code",
"state": "State/Province",
"title": "Locations",
"to_claim": "Locations to claim",
"view_gps": "View GPS Location"
"to_claim": "Locations to claim"
},
"login": {
"access_policy": "Access Policy",
@@ -820,7 +797,6 @@
"reset_password": "Reset Password",
"sign_in": "Sign In",
"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login",
"suspended_error": "Suspended account, please contact your administrator",
"verification": "Verify your login",
"waiting_for_email_verification": "Account not yet email validated. Please look at your inbox or ask your administrator to resend a validation",
"welcome_back": "Welcome Back!",
@@ -926,7 +902,7 @@
"dfs": "DFS Override",
"gw_commands": "Gateway Commands",
"identifier": "Identifier",
"key_verification": "Signing Key Information",
"key_verification": "Signing Key Verification",
"restricted": "Restricted",
"signed_upgrade": "Signed Upgrade Only",
"title": "Restrictions",
@@ -1046,7 +1022,6 @@
},
"system": {
"backend_logs": "Back-End Logs",
"configuration": "Configuration",
"could_not_retrieve": "Error: could not retrieve {{name}} system information",
"endpoint": "Endpoint",
"hostname": "Host Name",
@@ -1057,10 +1032,6 @@
"os": "Operating System",
"processors": "Processors",
"reload_chosen_subsystems": "Reload Chosen Subsystems",
"secrets": "Secrets",
"secrets_create": "Create Secret",
"secrets_one": "Secret",
"services": "Services",
"start": "Start",
"subsystems": "Subsystems",
"success_reload": "Successfully sent reload command!",
@@ -1082,11 +1053,9 @@
"previous_page": "Previous Page"
},
"user": {
"email_not_validated": "email not validated",
"error_fetching": "Error fetching user information: {{e}}",
"password": "Password",
"role": "Role",
"suspended": "suspended",
"title": "User"
},
"users": {
@@ -1131,11 +1100,9 @@
"successfully_update_devices": "Updating {{num}} devices!",
"title": "Venues",
"update_all_devices": "Update All Device Configurations",
"update_success": "Venue updated!",
"upgrade_all_devices": "Upgrade All Devices Firmware",
"upgrade_all_devices": "Upgrade All Devices to Latest Firmware",
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
"upgrade_all_devices_success": "Successfully started upgrading devices!",
"upgrade_options_available": "Here are all available revisions, please select the one you want ALL of this venue's devices to be upgrade to",
"use_existing": "Use Existing",
"use_existing_contacts": "Use Existing Contacts",
"use_this_contact": "Use this contact"

View File

@@ -79,11 +79,8 @@
"live_view_help": "Ayuda de visualización en vivo",
"memory": "Memoria",
"memory_used": "Memoria usada",
"missing_board": "El monitoreo analítico en este lugar ya no está activo. Haga clic aquí para reiniciar el monitoreo",
"missing_board": "El monitoreo analítico en este lugar ya no está activo, reinicie el monitoreo usando el menú superior",
"mode": "Modo",
"monitoring": "Vigilancia",
"no_board": "Sin monitoreo",
"no_board_description": "No está monitoreando este lugar en este momento, haga clic aquí para comenzar",
"noise": "Ruido",
"packets": "Paquetes",
"radio": "RADIO",
@@ -94,8 +91,6 @@
"retries": "Reintentos",
"search_serials": "Buscar seriales",
"stop_monitoring": "Dejar de monitorear",
"stop_monitoring_success": "¡Se detuvo el lugar de monitoreo!",
"stop_monitoring_warning": "¿Está seguro? Esto borrará todos los datos de monitoreo grabados para este lugar.",
"temperature": "temperatura",
"title": "ANALÍTICA",
"total_data": "Datos totales",
@@ -180,7 +175,6 @@
"other": "comandos",
"override_dfs": "Anular DFS",
"reboot": "Reiniciar",
"reboot_description": "¿Quieres reiniciar este dispositivo?",
"reboot_error": "Error al enviar el comando de reinicio: {{e}}",
"reboot_success": "¡Comando de reinicio enviado con éxito!",
"revision": "revisión",
@@ -397,7 +391,6 @@
"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}",
"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}",
"weight": "Peso",
"wifi_bands_max": "No puede haber más de 8 SSID usando esta banda wifi",
"wifi_frames": "Marcos WiFi"
},
"contacts": {
@@ -607,7 +600,6 @@
"certificate_expires_in": "El certificado caduca en",
"certificate_expiry": "Cert. Expira en",
"connected": "Conectado",
"crash_logs": "Registros de fallas",
"create_errors": "errores al intentar crear dispositivos",
"create_success": " dispositivos creados con éxito",
"current_firmware": "Firmware actual",
@@ -621,7 +613,6 @@
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
"logs_one": "Iniciar sesión",
"new_devices": "Nuevos dispositivos",
"no_model_image": "No se encontró ninguna imagen de modelo",
"not_connected": "No conectado",
@@ -630,8 +621,6 @@
"one": "Dispositivo",
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
"restricted": "Restringido",
"restricted_overriden": "Este es un dispositivo restringido, pero está en modo de desarrollo. Actualmente se ignoran todas las restricciones.",
"restrictions_overriden_title": "MODO DE DESARROLLO",
"sanity": "Cordura",
"start_import": "Iniciar la importación de dispositivos",
"test_batch": "Datos de importación de prueba",
@@ -682,15 +671,7 @@
"test_digicert_creds": "Credenciales de prueba",
"title": "entidades",
"tree": "Árbol de entidades",
"update_success": "¡Entidad actualizada!",
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
},
"firmware": {
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
"last_db_update_modal": "Base de datos de firmware",
"last_db_update_title": "Base de datos",
"start_db_update": "Iniciar actualización de la base de datos",
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz. Cree nuevas entidades y cree lugares bajo estas."
},
"footer": {
"powered_by": "energizado por",
@@ -784,17 +765,13 @@
"city": "ciudad",
"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación",
"country": "País",
"elevation": "Elevación",
"geocode": "Código geográfico",
"lat": "Latitud",
"longitude": "Longitud",
"one": "Ubicación",
"other": "Ubicaciones",
"postal": "código postal",
"state": "Provincia del estado",
"title": "Ubicaciones",
"to_claim": "Ubicaciones para reclamar",
"view_gps": ""
"to_claim": "Ubicaciones para reclamar"
},
"login": {
"access_policy": "Política de acceso",
@@ -820,7 +797,6 @@
"reset_password": "Restablecer la contraseña",
"sign_in": "Registrarse",
"sms_instructions": "Debería recibir un código de 6 dígitos en su teléfono pronto. Por favor, introdúzcalo a continuación para iniciar sesión",
"suspended_error": "Cuenta suspendida, comuníquese con su administrador",
"verification": "Verifica tu inicio de sesión",
"waiting_for_email_verification": "Cuenta aún no validada por correo electrónico. Mire su bandeja de entrada o solicite a su administrador que vuelva a enviar una validación.",
"welcome_back": "¡Dar una buena acogida!",
@@ -926,7 +902,7 @@
"dfs": "Anulación de DFS",
"gw_commands": "Comandos de puerta de enlace",
"identifier": "Identificador",
"key_verification": "Información clave de firma",
"key_verification": "Verificación de clave de firma",
"restricted": "Restringido",
"signed_upgrade": "Solo actualización firmada",
"title": "Las restricciones",
@@ -1046,7 +1022,6 @@
},
"system": {
"backend_logs": "Registros de back-end",
"configuration": "Configuración",
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
"endpoint": "punto final",
"hostname": "Nombre de host",
@@ -1057,10 +1032,6 @@
"os": "sistema operativo",
"processors": "Procesadores",
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
"secrets": "Misterios",
"secrets_create": "Crear secreto",
"secrets_one": "secreto",
"services": "Servicios",
"start": "comienzo",
"subsystems": "Subsistemas",
"success_reload": "¡Comando de recarga enviado con éxito!",
@@ -1082,11 +1053,9 @@
"previous_page": "Página anterior"
},
"user": {
"email_not_validated": "correo electrónico no validado",
"error_fetching": "Error al obtener la información del usuario: {{e}}",
"password": "Contraseña",
"role": "papel",
"suspended": "Suspendido",
"title": "Usuario"
},
"users": {
@@ -1131,11 +1100,9 @@
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
"title": "Sedes",
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
"update_success": "Lugar actualizado!",
"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos",
"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente",
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",
"upgrade_options_available": "Aquí están todas las revisiones disponibles, seleccione la que desea que TODOS los dispositivos de este lugar se actualicen",
"use_existing": "Utilizar existente",
"use_existing_contacts": "Usar contactos existentes",
"use_this_contact": "Usa este contacto"

View File

@@ -79,11 +79,8 @@
"live_view_help": "Aide sur l'affichage en direct",
"memory": "mémoire",
"memory_used": "Mémoire utilisée",
"missing_board": "La surveillance analytique sur ce site n'est plus active. Cliquez ici pour redémarrer la surveillance",
"missing_board": "La surveillance analytique sur ce lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut",
"mode": "Mode",
"monitoring": "surveillance",
"no_board": "Aucune surveillance",
"no_board_description": "Vous ne surveillez pas ce lieu pour le moment, cliquez ici pour commencer",
"noise": "Bruit",
"packets": "Paquets",
"radio": "Radio",
@@ -94,8 +91,6 @@
"retries": "Tentatives",
"search_serials": "Rechercher des publications en série",
"stop_monitoring": "Arrêter la surveillance",
"stop_monitoring_success": "Lieu de surveillance arrêté !",
"stop_monitoring_warning": "Êtes-vous sûr? Cela effacera toutes les données de surveillance enregistrées pour ce lieu",
"temperature": "Température",
"title": "ANALYTIQUE",
"total_data": "Données totales",
@@ -180,7 +175,6 @@
"other": "Les commandes",
"override_dfs": "Remplacer DFS",
"reboot": "Redémarrer",
"reboot_description": "Voulez-vous redémarrer cet appareil ?",
"reboot_error": "Erreur lors de l'envoi de la commande de redémarrage : {{e}}",
"reboot_success": "Commande de redémarrage envoyée avec succès !",
"revision": "Révision",
@@ -397,7 +391,6 @@
"warning_pushes_one": "En attente de connexion des appareils : {{count}}",
"warning_pushes_other": "En attente de connexion des appareils : {{count}}",
"weight": "Poids",
"wifi_bands_max": "Il ne peut y avoir plus de 8 SSID utilisant cette bande wifi",
"wifi_frames": "Cadres Wi-Fi"
},
"contacts": {
@@ -607,7 +600,6 @@
"certificate_expires_in": "Le certificat expire dans",
"certificate_expiry": "Cert. Expire dans",
"connected": "Connecté",
"crash_logs": "Journaux des plantages",
"create_errors": "erreurs lors de la tentative de création d'appareils",
"create_success": " appareils créés avec succès",
"current_firmware": "Firmware actuel",
@@ -621,7 +613,6 @@
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
"logs_one": "Bûche",
"new_devices": "nouveaux appareils",
"no_model_image": "Aucune image de modèle trouvée",
"not_connected": "Pas connecté",
@@ -630,8 +621,6 @@
"one": "Dispositif",
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
"restricted": "Limité",
"restricted_overriden": "Il s'agit d'un appareil restreint, mais il est en mode développement. Toutes les restrictions sont actuellement ignorées",
"restrictions_overriden_title": "Mode développement",
"sanity": "Santé mentale",
"start_import": "Démarrer l'importation de l'appareil",
"test_batch": "Tester les données d'importation",
@@ -682,15 +671,7 @@
"test_digicert_creds": "Tester les informations d'identification",
"title": "Entités",
"tree": "Arborescence des entités",
"update_success": "Entité mise à jour !",
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
},
"firmware": {
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
"last_db_update_modal": "Base de données du micrologiciel",
"last_db_update_title": "Base de données",
"start_db_update": "Démarrer la mise à jour de la base de données",
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
"venues_under_root": "Les sites ne peuvent pas être créés directement sous l'entité racine. Veuillez créer de nouvelles entités et créer des lieux sous celles-ci."
},
"footer": {
"powered_by": "Alimenté par",
@@ -784,17 +765,13 @@
"city": "Ville",
"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous",
"country": "Pays",
"elevation": "Élévation",
"geocode": "Geo code",
"lat": "Latitude",
"longitude": "Longitude",
"one": "Emplacement",
"other": "Emplacements",
"postal": "Zip / code postal",
"state": "Etat / Province",
"title": "Emplacements",
"to_claim": "Emplacements à réclamer",
"view_gps": ""
"to_claim": "Emplacements à réclamer"
},
"login": {
"access_policy": "Politique d'accès",
@@ -820,7 +797,6 @@
"reset_password": "Réinitialiser le mot de passe",
"sign_in": "se connecter",
"sms_instructions": "Vous devriez bientôt recevoir un code à 6 chiffres sur votre téléphone. Veuillez le saisir ci-dessous pour vous connecter",
"suspended_error": "Compte suspendu, veuillez contacter votre administrateur",
"verification": "Vérifiez votre connexion",
"waiting_for_email_verification": "Compte pas encore e-mail validé. Veuillez consulter votre boîte de réception ou demander à votre administrateur de renvoyer une validation",
"welcome_back": "Nous saluons le retour!",
@@ -926,7 +902,7 @@
"dfs": "Remplacement DFS",
"gw_commands": "Commandes de passerelle",
"identifier": "Identifiant",
"key_verification": "Signature des informations clés",
"key_verification": "Vérification de la clé de signature",
"restricted": "Limité",
"signed_upgrade": "Mise à niveau signée uniquement",
"title": "Restrictions",
@@ -1046,7 +1022,6 @@
},
"system": {
"backend_logs": "Journaux principaux",
"configuration": "Configuration",
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
"endpoint": "Point final",
"hostname": "nom d'hôte",
@@ -1057,10 +1032,6 @@
"os": "Système opérateur",
"processors": "Processeurs",
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
"secrets": "Secrets",
"secrets_create": "Créer un secret",
"secrets_one": "Secret",
"services": "Prestations de service",
"start": "Début",
"subsystems": "Sous-systèmes",
"success_reload": "Commande de rechargement envoyée avec succès !",
@@ -1082,11 +1053,9 @@
"previous_page": "Page précédente"
},
"user": {
"email_not_validated": "Mail non valide",
"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}",
"password": "Mot de passe",
"role": "Rôle",
"suspended": "Suspendu",
"title": "Utilisateur"
},
"users": {
@@ -1131,11 +1100,9 @@
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
"title": "Les lieux",
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
"update_success": "Lieu mis à jour !",
"upgrade_all_devices": "Mettre à niveau le micrologiciel de tous les appareils",
"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel",
"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}",
"upgrade_all_devices_success": "La mise à niveau des appareils a démarré avec succès !",
"upgrade_options_available": "Voici toutes les révisions disponibles, veuillez sélectionner celle vers laquelle vous souhaitez que TOUS les appareils de ce lieu soient mis à niveau",
"use_existing": "Utiliser l'existant",
"use_existing_contacts": "Utiliser les contacts existants",
"use_this_contact": "Utilisez ce contact"

View File

@@ -79,11 +79,8 @@
"live_view_help": "Ajuda da visualização ao vivo",
"memory": "Memória",
"memory_used": "Memória Usada",
"missing_board": "O monitoramento analítico neste local não está mais ativo. Clique aqui para reiniciar o monitoramento",
"missing_board": "O monitoramento analítico neste local não está mais ativo, reinicie o monitoramento usando o menu superior",
"mode": "Modo",
"monitoring": "Monitoramento",
"no_board": "Sem monitoramento",
"no_board_description": "Você não está monitorando este local no momento, clique aqui para começar",
"noise": "Barulho",
"packets": "Pacotes",
"radio": "Rádio",
@@ -94,8 +91,6 @@
"retries": "Novas tentativas",
"search_serials": "Pesquisar séries",
"stop_monitoring": "Parar o monitoramento",
"stop_monitoring_success": "Local de monitoramento interrompido!",
"stop_monitoring_warning": "Tem certeza? Isso apagará todos os dados de monitoramento gravados para este local",
"temperature": "Temperatura",
"title": "Analytics",
"total_data": "Dados totais",
@@ -180,7 +175,6 @@
"other": "comandos",
"override_dfs": "Substituir DFS",
"reboot": "Reiniciar",
"reboot_description": "Deseja reiniciar este dispositivo?",
"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}",
"reboot_success": "Comando de reinicialização enviado com sucesso!",
"revision": "revisão",
@@ -397,7 +391,6 @@
"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}",
"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}",
"weight": "Peso",
"wifi_bands_max": "Não pode haver mais de 8 SSIDs usando esta banda wi-fi",
"wifi_frames": "Quadros WiFi"
},
"contacts": {
@@ -607,7 +600,6 @@
"certificate_expires_in": "Certificado expira em",
"certificate_expiry": "Certificado expira em",
"connected": "Conectado",
"crash_logs": "Registros de falhas",
"create_errors": "erros ao tentar criar dispositivos",
"create_success": " dispositivos criados com sucesso",
"current_firmware": "Firmware atual",
@@ -621,7 +613,6 @@
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
"logs_one": "Registro",
"new_devices": "novos dispositivos",
"no_model_image": "Nenhuma imagem de modelo encontrada",
"not_connected": "Não conectado",
@@ -630,8 +621,6 @@
"one": "Dispositivo",
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
"restricted": "Restrito",
"restricted_overriden": "Este é um dispositivo restrito, mas está em modo de desenvolvimento. Todas as restrições são atualmente ignoradas",
"restrictions_overriden_title": "Modo de desenvolvedor",
"sanity": "Sanidade",
"start_import": "Iniciar importação de dispositivos",
"test_batch": "Dados de importação de teste",
@@ -682,15 +671,7 @@
"test_digicert_creds": "Credenciais de teste",
"title": "Entidades",
"tree": "Árvore de entidades",
"update_success": "Entidade atualizada!",
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
},
"firmware": {
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
"last_db_update_modal": "banco de dados de firmware",
"last_db_update_title": "base de dados",
"start_db_update": "Iniciar atualização do banco de dados",
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz. Por favor, crie novas entidades e crie locais sob elas."
},
"footer": {
"powered_by": "Distribuído por",
@@ -784,17 +765,13 @@
"city": "Cidade",
"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo",
"country": "País",
"elevation": "elevação",
"geocode": "Código geográfico",
"lat": "Latitude",
"longitude": "Longitude",
"one": "Localização",
"other": "Localizações",
"postal": "CEP / Código Postal",
"state": "Estado / Província",
"title": "Localizações",
"to_claim": "Locais para reivindicar",
"view_gps": ""
"to_claim": "Locais para reivindicar"
},
"login": {
"access_policy": "Política de Acesso",
@@ -820,7 +797,6 @@
"reset_password": "Redefinir senha",
"sign_in": "assinar em",
"sms_instructions": "Você deve receber um código de 6 dígitos em seu telefone em breve. Por favor, insira-o abaixo para fazer login",
"suspended_error": "Conta suspensa, entre em contato com seu administrador",
"verification": "Verifique seu login",
"waiting_for_email_verification": "Conta ainda não validada por e-mail. Verifique sua caixa de entrada ou peça ao administrador para reenviar uma validação",
"welcome_back": "Bem vindo de volta!",
@@ -926,7 +902,7 @@
"dfs": "Substituição DFS",
"gw_commands": "Comandos de gateway",
"identifier": "Identificador",
"key_verification": "Informações Chave de Assinatura",
"key_verification": "Verificação da chave de assinatura",
"restricted": "Restrito",
"signed_upgrade": "Somente atualização assinada",
"title": "RESTRIÇÕES",
@@ -1046,7 +1022,6 @@
},
"system": {
"backend_logs": "Registros de back-end",
"configuration": "Configuração",
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
"endpoint": "Ponto final",
"hostname": "Nome de anfitrião",
@@ -1057,10 +1032,6 @@
"os": "Sistema Operacional",
"processors": "Processadores",
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
"secrets": "Segredos",
"secrets_create": "Criar Segredo",
"secrets_one": "Segredo",
"services": "Serviços",
"start": "Começar",
"subsystems": "Subsistemas",
"success_reload": "Comando de recarga enviado com sucesso!",
@@ -1082,11 +1053,9 @@
"previous_page": "Página anterior"
},
"user": {
"email_not_validated": "e-mail não validado",
"error_fetching": "Erro ao buscar informações do usuário: {{e}}",
"password": "Senha",
"role": "Função",
"suspended": "Suspenso",
"title": "Do utilizador"
},
"users": {
@@ -1131,11 +1100,9 @@
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
"title": "Locais",
"update_all_devices": "Atualizar todas as configurações do dispositivo",
"update_success": "Local atualizado!",
"upgrade_all_devices": "Atualize o firmware de todos os dispositivos",
"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente",
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",
"upgrade_options_available": "Aqui estão todas as revisões disponíveis, selecione aquela para a qual você deseja que TODOS os dispositivos deste local sejam atualizados",
"use_existing": "Usar existente",
"use_existing_contacts": "Usar contatos existentes",
"use_this_contact": "Use este contato"

View File

@@ -1,42 +1,40 @@
import * as React from 'react';
import { Alert, AlertIcon, Box, Button, Center, useToast } from '@chakra-ui/react';
import { MenuItem, useToast } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { Modal } from '../Modal';
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
import { useRebootDevice } from 'hooks/Network/Devices';
import { useMutationResult } from 'hooks/useMutationResult';
import { AxiosError } from 'models/Axios';
import { GatewayDevice } from 'models/Device';
export type RebootModalProps = {
serialNumber: string;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
type Props = {
device: GatewayDevice;
refresh: () => void;
};
export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => {
const RebootMenuItem = ({ device, refresh }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const addEventListeners = useControllerStore((state) => state.addEventListeners);
const { mutateAsync: reboot, isLoading } = useRebootDevice({ serialNumber });
const { mutateAsync: reboot } = useRebootDevice({ serialNumber: device.serialNumber });
const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({
objName: t('devices.one'),
operationType: 'reboot',
refresh: () => {
refresh();
addEventListeners([
{
id: `device-connection-${serialNumber}`,
id: `device-connection-${device.serialNumber}`,
type: 'DEVICE_CONNECTION',
serialNumber,
serialNumber: device.serialNumber,
callback: () => {
const id = `device-connection-notification-${serialNumber}`;
const id = `device-connection-notification-${device.serialNumber}`;
if (!toast.isActive(id)) {
toast({
id,
title: t('common.success'),
description: t('controller.devices.finished_reboot', { serialNumber }),
description: t('controller.devices.finished_reboot', { serialNumber: device.serialNumber }),
status: 'success',
duration: 5000,
isClosable: true,
@@ -46,17 +44,17 @@ export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => {
},
},
{
id: `device-disconnected-${serialNumber}`,
id: `device-disconnected-${device.serialNumber}`,
type: 'DEVICE_DISCONNECTION',
serialNumber,
serialNumber: device.serialNumber,
callback: () => {
const id = `device-disconnection-notification-${serialNumber}`;
const id = `device-disconnection-notification-${device.serialNumber}`;
if (!toast.isActive(id)) {
toast({
id,
title: t('common.success'),
description: t('controller.devices.started_reboot', { serialNumber }),
description: t('controller.devices.started_reboot', { serialNumber: device.serialNumber }),
status: 'success',
duration: 5000,
isClosable: true,
@@ -68,39 +66,17 @@ export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => {
]);
},
});
const handleRebootClick = () =>
reboot(undefined, {
onSuccess: () => {
onRebootSuccess();
modalProps.onClose();
},
onError: (e) => {
onRebootError(e as AxiosError);
},
});
return (
<Modal
{...modalProps}
title={t('commands.reboot')}
topRightButtons={
<Button colorScheme="blue" onClick={handleRebootClick} isLoading={isLoading}>
{t('commands.reboot')}
</Button>
}
options={{
modalSize: 'sm',
}}
>
<Box>
<Center mb={2}>
<Alert status="info" w="unset">
<AlertIcon />
{t('commands.reboot_description')}
</Alert>
</Center>
</Box>
</Modal>
);
return <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>;
};
export default RebootMenuItem;

View File

@@ -1,19 +1,9 @@
import React from 'react';
import {
Button,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Spinner,
Tooltip,
useToast,
} from '@chakra-ui/react';
import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip, useToast } from '@chakra-ui/react';
import axios from 'axios';
import { Wrench } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import RebootMenuItem from './RebootButton';
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices';
import { useUpdateDeviceToLatest } from 'hooks/Network/Firmware';
@@ -32,7 +22,6 @@ interface Props {
onOpenConfigureModal: (serialNumber: string) => void;
onOpenTelemetryModal: (serialNumber: string) => void;
onOpenScriptModal: (device: GatewayDevice) => void;
onOpenRebootModal: (serialNumber: string) => void;
size?: 'sm' | 'md' | 'lg';
isCompact?: boolean;
}
@@ -49,7 +38,6 @@ const DeviceActionDropdown = ({
onOpenTelemetryModal,
onOpenConfigureModal,
onOpenScriptModal,
onOpenRebootModal,
size,
isCompact,
}: Props) => {
@@ -157,9 +145,7 @@ const DeviceActionDropdown = ({
},
);
};
const handleConnectClick = () => getRtty();
const handleRebootClick = () => onOpenRebootModal(device.serialNumber);
return (
<Menu>
@@ -186,7 +172,6 @@ const DeviceActionDropdown = ({
</MenuButton>
)}
</Tooltip>
<Portal>
<MenuList>
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
@@ -194,7 +179,7 @@ const DeviceActionDropdown = ({
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
<MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>
<RebootMenuItem device={device} refresh={refresh} />
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
@@ -203,7 +188,6 @@ const DeviceActionDropdown = ({
</MenuItem>
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
</MenuList>
</Portal>
</Menu>
);
};

View File

@@ -53,7 +53,6 @@ export type DataTableProps = {
obj?: string;
sortBy?: { id: string; desc: boolean }[];
hiddenColumns?: string[];
hideEmptyListText?: boolean;
hideControls?: boolean;
minHeight?: string | number;
fullScreen?: boolean;
@@ -78,7 +77,6 @@ const _DataTable = ({
sortBy,
hiddenColumns,
hideControls,
hideEmptyListText,
count,
setPageInfo,
isManual,
@@ -88,7 +86,6 @@ const _DataTable = ({
const { t } = useTranslation();
const breakpoint = useBreakpoint();
const textColor = useColorModeValue('gray.700', 'white');
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
const getPageSize = () => {
try {
if (showAllRows) return 1000000;
@@ -145,10 +142,6 @@ const _DataTable = ({
usePagination,
) as TableInstanceWithHooks<object>;
const handleGoToPage = (newPage: number) => {
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage));
gotoPage(newPage);
};
const handleNextPage = () => {
nextPage();
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1));
@@ -263,13 +256,7 @@ const _DataTable = ({
{page.map((row: Row) => {
prepareRow(row);
return (
<Tr
{...row.getRowProps()}
key={uuid()}
_hover={{
backgroundColor: hoveredRowBg,
}}
>
<Tr {...row.getRowProps()} key={uuid()}>
{
// @ts-ignore
row.cells.map((cell) => (
@@ -301,7 +288,7 @@ const _DataTable = ({
</Tbody>
)}
</Table>
{!isLoading && data.length === 0 && !hideEmptyListText && (
{!isLoading && data.length === 0 && (
<Center>
{obj ? (
<Heading size="md" pt={12}>
@@ -322,7 +309,7 @@ const _DataTable = ({
<Tooltip label={t('table.first_page')}>
<IconButton
aria-label="Go to first page"
onClick={() => handleGoToPage(0)}
onClick={() => gotoPage(0)}
isDisabled={!canPreviousPage}
icon={<ArrowLeftIcon h={3} w={3} />}
mr={4}
@@ -360,7 +347,7 @@ const _DataTable = ({
max={pageOptions.length}
onChange={(_: unknown, numberValue: number) => {
const newPage = numberValue ? numberValue - 1 : 0;
handleGoToPage(newPage);
gotoPage(newPage);
}}
defaultValue={pageIndex + 1}
>
@@ -399,7 +386,7 @@ const _DataTable = ({
<Tooltip label={t('table.last_page')}>
<IconButton
aria-label="Go to last page"
onClick={() => handleGoToPage(pageCount - 1)}
onClick={() => gotoPage(pageCount - 1)}
isDisabled={!canNextPage}
icon={<ArrowRightIcon h={3} w={3} />}
ml={4}

View File

@@ -100,7 +100,6 @@ const SortableDataTable: React.FC<Props> = ({
}) => {
const { t } = useTranslation();
const breakpoint = useBreakpoint();
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
const textColor = useColorModeValue('gray.700', 'white');
const getPageSize = () => {
const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined;
@@ -224,13 +223,7 @@ const SortableDataTable: React.FC<Props> = ({
{page.map((row: Row) => {
prepareRow(row);
return (
<Tr
{...row.getRowProps()}
key={uuid()}
_hover={{
backgroundColor: hoveredRowBg,
}}
>
<Tr {...row.getRowProps()} key={uuid()}>
{
// @ts-ignore
row.cells.map((cell) => (

View File

@@ -1,27 +0,0 @@
import * as React from 'react';
const _GoogleMapMarker = (options: google.maps.MarkerOptions) => {
const [marker, setMarker] = React.useState<google.maps.Marker>();
React.useEffect(() => {
if (!marker) {
setMarker(new google.maps.Marker());
}
return () => {
if (marker) {
marker.setMap(null);
}
};
}, [marker]);
React.useEffect(() => {
if (marker) {
marker.setOptions(options);
}
}, [marker, options]);
return null;
};
export const GoogleMapMarker = React.memo(_GoogleMapMarker);

View File

@@ -1,89 +0,0 @@
import * as React from 'react';
import { isLatLngLiteral } from '@googlemaps/typescript-guards';
import { createCustomEqual } from 'fast-equals';
const deepCompareEqualsForMaps = createCustomEqual((deepEqual) =>
// @ts-ignore
(a: number | google.maps.LatLng | google.maps.LatLngLiteral, b: number | google.maps.LatLng | google.maps.LatLngLiteral) => {
if (
isLatLngLiteral(a) ||
a instanceof google.maps.LatLng ||
isLatLngLiteral(b) ||
b instanceof google.maps.LatLng
) {
return new google.maps.LatLng(a).equals(new google.maps.LatLng(b));
}
// @ts-ignore
return deepEqual(a, b);
},
);
const useDeepCompareMemoize = (value: unknown) => {
const ref = React.useRef<unknown>();
if (!deepCompareEqualsForMaps(value, ref.current)) {
ref.current = value;
}
return ref.current;
};
const useDeepCompareEffectForMaps = (callback: React.EffectCallback, dependencies: unknown[]) => {
React.useEffect(callback, dependencies.map(useDeepCompareMemoize));
};
export interface GoogleMapProps extends google.maps.MapOptions {
style: { [key: string]: string };
onClick?: (e: google.maps.MapMouseEvent) => void;
onIdle?: (map: google.maps.Map) => void;
children?: React.ReactNode;
}
const _GoogleMap = ({ style, onClick, onIdle, children, ...options }: GoogleMapProps) => {
const ref = React.useRef<HTMLDivElement>(null);
const [map, setMap] = React.useState<google.maps.Map>();
// because React does not do deep comparisons, a custom hook is used
useDeepCompareEffectForMaps(() => {
if (map) {
map.setOptions(options);
}
}, [map, options]);
React.useEffect(() => {
if (ref.current && !map) {
setMap(new window.google.maps.Map(ref.current, {}));
}
}, [ref, map]);
React.useEffect(() => {
if (map) {
['click', 'idle'].forEach((eventName) => google.maps.event.clearListeners(map, eventName));
if (onClick) {
map.addListener('click', onClick);
}
if (onIdle) {
map.addListener('idle', () => onIdle(map));
}
}
}, [map, onClick, onIdle]);
return (
<>
<div ref={ref} style={style} />
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
// set the map prop on the child component
// @ts-ignore
return React.cloneElement(child, { map });
}
return null;
})}
</>
);
};
export const GoogleMap = React.memo(_GoogleMap);

View File

@@ -57,8 +57,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
upgrade({
keepRedirector: isRedirector,
uri,
signature:
device?.restrictedDevice && !device?.restrictionDetails?.developer ? ref.current?.values?.signature : undefined,
signature: device?.restrictedDevice ? ref.current?.values?.signature : undefined,
});
};
@@ -90,7 +89,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
</FormLabel>
<Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" />
</FormControl>
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
{device?.restrictedDevice && (
<Formik<{ signature?: string }>
innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined}
key={formKey}

View File

@@ -1,19 +1,15 @@
import React from 'react';
import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react';
import { Flex, ModalHeader as Header, Spacer } from '@chakra-ui/react';
export interface ModalHeaderProps {
title: string;
left?: React.ReactNode;
right: React.ReactNode;
}
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => (
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, right }) => (
<Header>
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
{title}
<HStack spacing={2} ml={2}>
{left ?? null}
</HStack>
<Spacer />
{right}
</Flex>

View File

@@ -8,7 +8,6 @@ export type ModalProps = {
onClose: () => void;
title: string;
topRightButtons?: React.ReactNode;
tags?: React.ReactNode;
options?: {
modalSize?: 'sm' | 'md' | 'lg';
maxWidth?: LayoutProps['maxWidth'];
@@ -16,7 +15,7 @@ export type ModalProps = {
children: React.ReactElement;
};
const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, children }: ModalProps) => {
const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => {
const maxWidth = React.useMemo(() => {
if (options?.maxWidth) return options.maxWidth;
if (options?.modalSize === 'sm') return undefined;
@@ -33,7 +32,6 @@ const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, childr
<ModalContent maxWidth={maxWidth}>
<ModalHeader
title={title}
left={tags}
right={
<HStack spacing={2}>
{topRightButtons}

View File

@@ -183,9 +183,7 @@ const CustomScriptForm = ({
<>
<Flex>
<Box>
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
<SignatureField name="signature" isDisabled={areFieldsDisabled} />
)}
{device?.restrictedDevice && <SignatureField name="signature" isDisabled={areFieldsDisabled} />}
</Box>
</Flex>
<SelectField

View File

@@ -76,7 +76,7 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
when: 0,
deferred: data.deferred,
timeout: data.timeout,
signature: device?.restrictedDevice && !device?.restrictionDetails?.developer ? data.signature : undefined,
signature: device?.restrictedDevice ? data.signature : undefined,
uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined,
scriptId: selectedScript,
type: data.type,

View File

@@ -149,8 +149,6 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
connectedDevices: msg.statistics.numberOfDevices,
connectingDevices: msg.statistics.numberOfConnectingDevices,
averageConnectionTime: msg.statistics.averageConnectedTime,
tx: msg.statistics.tx,
rx: msg.statistics.rx,
},
queryClient,
);

View File

@@ -52,8 +52,6 @@ type ConnectionStatisticsMessage = {
numberOfDevices: number;
numberOfConnectingDevices: number;
averageConnectedTime: number;
tx: number;
rx: number;
};
};
serialNumbers?: undefined;
@@ -87,8 +85,6 @@ export type SocketWebSocketNotificationData =
numberOfDevices: number;
numberOfConnectingDevices: number;
averageConnectedTime: number;
rx: number;
tx: number;
};
serialNumber?: undefined;
log?: undefined;

View File

@@ -11,10 +11,8 @@ export type DeviceLog = {
severity: number;
};
const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1) => async () =>
axiosGw
.get(`device/${serialNumber}/logs?newest=true&limit=${limit}&logType=${logType}`)
.then((response) => response.data) as Promise<{
const getDeviceLogs = (limit: number, serialNumber?: string) => async () =>
axiosGw.get(`device/${serialNumber}/logs?newest=true&limit=${limit}`).then((response) => response.data) as Promise<{
values: DeviceLog[];
serialNumber: string;
}>;
@@ -23,29 +21,20 @@ export const useGetDeviceLogs = ({
serialNumber,
limit,
onError,
logType,
}: {
serialNumber?: string;
limit: number;
onError?: (e: AxiosError) => void;
logType?: 0 | 1;
}) =>
useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), {
useQuery(['devicelogs', serialNumber, { limit }], getDeviceLogs(limit, serialNumber), {
keepPreviousData: true,
enabled: serialNumber !== undefined && serialNumber !== '',
staleTime: 30000,
onError,
});
const deleteLogs = async ({
serialNumber,
endDate,
logType,
}: {
serialNumber: string;
endDate: number;
logType: 0 | 1;
}) => axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}&logType=${logType}`);
const deleteLogs = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) =>
axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}`);
export const useDeleteLogs = () => {
const queryClient = useQueryClient();
@@ -56,25 +45,15 @@ export const useDeleteLogs = () => {
});
};
const getLogsBatch = (
serialNumber?: string,
start?: number,
end?: number,
limit?: number,
offset?: number,
logType?: 0 | 1,
) =>
const getLogsBatch = (serialNumber?: string, start?: number, end?: number, limit?: number, offset?: number) =>
axiosGw
.get(
`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}&logType=${logType}`,
)
.get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}`)
.then((response) => response.data) as Promise<{
values: DeviceLog[];
serialNumber: string;
}>;
const getDeviceLogsWithTimestamps =
(serialNumber?: string, start?: number, end?: number, logType?: 0 | 1) => async () => {
const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () => {
let offset = 0;
const limit = 100;
let logs: DeviceLog[] = [];
@@ -84,34 +63,28 @@ const getDeviceLogsWithTimestamps =
};
do {
// eslint-disable-next-line no-await-in-loop
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset, logType);
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset);
logs = logs.concat(latestResponse.values);
offset += limit;
} while (latestResponse.values.length === limit);
return {
values: logs,
};
};
};
export const useGetDeviceLogsWithTimestamps = ({
serialNumber,
start,
end,
onError,
logType,
}: {
serialNumber?: string;
start?: number;
end?: number;
onError?: (e: AxiosError) => void;
logType?: 0 | 1;
}) =>
useQuery(
['devicelogs', serialNumber, { start, end, logType }],
getDeviceLogsWithTimestamps(serialNumber, start, end, logType ?? 0),
{
useQuery(['devicelogs', serialNumber, { start, end }], getDeviceLogsWithTimestamps(serialNumber, start, end), {
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
staleTime: 1000 * 60,
onError,
},
);
});

View File

@@ -165,10 +165,7 @@ export type DevicesStats = {
averageConnectionTime: number;
connectedDevices: number;
connectingDevices: number;
tx: number;
rx: number;
};
const getInitialStats = async () =>
axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data);
export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => {

View File

@@ -7,32 +7,17 @@ import { AxiosError } from 'models/Axios';
import { Firmware } from 'models/Firmware';
import { Note } from 'models/Note';
const getAvailableFirmwareBatch = async (deviceType: string, limit: number, offset: number) =>
axiosFms
.get(`firmwares?deviceType=${deviceType}&limit=${limit}&offset=${offset}`)
.then(({ data }: { data: { firmwares: Firmware[] } }) => data);
const getAllAvailableFirmware = async (deviceType: string) => {
const limit = 500;
let offset = 0;
let data: { firmwares: Firmware[] } = { firmwares: [] };
let lastResponse: { firmwares: Firmware[] } = { firmwares: [] };
do {
// eslint-disable-next-line no-await-in-loop
lastResponse = await getAvailableFirmwareBatch(deviceType, limit, offset);
data = {
firmwares: [...data.firmwares, ...lastResponse.firmwares],
};
offset += 500;
} while (lastResponse.firmwares.length === 500);
return data;
};
export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) => {
const { t } = useTranslation();
const toast = useToast();
return useQuery(['get-device-profile'], () => getAllAvailableFirmware(deviceType), {
return useQuery(
['get-device-profile'],
() =>
axiosFms
.get(`firmwares?deviceType=${deviceType}&limit=10000&offset=0`)
.then(({ data }: { data: { firmwares: Firmware[] } }) => data),
{
enabled: deviceType !== '',
onError: (e: AxiosError) => {
if (!toast.isActive('firmware-fetching-error'))
@@ -49,7 +34,8 @@ export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string })
position: 'top-right',
});
},
});
},
);
};
export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) =>
@@ -70,13 +56,7 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe
return useMutation(
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
serialNumber,
when: 0,
keepRedirector,
uri,
signature,
}),
axiosGw.post(`device/${serialNumber}/upgrade`, { serialNumber, when: 0, keepRedirector, uri, signature }),
{
onSuccess: () => {
toast({
@@ -242,23 +222,3 @@ export const useGetFirmwareDashboard = () =>
keepPreviousData: true,
refetchInterval: 30000,
});
const getLastDbUpdate = async () =>
axiosFms.get(`firmwares?updateTimeOnly=true`).then((response) => response.data as { lastUpdateTime: number });
export const useGetFirmwareDbUpdate = () =>
useQuery(['firmware', 'db'], getLastDbUpdate, {
keepPreviousData: true,
staleTime: 30 * 1000,
});
const updateDb = async () => axiosFms.put(`firmwares?update=true`);
export const useUpdateFirmwareDb = () => {
const queryClient = useQueryClient();
return useMutation(updateDb, {
onSuccess: () => {
queryClient.invalidateQueries(['firmware', 'db']);
},
});
};

View File

@@ -1,83 +0,0 @@
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { axiosSec } from 'constants/axiosInstances';
export type SecretName = 'google.maps.apikey' | string;
export type Secret = {
key: SecretName;
value: string;
};
export type SecretDictionaryValue = {
key: SecretName;
description: string;
};
const getSecret = async (context: QueryFunctionContext<string[], unknown>) =>
axiosSec.get(`/systemSecret/${context.queryKey[1]}`).then(({ data }: { data: Secret }) => data);
export const useGetSystemSecret = ({ secret }: { secret: SecretName }) =>
useQuery(['secrets', secret], getSecret, {
staleTime: 1000 * 60 * 10,
refetchInterval: 1000 * 60 * 10,
});
const getAllSecrets = async () =>
axiosSec.get('/systemSecret/0?all=true').then(({ data }: { data: { secrets: Secret[] } }) => data.secrets);
export const useGetAllSystemSecrets = () => {
const queryClient = useQueryClient();
return useQuery(['secrets'], getAllSecrets, {
staleTime: 1000 * 60 * 10,
refetchInterval: 1000 * 60 * 10,
onSuccess: (data) => {
for (const secret of data) {
queryClient.setQueryData(['secrets', secret.key], secret);
}
},
});
};
const getSecretsDictionary = async () =>
axiosSec
.get('/systemSecret/0?dictionary=true')
.then(({ data }: { data: { knownKeys: SecretDictionaryValue[] } }) => data.knownKeys);
export const useGetSystemSecretsDictionary = () =>
useQuery(['secrets', 'dictionary'], getSecretsDictionary, {
staleTime: 1000 * 60 * 10,
refetchInterval: 1000 * 60 * 10,
});
const updateSecret = async ({ key, value }: { key: string; value: string }) =>
axiosSec.put(`/systemSecret/${key}?value=${value}`, { key, value });
export const useUpdateSystemSecret = () => {
const queryClient = useQueryClient();
return useMutation(updateSecret, {
onSuccess: () => {
queryClient.invalidateQueries(['secrets']);
},
});
};
export const useCreateSystemSecret = () => {
const queryClient = useQueryClient();
return useMutation(updateSecret, {
onSuccess: () => {
queryClient.invalidateQueries(['secrets']);
},
});
};
const deleteSecret = async (key: string) => axiosSec.delete(`/systemSecret/${key}`);
export const useDeleteSystemSecret = () => {
const queryClient = useQueryClient();
return useMutation(deleteSecret, {
onSuccess: () => {
queryClient.invalidateQueries(['secrets']);
},
});
};

View File

@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { axiosGw } from 'constants/axiosInstances';
import { AxiosError } from 'models/Axios';
@@ -21,18 +21,6 @@ type DeviceInterfaceStatistics = {
tx_errors: number;
tx_packets: number;
};
'counters-aggregate'?: {
collisions: number;
multicast: number;
rx_bytes: number;
rx_dropped: number;
rx_errors: number;
rx_packets: number;
tx_bytes: number;
tx_dropped: number;
tx_errors: number;
tx_packets: number;
};
ssids?: {
associations?: {
ack_signal: number;
@@ -160,11 +148,6 @@ export type DeviceStatistics = {
};
};
};
gps?: {
elevation: string;
latitude: string;
longitude: string;
};
version?: number;
};
const getLastStats = (serialNumber?: string) =>
@@ -180,7 +163,7 @@ export const useGetDeviceLastStats = ({
onError?: (e: AxiosError) => void;
}) =>
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
enabled: serialNumber !== undefined && serialNumber !== '',
enabled: serialNumber !== undefined && serialNumber !== '' && false,
staleTime: 1000 * 60,
onError,
});
@@ -200,12 +183,24 @@ export const useGetDeviceNewestStats = ({
serialNumber?: string;
limit: number;
onError?: (e: AxiosError) => void;
}) =>
useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
}) => {
const queryClient = useQueryClient();
return useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
enabled: serialNumber !== undefined && serialNumber !== '',
staleTime: 1000 * 60,
onSuccess: (response) => {
const entry = response.data[0];
// If we have a valid entry, we prefill lastStats, if not we trigger a fetch of the last statistics
if (entry) {
queryClient.setQueryData(['device', serialNumber, 'last-statistics'], entry.data);
} else {
queryClient.fetchQuery(['device', serialNumber, 'last-statistics']);
}
},
onError,
});
};
const getOuis = (macs?: string[]) => async () =>
axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{

View File

@@ -1,5 +1,5 @@
import { useToast } from '@chakra-ui/react';
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { axiosSec } from 'constants/axiosInstances';
import { AxiosError } from 'models/Axios';
@@ -58,23 +58,11 @@ export type User = {
waitingForEmailCheck: boolean;
};
const getAvatarPromises = (userList: User[], queryClient: QueryClient) => {
const getAvatarPromises = (userList: User[]) => {
const promises = userList.map(async (user) => {
if (user.avatar !== '' && user.avatar !== '0') {
// If the avatar is already in the cache, return it
const cachedAvatar = queryClient.getQueryData(['avatar', user.id, user.avatar]);
if (cachedAvatar) return cachedAvatar;
return axiosSec
.get(`avatar/${user.id}?cache=${user.avatar}`, {
return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, {
responseType: 'arraybuffer',
})
.then((response) => {
queryClient.setQueryData(['avatar', user.id, user.avatar], response);
return response;
})
.catch((e) => {
throw e;
});
}
return Promise.resolve('');
@@ -83,35 +71,10 @@ const getAvatarPromises = (userList: User[], queryClient: QueryClient) => {
return promises;
};
const getBatchUsers = async (offset: number, limit: number) => {
const users = await axiosSec
.get(`users?offset=${offset}&limit=${limit}&withExtendedInfo=true`)
.then(({ data }) => data.users as User[]);
const getUsers = async () => {
const users = await axiosSec.get('users').then(({ data }) => data.users as User[]);
return users;
};
const getAllUsers = async () => {
let users: User[] = [];
let offset = 0;
const limit = 500;
let lastResponseLength = 0;
do {
// eslint-disable-next-line no-await-in-loop
const response = await getBatchUsers(offset, limit);
users = [...users, ...response];
offset += limit;
lastResponseLength = response.length;
} while (lastResponseLength === limit);
return users;
};
const getUsers = async (queryClient: QueryClient) => {
const users = await getAllUsers();
const avatars = await Promise.allSettled(getAvatarPromises(users, queryClient)).then((results) =>
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
results.map((response) => {
if (response.status === 'fulfilled' && response?.value !== '') {
const base64 = btoa(
@@ -130,10 +93,8 @@ const getUsers = async (queryClient: QueryClient) => {
export const useGetUsers = () => {
const { t } = useTranslation();
const toast = useToast();
const queryClient = useQueryClient();
return useQuery(['users'], () => getUsers(queryClient), {
staleTime: 30 * 1000,
return useQuery(['users'], getUsers, {
onError: (e: AxiosError) => {
if (!toast.isActive('users-fetching-error'))
toast({
@@ -157,7 +118,7 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
const toast = useToast();
return useQuery(
['users', id],
['get-user', id],
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
{
enabled,
@@ -212,41 +173,16 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres
},
});
};
export const useSuspendUser = ({ id }: { id: string }) => {
const queryClient = useQueryClient();
return useMutation(
(isSuspended: boolean) =>
export const useSuspendUser = ({ id }: { id: string }) =>
useMutation((isSuspended: boolean) =>
axiosSec.put(`user/${id}`, {
suspended: isSuspended,
}),
{
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
},
);
};
export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}));
export const useResetMfa = ({ id }: { id: string }) => {
const queryClient = useQueryClient();
return useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}), {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
});
};
export const useResetPassword = ({ id }: { id: string }) => {
const queryClient = useQueryClient();
return useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}), {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
});
};
export const useResetPassword = ({ id }: { id: string }) =>
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
export const useDeleteUser = () => {

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
import { bytesString } from 'helpers/stringHelper';
import { useGetDevicesStats } from 'hooks/Network/Devices';
const SidebarDevices = () => {
@@ -49,7 +48,7 @@ const SidebarDevices = () => {
if (!getStats.data) return null;
return (
<VStack mb={-1}>
<VStack spacing={4}>
<Flex flexDir="column" textAlign="center">
<Heading size="md">{getStats.data.connectedDevices}</Heading>
<Heading size="xs">
@@ -58,16 +57,6 @@ const SidebarDevices = () => {
<Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400">
({getStats.data.connectingDevices} {t('controller.devices.connecting')})
</Heading>
<Heading
size="xs"
mt={1}
fontStyle="italic"
fontWeight="normal"
color="gray.400"
hidden={getStats.data.rx === undefined || getStats.data.tx === undefined}
>
Rx: {bytesString(getStats.data.rx)}, Tx: {bytesString(getStats.data.tx)}
</Heading>
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
<Heading size="md" textAlign="center" mt={2}>
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}

View File

@@ -1,75 +0,0 @@
import * as React from 'react';
import { Box, Button, Flex, FormControl, FormLabel, useDisclosure } from '@chakra-ui/react';
import { Wrapper } from '@googlemaps/react-wrapper';
import { Globe } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { GoogleMap } from 'components/Maps/GoogleMap';
import { GoogleMapMarker } from 'components/Maps/GoogleMap/Marker';
import { Modal } from 'components/Modals/Modal';
import { useGetSystemSecret } from 'hooks/Network/Secrets';
import { useGetDeviceLastStats } from 'hooks/Network/Statistics';
type Props = {
serialNumber: string;
};
const LocationDisplayButton = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const getGoogleApiKey = useGetSystemSecret({ secret: 'google.maps.apikey' });
const getLastStats = useGetDeviceLastStats({ serialNumber });
const location: google.maps.LatLngLiteral | undefined = React.useMemo(() => {
if (!getLastStats.data?.gps) return undefined;
try {
return {
lat: Number.parseFloat(getLastStats.data.gps.latitude),
lng: Number.parseFloat(getLastStats.data.gps.longitude),
};
} catch (e) {
return undefined;
}
}, [getLastStats.data?.gps]);
if (!location) {
return null;
}
return (
<>
<Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue">
{t('locations.view_gps')}
</Button>
<Modal isOpen={isOpen} onClose={onClose} title={t('locations.one')}>
<Box w="100%" h="100%">
<Flex mb={4}>
<FormControl w="unset">
<FormLabel>{t('locations.lat')}</FormLabel>
<pre>{location.lat}</pre>
</FormControl>
<FormControl w="unset" mx={4}>
<FormLabel>{t('locations.longitude')}</FormLabel>
<pre>{location.lng}</pre>
</FormControl>
<FormControl w="unset">
<FormLabel>{t('locations.elevation')}</FormLabel>
<pre>{getLastStats.data?.gps?.elevation}</pre>
</FormControl>
</Flex>
{getGoogleApiKey.data ? (
<Box h="500px">
<Wrapper apiKey={getGoogleApiKey.data.value}>
<GoogleMap center={location} style={{ flexGrow: '1', height: '100%' }} zoom={10}>
<GoogleMapMarker position={location} />
</GoogleMap>
</Wrapper>
</Box>
) : null}
</Box>
</Modal>
</>
);
};
export default LocationDisplayButton;

View File

@@ -1,94 +0,0 @@
import * as React from 'react';
import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import HistoryDatePickers from '../DatePickers';
import DeleteLogModal from './DeleteModal';
import useDeviceLogsTable from './useDeviceLogsTable';
import { RefreshButton } from 'components/Buttons/RefreshButton';
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
import { DataTable } from 'components/DataTables/DataTable';
import { Column } from 'models/Table';
type Props = {
serialNumber: string;
};
const CrashLogs = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const [limit, setLimit] = React.useState(25);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { time, setTime, getCustomLogs, getLogs, columns, modal } = useDeviceLogsTable({
serialNumber,
limit,
logType: 1,
});
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
};
const onClear = () => {
setTime(undefined);
};
const raiseLimit = () => {
setLimit(limit + 25);
};
const noMoreAvailable = getLogs.data !== undefined && getLogs.data.values.length < limit;
const data = React.useMemo(() => {
if (getCustomLogs.data) return getCustomLogs.data.values.sort((a, b) => b.recorded - a.recorded);
if (getLogs.data) return getLogs.data.values;
return [];
}, [getLogs.data, getCustomLogs.data]);
return (
<Box>
<Flex>
<Spacer />
<HStack>
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ColumnPicker
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
setHiddenColumns={setHiddenColumns}
preference="gateway.device.logs.hiddenColumns"
/>
<DeleteLogModal serialNumber={serialNumber} logType={0} />
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
</HStack>
</Flex>
<Box overflowY="auto" h="300px">
<DataTable
columns={
columns as {
id: string;
Header: string;
Footer: string;
accessor: string;
}[]
}
data={data}
isLoading={getLogs.isFetching || getCustomLogs.isFetching}
hiddenColumns={hiddenColumns}
obj={t('controller.devices.logs')}
// @ts-ignore
hideControls
showAllRows
/>
{getLogs.data !== undefined && (
<Center mt={1} hidden={getCustomLogs.data !== undefined}>
{!noMoreAvailable || getLogs.isFetching ? (
<Button colorScheme="blue" onClick={raiseLimit} isLoading={getLogs.isFetching}>
{t('controller.devices.show_more')}
</Button>
) : (
<Heading size="sm">{t('controller.devices.no_more_available')}!</Heading>
)}
</Center>
)}
</Box>
{modal}
</Box>
);
};
export default CrashLogs;

View File

@@ -16,8 +16,8 @@ const CustomInputButton = React.forwardRef(
),
);
type Props = { serialNumber: string; logType: 0 | 1 };
const DeleteLogModal = ({ serialNumber, logType }: Props) => {
type Props = { serialNumber: string };
const DeleteLogModal = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const modalProps = useDisclosure();
@@ -26,7 +26,7 @@ const DeleteLogModal = ({ serialNumber, logType }: Props) => {
const onDeleteClick = () => {
deleteLogs.mutate(
{ endDate: Math.floor(date.getTime() / 1000), serialNumber, logType },
{ endDate: Math.floor(date.getTime() / 1000), serialNumber },
{
onSuccess: () => {
modalProps.onClose();

View File

@@ -1,55 +0,0 @@
import * as React from 'react';
import { Box, Button, Code, Heading, useClipboard } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { Modal } from 'components/Modals/Modal';
import { DeviceLog } from 'hooks/Network/DeviceLogs';
type Props = {
modalProps: {
isOpen: boolean;
onClose: () => void;
};
log?: DeviceLog;
};
const DetailedLogViewModal = ({ modalProps, log }: Props) => {
const { t } = useTranslation();
const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(log?.log ?? {}, null, 2));
React.useEffect(() => {
setValue(JSON.stringify(log?.log ?? {}, null, 2));
}, [log]);
if (!log) return null;
return (
<Modal
isOpen={modalProps.isOpen}
onClose={modalProps.onClose}
title={t('devices.logs_one')}
topRightButtons={
<Button onClick={onCopy} size="md" colorScheme="teal">
{hasCopied ? `${t('common.copied')}!` : t('common.copy')}
</Button>
}
>
<Box>
<Heading size="sm">
<FormattedDate date={log.recorded} />
</Heading>
<Heading size="sm">
{t('controller.devices.severity')}: {log.severity}
</Heading>
<Heading size="sm">
{t('controller.devices.config_id')}: {log.UUID}
</Heading>
<Code whiteSpace="pre-line" mt={2}>
{log.log}
</Code>
</Box>
</Modal>
);
};
export default DetailedLogViewModal;

View File

@@ -16,7 +16,7 @@ const LogHistory = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const [limit, setLimit] = React.useState(25);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit, logType: 0 });
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit });
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
@@ -48,7 +48,7 @@ const LogHistory = ({ serialNumber }: Props) => {
setHiddenColumns={setHiddenColumns}
preference="gateway.device.logs.hiddenColumns"
/>
<DeleteLogModal serialNumber={serialNumber} logType={0} />
<DeleteLogModal serialNumber={serialNumber} />
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
</HStack>
</Flex>

View File

@@ -1,8 +1,6 @@
import * as React from 'react';
import { Box, IconButton, Text, useDisclosure } from '@chakra-ui/react';
import { MagnifyingGlass } from 'phosphor-react';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import DetailedLogViewModal from './DetailedLogViewModal';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { DeviceLog, useGetDeviceLogs, useGetDeviceLogsWithTimestamps } from 'hooks/Network/DeviceLogs';
import { Column } from 'models/Table';
@@ -10,49 +8,18 @@ import { Column } from 'models/Table';
type Props = {
serialNumber: string;
limit: number;
logType: 0 | 1;
};
const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
const { t } = useTranslation();
const getLogs = useGetDeviceLogs({ serialNumber, limit, logType });
const modalProps = useDisclosure();
const [log, setLog] = React.useState<DeviceLog | undefined>();
const getLogs = useGetDeviceLogs({ serialNumber, limit });
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
const getCustomLogs = useGetDeviceLogsWithTimestamps({
serialNumber,
start: time ? Math.floor(time.start.getTime() / 1000) : undefined,
end: time ? Math.floor(time.end.getTime() / 1000) : undefined,
logType,
});
const onOpen = React.useCallback((v: DeviceLog) => {
setLog(v);
modalProps.onOpen();
}, []);
const logCell = React.useCallback(
(v: DeviceLog) =>
logType === 1 ? (
<Box display="flex">
<IconButton
aria-label="Open Log Details"
onClick={() => onOpen(v)}
colorScheme="blue"
icon={<MagnifyingGlass size={16} />}
size="xs"
mr={2}
/>
<Text my="auto" maxW="calc(20vw)" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
{v.log}
</Text>
</Box>
) : (
v.log
),
[onOpen],
);
const dateCell = React.useCallback(
(v: number) => (
<Box>
@@ -98,7 +65,6 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
Footer: '',
accessor: 'log',
customWidth: '35px',
Cell: (v) => logCell(v.cell.row.original),
disableSortBy: true,
},
{
@@ -119,7 +85,6 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
getCustomLogs,
time,
setTime,
modal: <DetailedLogViewModal modalProps={modalProps} log={log} />,
};
};

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
import CommandHistory from './CommandHistory';
import HealthCheckHistory from './HealthCheckHistory';
import LogHistory from './LogHistory';
import CrashLogs from './LogHistory/CrashLogs';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
@@ -33,9 +32,6 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
<Tab fontSize="lg" fontWeight="bold">
{t('controller.devices.logs')}
</Tab>
<Tab fontSize="lg" fontWeight="bold">
{t('devices.crash_logs')}
</Tab>
</TabList>
<TabPanels>
<TabPanel p={0}>
@@ -55,12 +51,10 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
<TabPanel>
<HealthCheckHistory serialNumber={serialNumber} />
</TabPanel>
<TabPanel>
<LogHistory serialNumber={serialNumber} />
</TabPanel>
<TabPanel>
<CrashLogs serialNumber={serialNumber} />
</TabPanel>
</TabPanels>
</Tabs>
</CardBody>

View File

@@ -1,16 +1,5 @@
import * as React from 'react';
import {
Box,
Flex,
Heading,
ListItem,
Tag,
TagLabel,
TagLeftIcon,
Text,
Tooltip,
UnorderedList,
} from '@chakra-ui/react';
import { Box, Flex, Heading, ListItem, Text, UnorderedList } from '@chakra-ui/react';
import { LockSimple, LockSimpleOpen } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { Card } from 'components/Containers/Card';
@@ -31,7 +20,7 @@ const RestrictionsCard = ({ serialNumber }: Props) => {
ssh: 'SSH',
rtty: 'RTTY',
tty: t('restrictions.tty'),
// developer: t('restrictions.developer'),
developer: t('restrictions.developer'),
upgrade: t('restrictions.signed_upgrade'),
commands: t('restrictions.gw_commands'),
} as { [key: string]: string };
@@ -49,52 +38,27 @@ const RestrictionsCard = ({ serialNumber }: Props) => {
return restrictedKeys.map(([k]) => <ListItem key={k}>{LABELS[k]}</ListItem>);
};
const isMissingSigningInfo =
!restrictions.key_info ||
(!restrictions.key_info.algo && !restrictions.key_info.vendor) ||
(restrictions.key_info.algo.length === 0 && restrictions.key_info.vendor.length === 0);
return (
<Card mb={4}>
<CardHeader>
<Heading size="md" my="auto" mr={2}>
{t('restrictions.title')}
</Heading>
{getDevice.data?.restrictionDetails?.developer ? (
<Tooltip label={t('devices.restricted_overriden')} hasArrow>
<Tag size="lg" colorScheme="green">
<TagLeftIcon boxSize="18px" as={LockSimpleOpen} />
<TagLabel>{t('devices.restrictions_overriden_title')}</TagLabel>
</Tag>
</Tooltip>
) : null}
<Heading size="md">{t('restrictions.title')}</Heading>
</CardHeader>
<CardBody p={0} display="block">
<Flex mt={2}>
<Heading size="sm" mr={2} my="auto">
<Heading size="sm" mr={2}>
{t('restrictions.countries')}:
</Heading>
<Text my="auto">
{restrictions.country?.length === 0 ? t('common.all') : restrictions.country.join(', ')}
</Text>
<Text>{restrictions.country.join(', ')}</Text>
</Flex>
<Flex mt={2}>
<Heading size="sm" mt={2} my="auto">
{t('restrictions.key_verification')} {isMissingSigningInfo ? ':' : ''}
<Heading size="sm" mt={2}>
{t('restrictions.key_verification')}
</Heading>
{isMissingSigningInfo ? (
<Text my="auto" ml={2}>
{t('common.none')}
</Text>
) : null}
</Flex>
<UnorderedList hidden={isMissingSigningInfo}>
<UnorderedList>
<ListItem>
{t('controller.wifi.vendor')}:{' '}
{restrictions.key_info?.vendor?.length > 0 ? restrictions.key_info?.vendor : '-'}
{t('controller.wifi.vendor')}: {restrictions.key_info?.vendor}
</ListItem>
<ListItem>
{t('restrictions.algo')}: {restrictions.key_info?.algo?.length > 0 ? restrictions.key_info?.algo : '-'}
{t('restrictions.algo')}: {restrictions.key_info?.algo}
</ListItem>
</UnorderedList>
<Flex mt={2}>

View File

@@ -48,14 +48,14 @@ const InterfaceChart = ({ data }: Props) => {
{
// Real 'Tx', but shown as 'Rx'
label: 'Tx',
data: data.rx.map((tx) => (Math.floor((tx / factor) * 100) / 100).toFixed(2)),
data: data.rx.map((tx) => Math.floor((tx / factor) * 100) / 100),
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
},
{
// Real 'Rx', but shown as 'Tx'
label: 'Rx',
data: data.tx.map((rx) => (Math.floor((rx / factor) * 100) / 100).toFixed(2)),
data: data.tx.map((rx) => Math.floor((rx / factor) * 100) / 100),
borderColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
backgroundColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
},

View File

@@ -38,7 +38,7 @@ const ViewLastStatsModal = ({ serialNumber }: Props) => {
if (getLastStats.data) {
setValue(JSON.stringify(getLastStats.data, null, 2));
}
}, [getLastStats.data, isOpen]);
}, [getLastStats.data]);
return (
<>
<Tooltip label={t('statistics.last_stats')}>

View File

@@ -77,10 +77,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
let rx = inter.counters?.rx_bytes ?? 0;
let tx = inter.counters?.tx_bytes ?? 0;
if (inter['counters-aggregate']) {
rx = inter['counters-aggregate'].rx_bytes;
tx = inter['counters-aggregate'].tx_bytes;
} else if (isInterUpstream) {
if (isInterUpstream) {
for (const ssid of inter.ssids ?? []) {
rx += ssid.counters?.rx_bytes ?? 0;
tx += ssid.counters?.tx_bytes ?? 0;

View File

@@ -1,8 +1,7 @@
import * as React from 'react';
import { Box, Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react';
import { Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import LocationDisplayButton from './LocationDisplayButton';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
@@ -91,12 +90,11 @@ const DeviceSummary = ({ serialNumber }: Props) => {
{!getDevice.data?.locale || getDevice.data?.locale === '' ? (
'-'
) : (
<Box mr={2}>
<>
<ReactCountryFlag style={ICON_STYLE} countryCode={getDevice.data.locale} svg />
{COUNTRY_LIST.find(({ value }) => value === getDevice.data.locale)?.label}
</Box>
</>
)}
<LocationDisplayButton serialNumber={serialNumber} />
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<Heading size="sm">{t('analytics.last_contact')}:</Heading>

View File

@@ -13,7 +13,7 @@ import {
useColorModeValue,
useDisclosure,
} from '@chakra-ui/react';
import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react';
import { Heart, HeartBreak, LockSimple, WifiHigh, WifiSlash } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import Masonry from 'react-masonry-css';
import DeviceDetails from './Details';
@@ -33,7 +33,6 @@ import { ConfigureModal } from 'components/Modals/ConfigureModal';
import { EventQueueModal } from 'components/Modals/EventQueueModal';
import FactoryResetModal from 'components/Modals/FactoryResetModal';
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
import { RebootModal } from 'components/Modals/RebootModal';
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
import { TelemetryModal } from 'components/Modals/TelemetryModal';
import { TraceModal } from 'components/Modals/TraceModal';
@@ -57,11 +56,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
const upgradeModalProps = useDisclosure();
const telemetryModalProps = useDisclosure();
const traceModalProps = useDisclosure();
const rebootModalProps = useDisclosure();
const scriptModal = useScriptModal();
// Sticky-top styles
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
const connectedTag = React.useMemo(() => {
if (!getStatus.data) return null;
@@ -105,28 +100,9 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
);
}, [getStatus.data, getHealth.data]);
const restrictedTag = React.useMemo(() => {
if (!getDevice.data || !getDevice.data.restrictedDevice) return null;
if (getDevice.data.restrictionDetails?.developer)
return (
<Tooltip label={t('devices.restricted_overriden')} hasArrow>
<Tag size="lg" colorScheme="green">
<TagLeftIcon boxSize="18px" as={LockSimpleOpen} />
<TagLabel>
{t('devices.restricted')} {isCompact ? '' : '(Dev Mode)'}
</TagLabel>
</Tag>
</Tooltip>
);
return (
<Tag size="lg" colorScheme="red">
<TagLeftIcon boxSize="18px" as={LockSimple} />
<TagLabel>{t('devices.restricted')}</TagLabel>
</Tag>
);
}, [getDevice.data, isCompact]);
// Sticky-top styles
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
const refresh = () => {
getDevice.refetch();
@@ -143,7 +119,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<Heading size="md">{serialNumber}</Heading>
{connectedTag}
{healthTag}
{restrictedTag}
{getDevice.data?.restrictedDevice && (
<Tag size="lg" colorScheme="gray">
<TagLeftIcon boxSize="18px" as={LockSimple} />
<TagLabel>{t('devices.restricted')}</TagLabel>
</Tag>
)}
</HStack>
<Spacer />
<HStack spacing={2}>
@@ -161,7 +142,6 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
onOpenConfigureModal={configureModalProps.onOpen}
onOpenTelemetryModal={telemetryModalProps.onOpen}
onOpenScriptModal={scriptModal.openModal}
onOpenRebootModal={rebootModalProps.onOpen}
size="md"
isCompact
/>
@@ -192,7 +172,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<Heading size="md">{serialNumber}</Heading>
{connectedTag}
{healthTag}
{restrictedTag}
{getDevice.data?.restrictedDevice && (
<Tag size="lg" colorScheme="gray">
<TagLeftIcon boxSize="18px" as={LockSimple} />
<TagLabel>{t('devices.restricted')}</TagLabel>
</Tag>
)}
</HStack>
<Spacer />
<HStack spacing={2}>
@@ -209,7 +194,6 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
onOpenEventQueue={eventQueueProps.onOpen}
onOpenConfigureModal={configureModalProps.onOpen}
onOpenTelemetryModal={telemetryModalProps.onOpen}
onOpenRebootModal={rebootModalProps.onOpen}
onOpenScriptModal={scriptModal.openModal}
size="md"
/>
@@ -233,7 +217,6 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<EventQueueModal serialNumber={serialNumber} modalProps={eventQueueProps} />
<ConfigureModal serialNumber={serialNumber} modalProps={configureModalProps} />
<TelemetryModal serialNumber={serialNumber} modalProps={telemetryModalProps} />
<RebootModal serialNumber={serialNumber} modalProps={rebootModalProps} />
{scriptModal.modal}
<Box mt={isCompact ? '0px' : '68px'}>
<Masonry

View File

@@ -1,16 +1,27 @@
import * as React from 'react';
import { FormControl, FormErrorMessage, FormLabel, Input, Textarea, useDisclosure, useToast } from '@chakra-ui/react';
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Textarea,
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { CreateButton } from 'components/Buttons/CreateButton';
import { SaveButton } from 'components/Buttons/SaveButton';
import { Modal } from 'components/Modals/Modal';
import { useCreateBlacklist } from 'hooks/Network/Blacklist';
import { AxiosError } from 'models/Axios';
const CreateBlacklistModal = () => {
const { t } = useTranslation();
const toast = useToast();
const initialRef = React.useRef<HTMLInputElement>(null);
const modalProps = useDisclosure();
const createDevice = useCreateBlacklist();
const [serialNumber, setSerialNumber] = React.useState<string>('');
@@ -32,50 +43,41 @@ const CreateBlacklistModal = () => {
});
modalProps.onClose();
},
onError: (e) => {
toast({
id: 'add-blacklist-error',
title: t('common.error'),
description: (e as AxiosError)?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
},
);
};
const isSerialValid = serialNumber.length === 12 && serialNumber.match('^[a-fA-F0-9]+$') !== null;
const onOpen = () => {
React.useEffect(() => {
setSerialNumber('');
setReason('');
modalProps.onOpen();
setTimeout(() => {
initialRef.current?.focus();
}, 200);
};
}, [modalProps.isOpen]);
return (
<>
<CreateButton onClick={onOpen} isCompact ml={2} />
<CreateButton onClick={modalProps.onOpen} isCompact ml={2} />
<Modal
{...modalProps}
title={t('controller.devices.add_blacklist')}
topRightButtons={<SaveButton onClick={onSave} isLoading={createDevice.isLoading} isCompact />}
>
<>
{createDevice.error && (
<Alert status="error" mb={4}>
<AlertIcon />
<Box>
<AlertTitle>{t('common.error')}</AlertTitle>
{
// @ts-ignore
<AlertDescription>{createDevice.error?.response?.data?.ErrorDescription}</AlertDescription>
}
</Box>
</Alert>
)}
<FormControl isInvalid={!isSerialValid} mb={2}>
<FormLabel>{t('inventory.serial_number')}</FormLabel>
<Input
type="text"
onChange={(e) => setSerialNumber(e.target.value)}
value={serialNumber}
w="140px"
ref={initialRef}
/>
<Input type="text" onChange={(e) => setSerialNumber(e.target.value)} value={serialNumber} w="140px" />
<FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage>
</FormControl>
<FormControl>

View File

@@ -34,7 +34,6 @@ interface Props {
onOpenConfigureModal: (serialNumber: string) => void;
onOpenTelemetryModal: (serialNumber: string) => void;
onOpenScriptModal: (device: GatewayDevice) => void;
onOpenRebootModal: (serialNumber: string) => void;
}
const Actions: React.FC<Props> = ({
@@ -48,7 +47,6 @@ const Actions: React.FC<Props> = ({
onOpenConfigureModal,
onOpenTelemetryModal,
onOpenScriptModal,
onOpenRebootModal,
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -104,7 +102,6 @@ const Actions: React.FC<Props> = ({
onOpenConfigureModal={onOpenConfigureModal}
onOpenTelemetryModal={onOpenTelemetryModal}
onOpenScriptModal={onOpenScriptModal}
onOpenRebootModal={onOpenRebootModal}
/>
<Tooltip hasArrow label={t('common.view_details')} placement="top">
<Link href={`#/devices/${device.serialNumber}`}>

View File

@@ -1,8 +1,9 @@
import * as React from 'react';
import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
import { Box, Button, Heading, Image, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
import { LockSimple } from 'phosphor-react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Actions from './Actions';
import DeviceListFirmwareButton from './FirmwareButton';
import AP from './icons/AP.png';
@@ -20,7 +21,6 @@ import { ConfigureModal } from 'components/Modals/ConfigureModal';
import { EventQueueModal } from 'components/Modals/EventQueueModal';
import FactoryResetModal from 'components/Modals/FactoryResetModal';
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
import { RebootModal } from 'components/Modals/RebootModal';
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
import { TelemetryModal } from 'components/Modals/TelemetryModal';
import { TraceModal } from 'components/Modals/TraceModal';
@@ -49,6 +49,7 @@ const BADGE_COLORS: Record<string, string> = {
const DeviceListCard = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [serialNumber, setSerialNumber] = React.useState<string>('');
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined);
@@ -59,7 +60,6 @@ const DeviceListCard = () => {
const eventQueueProps = useDisclosure();
const telemetryModalProps = useDisclosure();
const configureModalProps = useDisclosure();
const rebootModalProps = useDisclosure();
const scriptModal = useScriptModal();
const getCount = useGetDeviceCount({ enabled: true });
const getDevices = useGetDevices({
@@ -98,9 +98,9 @@ const DeviceListCard = () => {
setSerialNumber(serial);
configureModalProps.onOpen();
};
const onOpenReboot = (serial: string) => {
setSerialNumber(serial);
rebootModalProps.onOpen();
const goToSerial = (serial: string) => () => {
navigate(`/devices/${serial}`);
};
const badgeCell = React.useCallback(
@@ -160,9 +160,9 @@ const DeviceListCard = () => {
const serialCell = React.useCallback(
(device: DeviceWithStatus) => (
<Link href={`#/devices/${device.serialNumber}`} fontSize="sm" my="auto" pt={1}>
<Button variant="link" onClick={goToSerial(device.serialNumber)} fontSize="sm">
<pre>{device.serialNumber}</pre>
</Link>
</Button>
),
[],
);
@@ -216,7 +216,6 @@ const DeviceListCard = () => {
onOpenConfigureModal={onOpenConfigure}
onOpenTelemetryModal={onOpenTelemetry}
onOpenScriptModal={scriptModal.openModal}
onOpenRebootModal={onOpenReboot}
/>
),
[],
@@ -407,6 +406,7 @@ const DeviceListCard = () => {
// @ts-ignore
setPageInfo={setPageInfo}
saveSettingsId="gateway.devices.table"
minHeight="600px"
/>
</Box>
</CardBody>
@@ -417,7 +417,6 @@ const DeviceListCard = () => {
<EventQueueModal modalProps={eventQueueProps} serialNumber={serialNumber} />
<ConfigureModal modalProps={configureModalProps} serialNumber={serialNumber} />
<TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} />
<RebootModal modalProps={rebootModalProps} serialNumber={serialNumber} />
{scriptModal.modal}
</>
);

View File

@@ -191,7 +191,6 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
ml={2}
/>
{isEditingDescription && (
// @ts-ignore
<SaveButton onClick={onSaveDescription} ml={2} isCompact size="sm" isLoading={updateFirmware.isLoading} />
)}
</FormLabel>
@@ -203,15 +202,18 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
isDisabled={!isEditingDescription}
/>
</FormControl>
</SimpleGrid>
<FormControl mt={2}>
<FormControl>
<FormLabel>
{t('common.notes')}{' '}
<Popover trigger="click" placement="auto">
{({ onClose }) => (
<>
<PopoverTrigger>
<IconButton aria-label={`${t('crud.add')} ${t('common.note')}`} size="sm" icon={<Plus size={20} />} />
<IconButton
aria-label={`${t('crud.add')} ${t('common.note')}`}
size="sm"
icon={<Plus size={20} />}
/>
</PopoverTrigger>
<PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}>
<PopoverArrow />
@@ -237,17 +239,11 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
)}
</Popover>
</FormLabel>
</FormControl>
<Box overflowX="auto" overflowY="auto" maxH="400px" mb={4}>
<DataTable
columns={columns as Column<object>[]}
data={notes}
obj={t('common.notes')}
minHeight="200px"
showAllRows
hideControls
/>
<Box overflowX="auto" overflowY="auto" maxH="400px">
<DataTable columns={columns as Column<object>[]} data={notes} obj={t('common.notes')} minHeight="200px" />
</Box>
</FormControl>
</SimpleGrid>
</Modal>
);
};

View File

@@ -1,101 +0,0 @@
import * as React from 'react';
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Button,
Center,
Tag,
TagLabel,
Text,
useDisclosure,
useToast,
} from '@chakra-ui/react';
import axios from 'axios';
import { Database } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { Modal } from 'components/Modals/Modal';
import { useGetFirmwareDbUpdate, useUpdateFirmwareDb } from 'hooks/Network/Firmware';
const UpdateDbButton = () => {
const { t } = useTranslation();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const updateDb = useUpdateFirmwareDb();
const getLastUpdate = useGetFirmwareDbUpdate();
const onUpdateClick = async () => {
updateDb.mutate(undefined, {
onSuccess: () => {
toast({
id: `firmware-db-update-success`,
title: t('common.success'),
description: t('firmware.started_db_update'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
onError: (e) => {
if (axios.isAxiosError(e)) {
toast({
id: `firmware-db-update-error`,
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
},
});
};
return (
<>
<Button colorScheme="teal" leftIcon={<Database size={20} />} onClick={onOpen}>
{t('firmware.last_db_update_title')}
</Button>
<Modal
isOpen={isOpen}
onClose={onClose}
title={t('firmware.last_db_update_modal')}
tags={
<Tag colorScheme="blue" size="lg">
<TagLabel display="flex">
<Text mr={1}>Last Update:</Text>
<FormattedDate date={getLastUpdate.data?.lastUpdateTime} />
</TagLabel>
</Tag>
}
>
<Box>
<Alert status="warning">
<AlertIcon />
<Box>
<AlertTitle>{t('common.warning')}</AlertTitle>
<AlertDescription>{t('firmware.db_update_warning')}</AlertDescription>
</Box>
</Alert>
<Center my={4}>
<Button
colorScheme="red"
leftIcon={<Database size={20} />}
onClick={onUpdateClick}
isLoading={updateDb.isLoading}
>
{t('firmware.start_db_update')}
</Button>
</Center>
</Box>
</Modal>
</>
);
};
export default UpdateDbButton;

View File

@@ -16,7 +16,6 @@ import { MagnifyingGlass } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import FirmwareDetailsModal from './Modal';
import UpdateDbButton from './UpdateDbButton';
import UriCell from './UriCell';
import { RefreshButton } from 'components/Buttons/RefreshButton';
import { CardBody } from 'components/Containers/Card/CardBody';
@@ -144,7 +143,6 @@ const FirmwareListTable = () => {
</Box>
<Text>{t('controller.firmware.show_dev_releases')}</Text>
<Switch isChecked={showDevFirmware} onChange={toggle} size="lg" />
<UpdateDbButton />
<RefreshButton
onClick={() => {
getDeviceTypes.refetch();

View File

@@ -48,10 +48,7 @@ const _LoginForm: React.FC<_LoginFormProps> = ({ setActiveForm }) => {
const displayError = useMemo(() => {
const loginError: AxiosError = error as AxiosError;
if (loginError?.response?.data?.ErrorCode === 5) return t('login.waiting_for_email_verification');
if (loginError?.response?.data?.ErrorCode === 15) {
return t('login.suspended_error');
}
if (loginError?.response?.data?.ErrorCode === 4) return t('login.waiting_for_email_verification');
return t('login.invalid_credentials');
}, [t, error]);

View File

@@ -1,139 +0,0 @@
import React from 'react';
import { CopyIcon } from '@chakra-ui/icons';
import {
IconButton,
Tooltip,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Center,
Box,
Button,
useDisclosure,
HStack,
Text,
useClipboard,
} from '@chakra-ui/react';
import { Eye, Trash } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import EditSecretButton from './EditButton';
import { Secret, useDeleteSystemSecret } from 'hooks/Network/Secrets';
interface Props {
secret: Secret;
isDisabled?: boolean;
}
const SystemSecretActions = ({ secret, isDisabled }: Props) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const deleteSecret = useDeleteSystemSecret();
const { hasCopied, onCopy } = useClipboard(secret.value);
const handleDeleteClick = React.useCallback(() => {
deleteSecret.mutate(secret.key, {
onSuccess: () => {
onClose();
},
});
}, []);
return (
<HStack mx="auto">
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
<Box>
<PopoverTrigger>
<IconButton
aria-label="delete-device"
colorScheme="red"
icon={<Trash size={20} />}
size="sm"
isDisabled={isDisabled}
/>
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent w="340px">
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.delete')} {secret.key}
</PopoverHeader>
<PopoverBody>
<Text whiteSpace="break-spaces">{t('crud.delete_confirm', { obj: t('system.secrets_one') })}</Text>
</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteSecret.isLoading}>
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
<Tooltip
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`}
hasArrow
closeOnClick={false}
>
<IconButton
aria-label={t('common.copy')}
icon={<CopyIcon h={5} w={5} />}
onClick={onCopy}
size="sm"
colorScheme="teal"
mr={2}
/>
</Tooltip>
<EditSecretButton secret={secret} />
<Popover>
<Tooltip label={`${t('common.view')} ${t('system.secrets_one')}`} hasArrow closeOnClick={false}>
<Box>
<PopoverTrigger>
<IconButton aria-label={t('common.view')} icon={<Eye size={20} />} size="sm" colorScheme="purple" />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent w="560px">
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('common.view')} {secret.key}
<Tooltip
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`}
hasArrow
closeOnClick={false}
>
<IconButton
aria-label={t('common.copy')}
icon={<CopyIcon h={4} w={4} />}
onClick={onCopy}
size="xs"
colorScheme="teal"
ml={2}
/>
</Tooltip>
</PopoverHeader>
<PopoverBody>
<Text whiteSpace="break-spaces">
<Center>
<pre style={{ fontFamily: 'monospace' }}>{secret.value}</pre>
</Center>
</Text>
</PopoverBody>
</PopoverContent>
</Popover>
</HStack>
);
};
export default SystemSecretActions;

View File

@@ -1,118 +0,0 @@
import * as React from 'react';
import {
Box,
FormControl,
FormErrorMessage,
FormLabel,
Input,
Textarea,
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { CreateButton } from '../../../components/Buttons/CreateButton';
import { SaveButton } from '../../../components/Buttons/SaveButton';
import { Modal } from '../../../components/Modals/Modal';
import { useCreateSystemSecret } from 'hooks/Network/Secrets';
import { AxiosError } from 'models/Axios';
type FormValues = {
key: string;
value: string;
};
const DEFAULT_FORM_VALUES: FormValues = {
key: '',
value: '',
};
const SystemSecretCreateButton = () => {
const { t } = useTranslation();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const [form, setForm] = React.useState<FormValues>(DEFAULT_FORM_VALUES);
const [isNameChanged, setIsNameChanged] = React.useState(false);
const [isValueChanged, setIsValueChanged] = React.useState(false);
const create = useCreateSystemSecret();
const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, key: e.target.value });
if (!isNameChanged) setIsNameChanged(true);
};
const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setForm({ ...form, value: e.target.value });
if (!isValueChanged) setIsValueChanged(true);
};
const isNameError = form.key.length === 0;
const isValueError = form.value.length === 0;
const onSubmit = () => {
create.mutate(form, {
onSuccess: () => {
toast({
id: 'create-system-secret-success',
title: t('common.success'),
description: t('crud.success_update_obj', {
obj: t('system.secrets_one'),
}),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
},
onError: (e) => {
toast({
id: 'create-system-secret-error',
title: t('common.error'),
description: (e as AxiosError)?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
});
};
const handleOpenClick = () => {
setIsNameChanged(false);
setIsValueChanged(false);
setForm(DEFAULT_FORM_VALUES);
onOpen();
};
return (
<>
<CreateButton onClick={handleOpenClick} isCompact />
<Modal
isOpen={isOpen}
onClose={onClose}
title={t('system.secrets_create')}
topRightButtons={
<SaveButton onClick={onSubmit} isDisabled={isNameError || isValueError} isLoading={create.isLoading} />
}
options={{
modalSize: 'sm',
}}
>
<Box>
<FormControl mb={2} isInvalid={isNameError && isNameChanged}>
<FormLabel>{t('common.name')}</FormLabel>
<Input value={form.key} onChange={onKeyChange} />
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
</FormControl>
<FormControl mb={2} isInvalid={isValueError && isValueChanged}>
<FormLabel>{t('common.value')}</FormLabel>
<Textarea value={form.value} onChange={onValueChange} rows={2} />
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
</FormControl>
</Box>
</Modal>
</>
);
};
export default SystemSecretCreateButton;

View File

@@ -1,146 +0,0 @@
import * as React from 'react';
import {
Box,
Button,
Center,
FormControl,
FormErrorMessage,
FormLabel,
IconButton,
Input,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Text,
Textarea,
Tooltip,
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { Pencil } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { Secret, useUpdateSystemSecret } from 'hooks/Network/Secrets';
import { AxiosError } from 'models/Axios';
type FormValues = {
key: string;
value: string;
};
type Props = {
secret: Secret;
};
const EditSecretButton = ({ secret }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const [form, setForm] = React.useState<FormValues>({
key: secret.key,
value: secret.value,
});
const [isNameChanged, setIsNameChanged] = React.useState(false);
const [isValueChanged, setIsValueChanged] = React.useState(false);
const update = useUpdateSystemSecret();
const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, key: e.target.value });
if (!isNameChanged) setIsNameChanged(true);
};
const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setForm({ ...form, value: e.target.value });
if (!isValueChanged) setIsValueChanged(true);
};
const isNameError = form.key.length === 0;
const isValueError = form.value.length === 0;
const onSubmit = () => {
update.mutate(form, {
onSuccess: () => {
toast({
id: 'create-system-secret-success',
title: t('common.success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
},
onError: (e) => {
toast({
id: 'create-system-secret-error',
title: t('common.error'),
description: (e as AxiosError)?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
});
};
const handleOpenClick = () => {
setIsNameChanged(false);
setIsValueChanged(false);
setForm({
key: secret.key,
value: secret.value,
});
onOpen();
};
return (
<Popover isOpen={isOpen} onOpen={handleOpenClick} onClose={onClose}>
<Tooltip hasArrow label={t('crud.edit')} placement="top" isDisabled={isOpen}>
<Box>
<PopoverTrigger>
<IconButton aria-label="delete-device" colorScheme="blue" icon={<Pencil size={20} />} size="sm" />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent w="340px">
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.edit')} {secret.key}
</PopoverHeader>
<PopoverBody>
<Text whiteSpace="break-spaces">
<Box>
<FormControl mb={2} isInvalid={isNameError && isNameChanged}>
<FormLabel>{t('common.name')}</FormLabel>
<Input value={form.key} onChange={onKeyChange} />
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
</FormControl>
<FormControl mb={2} isInvalid={isValueError && isValueChanged}>
<FormLabel>{t('common.value')}</FormLabel>
<Textarea value={form.value} onChange={onValueChange} rows={2} />
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
</FormControl>
</Box>
</Text>
</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="blue" ml="1" onClick={onSubmit} isLoading={update.isLoading}>
{t('common.save')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
);
};
export default EditSecretButton;

View File

@@ -1,70 +0,0 @@
import * as React from 'react';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { DataTable } from '../../../components/DataTables/DataTable';
import SystemSecretActions from './Actions';
import { Secret, useGetAllSystemSecrets, useGetSystemSecretsDictionary } from 'hooks/Network/Secrets';
import { Column } from 'models/Table';
const SystemSecretsTable = () => {
const { t } = useTranslation();
const getSecrets = useGetAllSystemSecrets();
const getDictionary = useGetSystemSecretsDictionary();
const descriptionCell = React.useCallback(
(secret: Secret) => {
if (!getDictionary.data) return '-';
return getDictionary.data.find((d) => d.key === secret.key)?.description ?? '-';
},
[getDictionary.data],
);
const actionCell = React.useCallback((secret: Secret) => <SystemSecretActions secret={secret} />, []);
const columns = React.useMemo(
(): Column<Secret>[] => [
{
id: 'key',
Header: t('common.name'),
Footer: '',
accessor: 'key',
alwaysShow: true,
customWidth: '120px',
},
{
id: 'description',
Header: t('common.description'),
Footer: '',
Cell: ({ cell }) => descriptionCell(cell.row.original),
accessor: 'description',
hasPopover: true,
},
{
id: 'actions',
Header: t('common.actions'),
Footer: '',
Cell: (v) => actionCell(v.cell.row.original),
disableSortBy: true,
customWidth: '120px',
alwaysShow: true,
},
],
[t, descriptionCell],
);
return (
<Box w="100%">
<DataTable
columns={columns as Column<object>[]}
saveSettingsId="system.secrets.table"
data={getSecrets.data ?? []}
obj={t('keys.other')}
sortBy={[{ id: 'key', desc: false }]}
showAllRows
hideControls
/>
</Box>
);
};
export default SystemSecretsTable;

View File

@@ -1,53 +0,0 @@
import * as React from 'react';
import {
BackgroundProps,
Box,
EffectProps,
Heading,
InteractivityProps,
LayoutProps,
PositionProps,
SpaceProps,
Spacer,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import SystemSecretCreateButton from './CreateButton';
import SystemSecretsTable from './Table';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { useAuth } from 'contexts/AuthProvider';
export interface SystemSecretsCardProps
extends LayoutProps,
SpaceProps,
BackgroundProps,
InteractivityProps,
PositionProps,
EffectProps {}
export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => {
const { t } = useTranslation();
const { user } = useAuth();
if (!user || user.userRole !== 'root') {
return null;
}
return (
<Box px={4} py={4}>
<Card variant="widget" {...props}>
<CardHeader>
<Heading size="md" my="auto">
{t('system.secrets')}
</Heading>
<Spacer />
<SystemSecretCreateButton />
</CardHeader>
<CardBody p={4}>
<SystemSecretsTable />
</CardBody>
</Card>
</Box>
);
};

View File

@@ -21,8 +21,8 @@ import axios from 'axios';
import { FloppyDisk } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { Modal } from '../../../../components/Modals/Modal';
import { LoadingOverlay } from 'components/LoadingOverlay';
import { Modal } from 'components/Modals/Modal';
import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System';
type Props = {

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { DataTable } from '../../../components/DataTables/DataTable';
import { DataTable } from 'components/DataTables/DataTable';
import { compactDate } from 'helpers/dateFormatting';
import { Column } from 'models/Table';

View File

@@ -19,11 +19,11 @@ import {
import { MultiValue, Select } from 'chakra-react-select';
import { ArrowsClockwise } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
import SystemLoggingButton from './LoggingButton';
import SystemCertificatesTable from './SystemCertificatesTable';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { compactSecondsToDetailed } from 'helpers/dateFormatting';
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System';
@@ -65,7 +65,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
return (
<>
<Card variant="widget">
<Card>
<Box display="flex" mb={2}>
<Heading pt={0}>{endpoint.type}</Heading>
<Spacer />
@@ -73,7 +73,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
<Button
mt={1}
minWidth="112px"
colorScheme="blue"
colorScheme="gray"
rightIcon={<ArrowsClockwise />}
onClick={refresh}
isLoading={isFetchingSystem || isFetchingSubsystems}
@@ -179,7 +179,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
ml={2}
onClick={handleReloadClick}
icon={<ArrowsClockwise size={20} />}
colorScheme="blue"
colorScheme="gray"
isLoading={isReloading}
isDisabled={subs.length === 0}
/>

View File

@@ -1,47 +1,27 @@
import React from 'react';
import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import { Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { RefreshButton } from '../../components/Buttons/RefreshButton';
import { SystemSecretsCard } from './SystemSecrets';
import SystemTile from './SystemTile';
import { RefreshButton } from 'components/Buttons/RefreshButton';
import { Card } from 'components/Containers/Card';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { axiosSec } from 'constants/axiosInstances';
import { useAuth } from 'contexts/AuthProvider';
import { useGetEndpoints } from 'hooks/Network/Endpoints';
const getDefaultTabIndex = () => {
const index = localStorage.getItem('system-tab-index') || '0';
try {
return parseInt(index, 10);
} catch {
return 0;
}
};
type Props = {
isOnlySec?: boolean;
};
const SystemPage = ({ isOnlySec }: Props) => {
const SystemPage = () => {
const { t } = useTranslation();
const { token, user } = useAuth();
const { token } = useAuth();
const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} });
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
const handleTabChange = (index: number) => {
setTabIndex(index);
localStorage.setItem('system-tab-index', index.toString());
};
const isRoot = user && user.userRole === 'root';
const endpointsList = React.useMemo(() => {
if (!token || (!isOnlySec && !endpoints)) return null;
if (!endpoints || !token) return null;
const endpointList = endpoints ? [...endpoints] : [];
const endpointList = [...endpoints];
endpointList.push({
uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '',
type: isOnlySec ? '' : 'owsec',
type: 'owsec',
id: 0,
vendor: 'owsec',
authenticationType: '',
@@ -57,51 +37,20 @@ const SystemPage = ({ isOnlySec }: Props) => {
}, [endpoints, token]);
return (
<Card p={0}>
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
<TabList>
<>
<Card mb={4} py={2} px={4}>
<CardHeader>
<Tab>{t('system.services')}</Tab>
<Tab hidden={!isRoot}>{t('system.configuration')}</Tab>
</CardHeader>
</TabList>
<TabPanels>
<TabPanel p={0}>
<Box
borderLeft="1px solid"
borderRight="1px solid"
borderBottom="1px solid"
borderColor="var(--chakra-colors-chakra-border-color)"
borderBottomLeftRadius="15px"
borderBottomRightRadius="15px"
>
{!isOnlySec && (
<CardHeader px={4} pt={4}>
<Heading size="md" my="auto">
{t('controller.firmware.endpoints')}
</Heading>
<Spacer />
<RefreshButton onClick={refetch} isFetching={isFetching} />
</CardHeader>
)}
<SimpleGrid minChildWidth="500px" spacing="20px" p={4}>
</Card>
<SimpleGrid minChildWidth="500px" spacing="20px" mb={3}>
{endpointsList}
</SimpleGrid>
</Box>
</TabPanel>
<TabPanel p={0}>
<Box
borderLeft="1px solid"
borderRight="1px solid"
borderBottom="1px solid"
borderColor="var(--chakra-colors-chakra-border-color)"
borderBottomLeftRadius="15px"
borderBottomRightRadius="15px"
>
<SystemSecretsCard />
</Box>
</TabPanel>
</TabPanels>
</Tabs>
</Card>
</>
);
};
export default SystemPage;

View File

@@ -12,11 +12,9 @@ interface Props {
isSuspended: boolean;
isWaitingForCheck: boolean;
refresh: () => void;
isDisabled?: boolean;
size?: 'sm' | 'md' | 'lg';
}
const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', isDisabled }: Props) => {
const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refresh }) => {
const { t } = useTranslation();
const toast = useToast();
const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh });
@@ -78,16 +76,9 @@ const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm',
return (
<Menu>
<Tooltip label={t('commands.other')}>
<MenuButton
as={IconButton}
aria-label="Commands"
icon={<Wrench size={20} />}
size={size}
ml={2}
isDisabled={isDisabled}
/>
<MenuButton as={IconButton} aria-label="Commands" icon={<Wrench size={20} />} size="sm" ml={2} />
</Tooltip>
<MenuList fontSize="md">
<MenuList>
<MenuItem onClick={handleSuspendClick}>
{isSuspended ? t('users.reactivate_user') : t('users.suspend')}
</MenuItem>

View File

@@ -62,7 +62,7 @@ const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Pro
useEffect(() => {
setFormKey(uuid());
}, [isOpen, editing]);
}, [isOpen]);
return (
<Formik

View File

@@ -1,14 +1,12 @@
import * as React from 'react';
import { useEffect } from 'react';
import { Spinner, Center, useDisclosure, useBoolean, Tag } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { Spinner, Center, useDisclosure, useBoolean } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { EditButton } from '../../../../components/Buttons/EditButton';
import { SaveButton } from '../../../../components/Buttons/SaveButton';
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
import { Modal } from '../../../../components/Modals/Modal';
import ActionsDropdown from '../ActionsDropdown';
import UpdateUserForm from './Form';
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
import { useGetUser, User } from 'hooks/Network/Users';
import { useFormRef } from 'hooks/useFormRef';
@@ -21,11 +19,10 @@ type Props = {
const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
const { t } = useTranslation();
const [editing, setEditing] = useBoolean();
const queryClient = useQueryClient();
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
const { form, formRef } = useFormRef<User>();
const canFetchUser = userId !== '' && isOpen;
const { data: user, isFetching, refetch } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
const { data: user, isFetching } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
const closeModal = () => (form.dirty ? openConfirm() : onClose());
@@ -34,11 +31,6 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
onClose();
};
const refresh = () => {
refetch();
queryClient.invalidateQueries(['users']);
};
useEffect(() => {
if (isOpen) setEditing.off();
}, [isOpen]);
@@ -48,40 +40,15 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
<Modal
isOpen={isOpen}
onClose={closeModal}
title={user?.name ?? t('crud.edit_obj', { obj: t('user.title') })}
tags={
<>
{user?.suspended ? (
<Tag colorScheme="yellow" size="lg">
{t('user.suspended')}
</Tag>
) : null}
{user?.waitingForEmailCheck ? (
<Tag colorScheme="blue" size="lg">
{t('user.email_not_validated')}
</Tag>
) : null}
</>
}
title={t('crud.edit_obj', { obj: t('user.title') })}
topRightButtons={
<>
<SaveButton
onClick={form.submitForm}
isLoading={form.isSubmitting}
isDisabled={!editing || !form.isValid || !form.dirty}
hidden={!editing}
/>
<ToggleEditButton ml={2} isEditing={editing} toggleEdit={setEditing.toggle} isDirty={form.dirty} />
{user ? (
<ActionsDropdown
id={user?.id}
isSuspended={user?.suspended}
isWaitingForCheck={user?.waitingForEmailCheck}
refresh={refresh}
size="md"
isDisabled={editing}
/>
) : null}
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
</>
}
>

View File

@@ -141,7 +141,7 @@ const UserTable = () => {
<Box overflowX="auto" w="100%">
<DataTable
columns={columns as Column<object>[]}
data={users ?? []}
data={users?.filter((curr) => curr.email !== user?.email) ?? []}
isLoading={isFetching}
obj={t('users.title')}
sortBy={[{ id: 'email', desc: false }]}