mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
synced 2025-10-30 02:12:33 +00:00
Compare commits
37 Commits
v2.8.0
...
v2.9.0-RC2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69b0d1ee9d | ||
|
|
ef52497b04 | ||
|
|
039e641046 | ||
|
|
f1f62efe6f | ||
|
|
b3053f32b2 | ||
|
|
09184b0402 | ||
|
|
98562fd967 | ||
|
|
65e9e64cb4 | ||
|
|
573ecbd58d | ||
|
|
a801fcca49 | ||
|
|
e9d16ee172 | ||
|
|
db4dfc93e8 | ||
|
|
975b715a7c | ||
|
|
cf17f03ae0 | ||
|
|
64f3ee797e | ||
|
|
e287705e88 | ||
|
|
9583b2bae0 | ||
|
|
2698993a6d | ||
|
|
a14b595e8c | ||
|
|
d7957b85ae | ||
|
|
227a51423d | ||
|
|
ea0e7340cc | ||
|
|
999680e94b | ||
|
|
566dbbb157 | ||
|
|
75d995d54e | ||
|
|
908faa491b | ||
|
|
7a254e343e | ||
|
|
016ac336b9 | ||
|
|
1cfd3a10ad | ||
|
|
1838029d22 | ||
|
|
7767043a5a | ||
|
|
b1cfa6db19 | ||
|
|
623d5a5546 | ||
|
|
8c676eb965 | ||
|
|
1e4ccce36c | ||
|
|
1808206e74 | ||
|
|
0fbc2b92aa |
@@ -8,7 +8,7 @@ fullnameOverride: ""
|
||||
images:
|
||||
owgwui:
|
||||
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
||||
tag: main
|
||||
tag: v2.9.0-RC2
|
||||
pullPolicy: Always
|
||||
|
||||
services:
|
||||
|
||||
97
package-lock.json
generated
97
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.8.0(44)",
|
||||
"version": "2.9.0(23)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "2.8.0(44)",
|
||||
"version": "2.9.0(23)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
@@ -16,6 +16,8 @@
|
||||
"@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",
|
||||
@@ -24,6 +26,7 @@
|
||||
"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",
|
||||
@@ -54,6 +57,7 @@
|
||||
"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",
|
||||
@@ -2848,6 +2852,30 @@
|
||||
"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,
|
||||
@@ -3501,6 +3529,12 @@
|
||||
"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,
|
||||
@@ -5529,7 +5563,6 @@
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
@@ -5537,6 +5570,11 @@
|
||||
"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,
|
||||
@@ -6642,8 +6680,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.1",
|
||||
"license": "MIT",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
@@ -8853,9 +8892,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
},
|
||||
@@ -11425,6 +11465,27 @@
|
||||
"@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,
|
||||
@@ -11786,6 +11847,12 @@
|
||||
"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
|
||||
@@ -13025,13 +13092,17 @@
|
||||
}
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"dev": true
|
||||
"version": "3.1.3"
|
||||
},
|
||||
"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,
|
||||
@@ -13688,7 +13759,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.1"
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
@@ -14981,7 +15054,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.8.0(44)",
|
||||
"version": "2.9.0(23)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"format": "prettier --write \"src/**/*.js\"",
|
||||
"format": "prettier --write \"src/**/*x.{ts,tsx,js,jsx}\"",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
|
||||
"clean": "rm -rf node_modules && rm -rf build"
|
||||
@@ -22,12 +22,15 @@
|
||||
"@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",
|
||||
@@ -60,6 +63,7 @@
|
||||
"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",
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
"live_view_help": "Hilfe zur Live-Ansicht",
|
||||
"memory": "Erinnerung",
|
||||
"memory_used": "Verwendeter Speicher",
|
||||
"missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu",
|
||||
"missing_board": "Analytics-Überwachung an diesem Ort ist nicht mehr aktiv. Klicken Sie hier, um die Überwachung neu zu starten",
|
||||
"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",
|
||||
@@ -91,6 +94,8 @@
|
||||
"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",
|
||||
@@ -175,6 +180,7 @@
|
||||
"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",
|
||||
@@ -391,6 +397,7 @@
|
||||
"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": {
|
||||
@@ -600,6 +607,7 @@
|
||||
"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",
|
||||
@@ -613,6 +621,7 @@
|
||||
"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",
|
||||
@@ -621,6 +630,8 @@
|
||||
"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",
|
||||
@@ -671,7 +682,15 @@
|
||||
"test_digicert_creds": "Anmeldeinformationen testen",
|
||||
"title": "Entitäten",
|
||||
"tree": "Entitätsbaum",
|
||||
"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."
|
||||
"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"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Unterstützt von",
|
||||
@@ -765,13 +784,17 @@
|
||||
"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"
|
||||
"to_claim": "Standorte zu beanspruchen",
|
||||
"view_gps": ""
|
||||
},
|
||||
"login": {
|
||||
"access_policy": "Zugangsrichtlinien",
|
||||
@@ -797,6 +820,7 @@
|
||||
"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!",
|
||||
@@ -902,7 +926,7 @@
|
||||
"dfs": "DFS-Überschreibung",
|
||||
"gw_commands": "Gateway-Befehle",
|
||||
"identifier": "Identifikator",
|
||||
"key_verification": "Überprüfung des Signaturschlüssels",
|
||||
"key_verification": "Signieren von Schlüsselinformationen",
|
||||
"restricted": "Beschränkt",
|
||||
"signed_upgrade": "Nur signiertes Upgrade",
|
||||
"title": "Beschränkungen",
|
||||
@@ -1022,6 +1046,7 @@
|
||||
},
|
||||
"system": {
|
||||
"backend_logs": "Back-End-Protokolle",
|
||||
"configuration": "Aufbau",
|
||||
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
|
||||
"endpoint": "Endpunkt",
|
||||
"hostname": "Hostname",
|
||||
@@ -1032,6 +1057,10 @@
|
||||
"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!",
|
||||
@@ -1053,9 +1082,11 @@
|
||||
"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": {
|
||||
@@ -1100,9 +1131,11 @@
|
||||
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
|
||||
"title": "Veranstaltungsorte",
|
||||
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
|
||||
"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware",
|
||||
"update_success": "Veranstaltungsort aktualisiert!",
|
||||
"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte",
|
||||
"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"
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
"live_view_help": "Live View Help",
|
||||
"memory": "Memory",
|
||||
"memory_used": "Memory Used",
|
||||
"missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu",
|
||||
"missing_board": "Analytics monitoring on this venue is no longer active. Click here to restart monitoring",
|
||||
"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",
|
||||
@@ -91,6 +94,8 @@
|
||||
"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",
|
||||
@@ -175,6 +180,7 @@
|
||||
"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",
|
||||
@@ -391,6 +397,7 @@
|
||||
"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": {
|
||||
@@ -600,6 +607,7 @@
|
||||
"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",
|
||||
@@ -613,6 +621,7 @@
|
||||
"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",
|
||||
@@ -621,6 +630,8 @@
|
||||
"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",
|
||||
@@ -671,7 +682,15 @@
|
||||
"test_digicert_creds": "Test Credentials",
|
||||
"title": "Entities",
|
||||
"tree": "Entity Tree",
|
||||
"venues_under_root": "Venues cannot be created directly under the root entity. Please create new entities and create venues under these."
|
||||
"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"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Powered By",
|
||||
@@ -765,13 +784,17 @@
|
||||
"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"
|
||||
"to_claim": "Locations to claim",
|
||||
"view_gps": "View GPS Location"
|
||||
},
|
||||
"login": {
|
||||
"access_policy": "Access Policy",
|
||||
@@ -797,6 +820,7 @@
|
||||
"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!",
|
||||
@@ -902,7 +926,7 @@
|
||||
"dfs": "DFS Override",
|
||||
"gw_commands": "Gateway Commands",
|
||||
"identifier": "Identifier",
|
||||
"key_verification": "Signing Key Verification",
|
||||
"key_verification": "Signing Key Information",
|
||||
"restricted": "Restricted",
|
||||
"signed_upgrade": "Signed Upgrade Only",
|
||||
"title": "Restrictions",
|
||||
@@ -1022,6 +1046,7 @@
|
||||
},
|
||||
"system": {
|
||||
"backend_logs": "Back-End Logs",
|
||||
"configuration": "Configuration",
|
||||
"could_not_retrieve": "Error: could not retrieve {{name}} system information",
|
||||
"endpoint": "Endpoint",
|
||||
"hostname": "Host Name",
|
||||
@@ -1032,6 +1057,10 @@
|
||||
"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!",
|
||||
@@ -1053,9 +1082,11 @@
|
||||
"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": {
|
||||
@@ -1100,9 +1131,11 @@
|
||||
"successfully_update_devices": "Updating {{num}} devices!",
|
||||
"title": "Venues",
|
||||
"update_all_devices": "Update All Device Configurations",
|
||||
"upgrade_all_devices": "Upgrade All Devices to Latest Firmware",
|
||||
"update_success": "Venue updated!",
|
||||
"upgrade_all_devices": "Upgrade All Devices 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"
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
"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, reinicie el monitoreo usando el menú superior",
|
||||
"missing_board": "El monitoreo analítico en este lugar ya no está activo. Haga clic aquí para reiniciar el monitoreo",
|
||||
"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",
|
||||
@@ -91,6 +94,8 @@
|
||||
"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",
|
||||
@@ -175,6 +180,7 @@
|
||||
"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",
|
||||
@@ -391,6 +397,7 @@
|
||||
"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": {
|
||||
@@ -600,6 +607,7 @@
|
||||
"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",
|
||||
@@ -613,6 +621,7 @@
|
||||
"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",
|
||||
@@ -621,6 +630,8 @@
|
||||
"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",
|
||||
@@ -671,7 +682,15 @@
|
||||
"test_digicert_creds": "Credenciales de prueba",
|
||||
"title": "entidades",
|
||||
"tree": "Árbol de entidades",
|
||||
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz. Cree nuevas entidades y cree lugares bajo estas."
|
||||
"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"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "energizado por",
|
||||
@@ -765,13 +784,17 @@
|
||||
"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"
|
||||
"to_claim": "Ubicaciones para reclamar",
|
||||
"view_gps": ""
|
||||
},
|
||||
"login": {
|
||||
"access_policy": "Política de acceso",
|
||||
@@ -797,6 +820,7 @@
|
||||
"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!",
|
||||
@@ -902,7 +926,7 @@
|
||||
"dfs": "Anulación de DFS",
|
||||
"gw_commands": "Comandos de puerta de enlace",
|
||||
"identifier": "Identificador",
|
||||
"key_verification": "Verificación de clave de firma",
|
||||
"key_verification": "Información clave de firma",
|
||||
"restricted": "Restringido",
|
||||
"signed_upgrade": "Solo actualización firmada",
|
||||
"title": "Las restricciones",
|
||||
@@ -1022,6 +1046,7 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1032,6 +1057,10 @@
|
||||
"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!",
|
||||
@@ -1053,9 +1082,11 @@
|
||||
"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": {
|
||||
@@ -1100,9 +1131,11 @@
|
||||
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
|
||||
"title": "Sedes",
|
||||
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
|
||||
"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente",
|
||||
"update_success": "Lugar actualizado!",
|
||||
"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos",
|
||||
"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"
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
"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 lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut",
|
||||
"missing_board": "La surveillance analytique sur ce site n'est plus active. Cliquez ici pour redémarrer la surveillance",
|
||||
"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",
|
||||
@@ -91,6 +94,8 @@
|
||||
"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",
|
||||
@@ -175,6 +180,7 @@
|
||||
"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",
|
||||
@@ -391,6 +397,7 @@
|
||||
"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": {
|
||||
@@ -600,6 +607,7 @@
|
||||
"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",
|
||||
@@ -613,6 +621,7 @@
|
||||
"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é",
|
||||
@@ -621,6 +630,8 @@
|
||||
"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",
|
||||
@@ -671,7 +682,15 @@
|
||||
"test_digicert_creds": "Tester les informations d'identification",
|
||||
"title": "Entités",
|
||||
"tree": "Arborescence des entités",
|
||||
"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."
|
||||
"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"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Alimenté par",
|
||||
@@ -765,13 +784,17 @@
|
||||
"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"
|
||||
"to_claim": "Emplacements à réclamer",
|
||||
"view_gps": ""
|
||||
},
|
||||
"login": {
|
||||
"access_policy": "Politique d'accès",
|
||||
@@ -797,6 +820,7 @@
|
||||
"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!",
|
||||
@@ -902,7 +926,7 @@
|
||||
"dfs": "Remplacement DFS",
|
||||
"gw_commands": "Commandes de passerelle",
|
||||
"identifier": "Identifiant",
|
||||
"key_verification": "Vérification de la clé de signature",
|
||||
"key_verification": "Signature des informations clés",
|
||||
"restricted": "Limité",
|
||||
"signed_upgrade": "Mise à niveau signée uniquement",
|
||||
"title": "Restrictions",
|
||||
@@ -1022,6 +1046,7 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1032,6 +1057,10 @@
|
||||
"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 !",
|
||||
@@ -1053,9 +1082,11 @@
|
||||
"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": {
|
||||
@@ -1100,9 +1131,11 @@
|
||||
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
|
||||
"title": "Les lieux",
|
||||
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
|
||||
"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel",
|
||||
"update_success": "Lieu mis à jour !",
|
||||
"upgrade_all_devices": "Mettre à niveau le micrologiciel de tous les appareils",
|
||||
"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"
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
"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, reinicie o monitoramento usando o menu superior",
|
||||
"missing_board": "O monitoramento analítico neste local não está mais ativo. Clique aqui para reiniciar o monitoramento",
|
||||
"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",
|
||||
@@ -91,6 +94,8 @@
|
||||
"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",
|
||||
@@ -175,6 +180,7 @@
|
||||
"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",
|
||||
@@ -391,6 +397,7 @@
|
||||
"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": {
|
||||
@@ -600,6 +607,7 @@
|
||||
"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",
|
||||
@@ -613,6 +621,7 @@
|
||||
"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",
|
||||
@@ -621,6 +630,8 @@
|
||||
"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",
|
||||
@@ -671,7 +682,15 @@
|
||||
"test_digicert_creds": "Credenciais de teste",
|
||||
"title": "Entidades",
|
||||
"tree": "Árvore de entidades",
|
||||
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz. Por favor, crie novas entidades e crie locais sob elas."
|
||||
"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"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Distribuído por",
|
||||
@@ -765,13 +784,17 @@
|
||||
"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"
|
||||
"to_claim": "Locais para reivindicar",
|
||||
"view_gps": ""
|
||||
},
|
||||
"login": {
|
||||
"access_policy": "Política de Acesso",
|
||||
@@ -797,6 +820,7 @@
|
||||
"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!",
|
||||
@@ -902,7 +926,7 @@
|
||||
"dfs": "Substituição DFS",
|
||||
"gw_commands": "Comandos de gateway",
|
||||
"identifier": "Identificador",
|
||||
"key_verification": "Verificação da chave de assinatura",
|
||||
"key_verification": "Informações Chave de Assinatura",
|
||||
"restricted": "Restrito",
|
||||
"signed_upgrade": "Somente atualização assinada",
|
||||
"title": "RESTRIÇÕES",
|
||||
@@ -1022,6 +1046,7 @@
|
||||
},
|
||||
"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",
|
||||
@@ -1032,6 +1057,10 @@
|
||||
"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!",
|
||||
@@ -1053,9 +1082,11 @@
|
||||
"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": {
|
||||
@@ -1100,9 +1131,11 @@
|
||||
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
|
||||
"title": "Locais",
|
||||
"update_all_devices": "Atualizar todas as configurações do dispositivo",
|
||||
"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente",
|
||||
"update_success": "Local atualizado!",
|
||||
"upgrade_all_devices": "Atualize o firmware de todos os dispositivos",
|
||||
"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"
|
||||
|
||||
@@ -12,7 +12,14 @@ export interface AlertButtonProps extends ThemeProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const _AlertButton: React.FC<AlertButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
||||
const _AlertButton: React.FC<AlertButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact = true,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
|
||||
@@ -11,7 +11,14 @@ export interface CreateButtonProps extends SpaceProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const _CreateButton: React.FC<CreateButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
||||
const _CreateButton: React.FC<CreateButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact = true,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const _DeleteButton: React.FC<DeleteButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
label,
|
||||
ml,
|
||||
...props
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip, useToast } from '@chakra-ui/react';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Portal,
|
||||
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';
|
||||
@@ -22,6 +32,7 @@ interface Props {
|
||||
onOpenConfigureModal: (serialNumber: string) => void;
|
||||
onOpenTelemetryModal: (serialNumber: string) => void;
|
||||
onOpenScriptModal: (device: GatewayDevice) => void;
|
||||
onOpenRebootModal: (serialNumber: string) => void;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
isCompact?: boolean;
|
||||
}
|
||||
@@ -38,8 +49,9 @@ const DeviceActionDropdown = ({
|
||||
onOpenTelemetryModal,
|
||||
onOpenConfigureModal,
|
||||
onOpenScriptModal,
|
||||
onOpenRebootModal,
|
||||
size,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
@@ -145,11 +157,13 @@ const DeviceActionDropdown = ({
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleConnectClick = () => getRtty();
|
||||
const handleRebootClick = () => onOpenRebootModal(device.serialNumber);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<Tooltip label={t('common.actions')}>
|
||||
{size === undefined || isCompact ? (
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
@@ -168,26 +182,28 @@ const DeviceActionDropdown = ({
|
||||
isDisabled={isDisabled}
|
||||
ml={2}
|
||||
>
|
||||
{t('commands.other')}
|
||||
{t('common.actions')}
|
||||
</MenuButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
|
||||
<MenuItem onClick={handleConnectClick}>{t('commands.connect')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</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>
|
||||
<MenuItem onClick={handleUpdateToLatest} hidden>
|
||||
{t('premium.toolbox.upgrade_to_latest')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
|
||||
</MenuList>
|
||||
<Portal>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
|
||||
<MenuItem onClick={handleConnectClick}>{t('commands.connect')}</MenuItem>
|
||||
<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>
|
||||
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
||||
<MenuItem onClick={handleUpdateToLatest} hidden>
|
||||
{t('premium.toolbox.upgrade_to_latest')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface EditButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -11,7 +12,15 @@ export interface EditButtonProps {
|
||||
ml?: string | number;
|
||||
}
|
||||
|
||||
const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, isLoading, isCompact, ...props }) => {
|
||||
const _EditButton: React.FC<EditButtonProps> = ({
|
||||
onClick,
|
||||
label,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact = true,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') {
|
||||
@@ -24,12 +33,12 @@ const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, is
|
||||
isDisabled={isDisabled}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
{label ?? t('common.edit')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<Tooltip label={label ?? t('common.edit')} hasArrow>
|
||||
<IconButton
|
||||
aria-label="edit"
|
||||
colorScheme="gray"
|
||||
|
||||
@@ -17,7 +17,7 @@ const _RefreshButton: React.FC<RefreshButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isFetching,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
ml,
|
||||
size,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const _ResponsiveButton: React.FC<ResponsiveButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
color,
|
||||
label,
|
||||
icon,
|
||||
|
||||
@@ -18,7 +18,7 @@ const _SaveButton: React.FC<SaveButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
isDirty,
|
||||
dirtyCheck,
|
||||
...props
|
||||
|
||||
@@ -20,7 +20,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
|
||||
isDirty,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
ml,
|
||||
...props
|
||||
}) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ const _WarningButton: React.FC<WarningButtonProps> = ({
|
||||
onClick,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const ColumnPicker = ({
|
||||
hiddenColumns,
|
||||
setHiddenColumns,
|
||||
size,
|
||||
isCompact,
|
||||
isCompact = true,
|
||||
}: ColumnPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { getPref, setPref } = useAuth();
|
||||
|
||||
@@ -44,15 +44,18 @@ const defaultProps = {
|
||||
sortBy: [],
|
||||
};
|
||||
|
||||
export type DataTableProps = {
|
||||
columns: readonly Column<object>[];
|
||||
data: object[];
|
||||
export type DataTableProps<TValue> = {
|
||||
columns: Column<TValue>[];
|
||||
data: TValue[];
|
||||
count?: number;
|
||||
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
||||
isLoading?: boolean;
|
||||
onRowClick?: (row: TValue) => void;
|
||||
isRowClickable?: (row: TValue) => boolean;
|
||||
obj?: string;
|
||||
sortBy?: { id: string; desc: boolean }[];
|
||||
hiddenColumns?: string[];
|
||||
hideEmptyListText?: boolean;
|
||||
hideControls?: boolean;
|
||||
minHeight?: string | number;
|
||||
fullScreen?: boolean;
|
||||
@@ -67,7 +70,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> &
|
||||
state: UsePaginationState<T>;
|
||||
};
|
||||
|
||||
const _DataTable = ({
|
||||
const _DataTable = <TValue extends object>({
|
||||
columns,
|
||||
data,
|
||||
isLoading,
|
||||
@@ -77,15 +80,19 @@ const _DataTable = ({
|
||||
sortBy,
|
||||
hiddenColumns,
|
||||
hideControls,
|
||||
hideEmptyListText,
|
||||
count,
|
||||
setPageInfo,
|
||||
isManual,
|
||||
saveSettingsId,
|
||||
showAllRows,
|
||||
}: DataTableProps) => {
|
||||
onRowClick,
|
||||
isRowClickable,
|
||||
}: DataTableProps<TValue>) => {
|
||||
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;
|
||||
@@ -140,8 +147,12 @@ const _DataTable = ({
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
) as TableInstanceWithHooks<object>;
|
||||
) as TableInstanceWithHooks<TValue>;
|
||||
|
||||
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));
|
||||
@@ -253,10 +264,19 @@ const _DataTable = ({
|
||||
</Thead>
|
||||
{data.length > 0 && (
|
||||
<Tbody {...getTableBodyProps()}>
|
||||
{page.map((row: Row) => {
|
||||
{page.map((row: Row<TValue>) => {
|
||||
prepareRow(row);
|
||||
const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true;
|
||||
const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined;
|
||||
return (
|
||||
<Tr {...row.getRowProps()} key={uuid()}>
|
||||
<Tr
|
||||
{...row.getRowProps()}
|
||||
key={uuid()}
|
||||
_hover={{
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
// @ts-ignore
|
||||
row.cells.map((cell) => (
|
||||
@@ -275,8 +295,26 @@ const _DataTable = ({
|
||||
fontSize="14px"
|
||||
// @ts-ignore
|
||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||
// @ts-ignore
|
||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
||||
fontFamily={
|
||||
// @ts-ignore
|
||||
cell.column.isMonospace
|
||||
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
// @ts-ignore
|
||||
cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick)
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
cursor={
|
||||
// @ts-ignore
|
||||
!cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick
|
||||
? 'pointer'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</Td>
|
||||
@@ -288,7 +326,7 @@ const _DataTable = ({
|
||||
</Tbody>
|
||||
)}
|
||||
</Table>
|
||||
{!isLoading && data.length === 0 && (
|
||||
{!isLoading && data.length === 0 && !hideEmptyListText && (
|
||||
<Center>
|
||||
{obj ? (
|
||||
<Heading size="md" pt={12}>
|
||||
@@ -309,7 +347,7 @@ const _DataTable = ({
|
||||
<Tooltip label={t('table.first_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to first page"
|
||||
onClick={() => gotoPage(0)}
|
||||
onClick={() => handleGoToPage(0)}
|
||||
isDisabled={!canPreviousPage}
|
||||
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||
mr={4}
|
||||
@@ -347,7 +385,7 @@ const _DataTable = ({
|
||||
max={pageOptions.length}
|
||||
onChange={(_: unknown, numberValue: number) => {
|
||||
const newPage = numberValue ? numberValue - 1 : 0;
|
||||
gotoPage(newPage);
|
||||
handleGoToPage(newPage);
|
||||
}}
|
||||
defaultValue={pageIndex + 1}
|
||||
>
|
||||
@@ -386,7 +424,7 @@ const _DataTable = ({
|
||||
<Tooltip label={t('table.last_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to last page"
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
onClick={() => handleGoToPage(pageCount - 1)}
|
||||
isDisabled={!canNextPage}
|
||||
icon={<ArrowRightIcon h={3} w={3} />}
|
||||
ml={4}
|
||||
@@ -401,4 +439,4 @@ const _DataTable = ({
|
||||
|
||||
_DataTable.defaultProps = defaultProps;
|
||||
|
||||
export const DataTable = React.memo(_DataTable);
|
||||
export const DataTable = React.memo(_DataTable) as unknown as typeof _DataTable;
|
||||
|
||||
@@ -100,6 +100,7 @@ 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;
|
||||
@@ -223,7 +224,13 @@ const SortableDataTable: React.FC<Props> = ({
|
||||
{page.map((row: Row) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<Tr {...row.getRowProps()} key={uuid()}>
|
||||
<Tr
|
||||
{...row.getRowProps()}
|
||||
key={uuid()}
|
||||
_hover={{
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
>
|
||||
{
|
||||
// @ts-ignore
|
||||
row.cells.map((cell) => (
|
||||
@@ -242,8 +249,12 @@ const SortableDataTable: React.FC<Props> = ({
|
||||
fontSize="14px"
|
||||
// @ts-ignore
|
||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||
// @ts-ignore
|
||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
||||
fontFamily={
|
||||
// @ts-ignore
|
||||
cell.column.isMonospace
|
||||
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</Td>
|
||||
|
||||
27
src/components/Maps/GoogleMap/Marker.tsx
Normal file
27
src/components/Maps/GoogleMap/Marker.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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);
|
||||
89
src/components/Maps/GoogleMap/index.tsx
Normal file
89
src/components/Maps/GoogleMap/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
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);
|
||||
@@ -5,17 +5,22 @@ import {
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Textarea,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { ClipboardText } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SaveButton } from '../../Buttons/SaveButton';
|
||||
import { Modal } from '../Modal';
|
||||
import { FileInputButton } from 'components/Buttons/FileInputButton';
|
||||
import { useConfigureDevice } from 'hooks/Network/Commands';
|
||||
import { useGetDevice } from 'hooks/Network/Devices';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
export type ConfigureModalProps = {
|
||||
serialNumber: string;
|
||||
@@ -29,11 +34,17 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const configure = useConfigureDevice({ serialNumber });
|
||||
const getDevice = useGetDevice({ serialNumber });
|
||||
|
||||
const [newConfig, setNewConfig] = React.useState('');
|
||||
|
||||
const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setNewConfig(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onImportConfiguration = () => {
|
||||
setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
|
||||
};
|
||||
const isValid = React.useMemo(() => {
|
||||
try {
|
||||
JSON.parse(newConfig);
|
||||
@@ -60,9 +71,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
modalProps.onClose();
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// console.log(e);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -79,10 +88,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||
{
|
||||
// @ts-ignore
|
||||
<AlertDescription>{configure.error?.response?.data?.ErrorDescription}</AlertDescription>
|
||||
}
|
||||
<AlertDescription>{(configure.error as AxiosError)?.response?.data?.ErrorDescription}</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -92,15 +98,25 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
</Alert>
|
||||
<FormControl isInvalid={!isValid && newConfig.length > 0}>
|
||||
<FormLabel>{t('configurations.one')}</FormLabel>
|
||||
<Box mb={2} w="240px">
|
||||
<FileInputButton
|
||||
value={newConfig}
|
||||
setValue={(v) => setNewConfig(v)}
|
||||
refreshId="1"
|
||||
accept=".json"
|
||||
isStringFile
|
||||
/>
|
||||
</Box>
|
||||
<Flex mb={2}>
|
||||
<Box w="240px">
|
||||
<FileInputButton
|
||||
value={newConfig}
|
||||
setValue={(v) => setNewConfig(v)}
|
||||
refreshId="1"
|
||||
accept=".json"
|
||||
isStringFile
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
rightIcon={<ClipboardText size={20} />}
|
||||
onClick={onImportConfiguration}
|
||||
hidden={!getDevice.data}
|
||||
ml={2}
|
||||
>
|
||||
Current Configuration
|
||||
</Button>
|
||||
</Flex>
|
||||
<Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} />
|
||||
<FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
@@ -57,7 +57,8 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
||||
upgrade({
|
||||
keepRedirector: isRedirector,
|
||||
uri,
|
||||
signature: device?.restrictedDevice ? ref.current?.values?.signature : undefined,
|
||||
signature:
|
||||
device?.restrictedDevice && !device?.restrictionDetails?.developer ? ref.current?.values?.signature : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -89,7 +90,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
||||
</FormLabel>
|
||||
<Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" />
|
||||
</FormControl>
|
||||
{device?.restrictedDevice && (
|
||||
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
|
||||
<Formik<{ signature?: string }>
|
||||
innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined}
|
||||
key={formKey}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Flex, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
||||
import { Flex, HStack, 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, right }) => (
|
||||
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => (
|
||||
<Header>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
||||
{title}
|
||||
<HStack spacing={2} ml={2}>
|
||||
{left ?? null}
|
||||
</HStack>
|
||||
<Spacer />
|
||||
{right}
|
||||
</Flex>
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ModalProps = {
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
topRightButtons?: React.ReactNode;
|
||||
tags?: React.ReactNode;
|
||||
options?: {
|
||||
modalSize?: 'sm' | 'md' | 'lg';
|
||||
maxWidth?: LayoutProps['maxWidth'];
|
||||
@@ -15,7 +16,7 @@ export type ModalProps = {
|
||||
children: React.ReactElement;
|
||||
};
|
||||
|
||||
const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => {
|
||||
const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, children }: ModalProps) => {
|
||||
const maxWidth = React.useMemo(() => {
|
||||
if (options?.maxWidth) return options.maxWidth;
|
||||
if (options?.modalSize === 'sm') return undefined;
|
||||
@@ -32,6 +33,7 @@ const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }:
|
||||
<ModalContent maxWidth={maxWidth}>
|
||||
<ModalHeader
|
||||
title={title}
|
||||
left={tags}
|
||||
right={
|
||||
<HStack spacing={2}>
|
||||
{topRightButtons}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { MenuItem, useToast } from '@chakra-ui/react';
|
||||
import { Alert, AlertIcon, Box, Button, Center, 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';
|
||||
|
||||
type Props = {
|
||||
device: GatewayDevice;
|
||||
refresh: () => void;
|
||||
export type RebootModalProps = {
|
||||
serialNumber: string;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
const RebootMenuItem = ({ device, refresh }: Props) => {
|
||||
export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
||||
const { mutateAsync: reboot } = useRebootDevice({ serialNumber: device.serialNumber });
|
||||
const { mutateAsync: reboot, isLoading } = useRebootDevice({ serialNumber });
|
||||
const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({
|
||||
objName: t('devices.one'),
|
||||
operationType: 'reboot',
|
||||
refresh: () => {
|
||||
refresh();
|
||||
addEventListeners([
|
||||
{
|
||||
id: `device-connection-${device.serialNumber}`,
|
||||
id: `device-connection-${serialNumber}`,
|
||||
type: 'DEVICE_CONNECTION',
|
||||
serialNumber: device.serialNumber,
|
||||
serialNumber,
|
||||
callback: () => {
|
||||
const id = `device-connection-notification-${device.serialNumber}`;
|
||||
const id = `device-connection-notification-${serialNumber}`;
|
||||
|
||||
if (!toast.isActive(id)) {
|
||||
toast({
|
||||
id,
|
||||
title: t('common.success'),
|
||||
description: t('controller.devices.finished_reboot', { serialNumber: device.serialNumber }),
|
||||
description: t('controller.devices.finished_reboot', { serialNumber }),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
@@ -44,17 +46,17 @@ const RebootMenuItem = ({ device, refresh }: Props) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: `device-disconnected-${device.serialNumber}`,
|
||||
id: `device-disconnected-${serialNumber}`,
|
||||
type: 'DEVICE_DISCONNECTION',
|
||||
serialNumber: device.serialNumber,
|
||||
serialNumber,
|
||||
callback: () => {
|
||||
const id = `device-disconnection-notification-${device.serialNumber}`;
|
||||
const id = `device-disconnection-notification-${serialNumber}`;
|
||||
|
||||
if (!toast.isActive(id)) {
|
||||
toast({
|
||||
id,
|
||||
title: t('common.success'),
|
||||
description: t('controller.devices.started_reboot', { serialNumber: device.serialNumber }),
|
||||
description: t('controller.devices.started_reboot', { serialNumber }),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
@@ -66,17 +68,39 @@ const RebootMenuItem = ({ device, refresh }: Props) => {
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRebootClick = () =>
|
||||
reboot(undefined, {
|
||||
onSuccess: () => {
|
||||
onRebootSuccess();
|
||||
modalProps.onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
onRebootError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
|
||||
return <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>;
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default RebootMenuItem;
|
||||
@@ -183,7 +183,9 @@ const CustomScriptForm = ({
|
||||
<>
|
||||
<Flex>
|
||||
<Box>
|
||||
{device?.restrictedDevice && <SignatureField name="signature" isDisabled={areFieldsDisabled} />}
|
||||
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
|
||||
<SignatureField name="signature" isDisabled={areFieldsDisabled} />
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<SelectField
|
||||
|
||||
@@ -60,8 +60,14 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
||||
let requestData: {
|
||||
[k: string]: unknown;
|
||||
serialNumber: string;
|
||||
script?: string;
|
||||
timeout?: number | undefined;
|
||||
} = data;
|
||||
|
||||
if (requestData.script) {
|
||||
requestData.script = btoa(requestData.script);
|
||||
}
|
||||
|
||||
if (selectedScript === 'diagnostics') {
|
||||
requestData = {
|
||||
serialNumber: device?.serialNumber ?? '',
|
||||
@@ -76,7 +82,7 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
||||
when: 0,
|
||||
deferred: data.deferred,
|
||||
timeout: data.timeout,
|
||||
signature: device?.restrictedDevice ? data.signature : undefined,
|
||||
signature: device?.restrictedDevice && !device?.restrictionDetails?.developer ? data.signature : undefined,
|
||||
uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined,
|
||||
scriptId: selectedScript,
|
||||
type: data.type,
|
||||
@@ -88,6 +94,19 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
||||
setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2));
|
||||
queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']);
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e) && e.response?.data?.ErrorDescription) {
|
||||
toast({
|
||||
id: 'script-update-error',
|
||||
title: t('common.error'),
|
||||
description: e.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
if (!waitForResponse) {
|
||||
toast({
|
||||
|
||||
@@ -57,7 +57,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
if (isOpen) resetData();
|
||||
}, [isOpen]);
|
||||
return (
|
||||
(<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
<ModalHeader
|
||||
@@ -66,7 +66,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
<>
|
||||
{csvData ? (
|
||||
// @ts-ignore
|
||||
(<CSVLink
|
||||
<CSVLink
|
||||
filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={csvData as object[]}
|
||||
>
|
||||
@@ -77,7 +77,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
label={t('common.download')}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</CSVLink>)
|
||||
</CSVLink>
|
||||
) : (
|
||||
<ResponsiveButton
|
||||
color="gray"
|
||||
@@ -118,6 +118,6 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
||||
confirm={closeCancelAndForm}
|
||||
cancel={closeConfirm}
|
||||
/>
|
||||
</Modal>)
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -149,6 +149,8 @@ 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,
|
||||
);
|
||||
|
||||
@@ -52,6 +52,8 @@ type ConnectionStatisticsMessage = {
|
||||
numberOfDevices: number;
|
||||
numberOfConnectingDevices: number;
|
||||
averageConnectedTime: number;
|
||||
tx: number;
|
||||
rx: number;
|
||||
};
|
||||
};
|
||||
serialNumbers?: undefined;
|
||||
@@ -85,6 +87,8 @@ export type SocketWebSocketNotificationData =
|
||||
numberOfDevices: number;
|
||||
numberOfConnectingDevices: number;
|
||||
averageConnectedTime: number;
|
||||
rx: number;
|
||||
tx: number;
|
||||
};
|
||||
serialNumber?: undefined;
|
||||
log?: undefined;
|
||||
|
||||
@@ -110,6 +110,18 @@ export const useDeleteCommand = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetSingleCommandHistory = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
||||
useQuery(
|
||||
['commands', serialNumber, commandId],
|
||||
() =>
|
||||
axiosGw
|
||||
.get(`command/${commandId}?serialNumber=${serialNumber}`)
|
||||
.then((response) => response.data as DeviceCommandHistory),
|
||||
{
|
||||
enabled: serialNumber !== undefined && serialNumber !== '' && commandId !== undefined && commandId !== '',
|
||||
},
|
||||
);
|
||||
|
||||
export type EventQueueResponse = {
|
||||
UUID: string;
|
||||
attachFile: number;
|
||||
@@ -245,6 +257,7 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
},
|
||||
onError: (e) => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
if (axios.isAxiosError(e)) {
|
||||
toast({
|
||||
id: 'script-error',
|
||||
@@ -263,14 +276,44 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const downloadScript = (serialNumber: string, commandId: string) =>
|
||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||
|
||||
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
||||
useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
||||
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
||||
enabled: false,
|
||||
onSuccess: (response) => {
|
||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = `Script_${commandId}.tar.gz`;
|
||||
const headerLine =
|
||||
(response.headers['content-disposition'] as string | undefined) ??
|
||||
(response.headers['content-disposition'] as string | undefined);
|
||||
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Script_${commandId}.tar.gz`;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const bufferResponse = e.response?.data;
|
||||
let errorMessage = '';
|
||||
// If the response is a buffer, parse to JSON object
|
||||
if (bufferResponse instanceof ArrayBuffer) {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||
errorMessage = json.ErrorDescription;
|
||||
}
|
||||
|
||||
toast({
|
||||
id: `script-download-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: errorMessage,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,8 +11,10 @@ export type DeviceLog = {
|
||||
severity: number;
|
||||
};
|
||||
|
||||
const getDeviceLogs = (limit: number, serialNumber?: string) => async () =>
|
||||
axiosGw.get(`device/${serialNumber}/logs?newest=true&limit=${limit}`).then((response) => response.data) as Promise<{
|
||||
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<{
|
||||
values: DeviceLog[];
|
||||
serialNumber: string;
|
||||
}>;
|
||||
@@ -21,20 +23,29 @@ export const useGetDeviceLogs = ({
|
||||
serialNumber,
|
||||
limit,
|
||||
onError,
|
||||
logType,
|
||||
}: {
|
||||
serialNumber?: string;
|
||||
limit: number;
|
||||
onError?: (e: AxiosError) => void;
|
||||
logType?: 0 | 1;
|
||||
}) =>
|
||||
useQuery(['devicelogs', serialNumber, { limit }], getDeviceLogs(limit, serialNumber), {
|
||||
useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), {
|
||||
keepPreviousData: true,
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
staleTime: 30000,
|
||||
onError,
|
||||
});
|
||||
|
||||
const deleteLogs = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) =>
|
||||
axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}`);
|
||||
const deleteLogs = async ({
|
||||
serialNumber,
|
||||
endDate,
|
||||
logType,
|
||||
}: {
|
||||
serialNumber: string;
|
||||
endDate: number;
|
||||
logType: 0 | 1;
|
||||
}) => axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}&logType=${logType}`);
|
||||
export const useDeleteLogs = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -45,46 +56,62 @@ export const useDeleteLogs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getLogsBatch = (serialNumber?: string, start?: number, end?: number, limit?: number, offset?: number) =>
|
||||
const getLogsBatch = (
|
||||
serialNumber?: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
logType?: 0 | 1,
|
||||
) =>
|
||||
axiosGw
|
||||
.get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}`)
|
||||
.get(
|
||||
`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}&logType=${logType}`,
|
||||
)
|
||||
.then((response) => response.data) as Promise<{
|
||||
values: DeviceLog[];
|
||||
serialNumber: string;
|
||||
}>;
|
||||
|
||||
const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () => {
|
||||
let offset = 0;
|
||||
const limit = 100;
|
||||
let logs: DeviceLog[] = [];
|
||||
let latestResponse: {
|
||||
values: DeviceLog[];
|
||||
serialNumber: string;
|
||||
const getDeviceLogsWithTimestamps =
|
||||
(serialNumber?: string, start?: number, end?: number, logType?: 0 | 1) => async () => {
|
||||
let offset = 0;
|
||||
const limit = 100;
|
||||
let logs: DeviceLog[] = [];
|
||||
let latestResponse: {
|
||||
values: DeviceLog[];
|
||||
serialNumber: string;
|
||||
};
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset, logType);
|
||||
logs = logs.concat(latestResponse.values);
|
||||
offset += limit;
|
||||
} while (latestResponse.values.length === limit);
|
||||
return {
|
||||
values: logs,
|
||||
};
|
||||
};
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
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 }], getDeviceLogsWithTimestamps(serialNumber, start, end), {
|
||||
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
|
||||
staleTime: 1000 * 60,
|
||||
onError,
|
||||
});
|
||||
useQuery(
|
||||
['devicelogs', serialNumber, { start, end, logType }],
|
||||
getDeviceLogsWithTimestamps(serialNumber, start, end, logType ?? 0),
|
||||
{
|
||||
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
|
||||
staleTime: 1000 * 60,
|
||||
onError,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -165,7 +165,10 @@ 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 }) => {
|
||||
|
||||
@@ -7,35 +7,49 @@ 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'],
|
||||
() =>
|
||||
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'))
|
||||
toast({
|
||||
id: 'firmware-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
obj: t('analytics.firmware'),
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
return useQuery(['get-device-profile'], () => getAllAvailableFirmware(deviceType), {
|
||||
enabled: deviceType !== '',
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('firmware-fetching-error'))
|
||||
toast({
|
||||
id: 'firmware-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
obj: t('analytics.firmware'),
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) =>
|
||||
@@ -56,7 +70,13 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe
|
||||
|
||||
return useMutation(
|
||||
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
|
||||
axiosGw.post(`device/${serialNumber}/upgrade`, { serialNumber, when: 0, keepRedirector, uri, signature }),
|
||||
axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
|
||||
serialNumber,
|
||||
when: 0,
|
||||
keepRedirector,
|
||||
uri,
|
||||
signature,
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@@ -222,3 +242,23 @@ 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']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
@@ -89,3 +89,18 @@ export const useDeleteHealthChecks = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getDevicesWithHealthBetween = (
|
||||
context: QueryFunctionContext<[string, string, { lowerLimit: number; upperLimit: number }]>,
|
||||
) =>
|
||||
axiosGw
|
||||
.get(`devices?health=true&lowLimit=${context.queryKey[2].lowerLimit}&highLimit=${context.queryKey[2].upperLimit}`)
|
||||
.then((res) => res.data.serialNumbers as string[]);
|
||||
|
||||
export const useGetDevicesWithHealthBetween = ({
|
||||
lowerLimit,
|
||||
upperLimit,
|
||||
}: {
|
||||
lowerLimit: number;
|
||||
upperLimit: number;
|
||||
}) => useQuery(['devices', 'health', { lowerLimit, upperLimit }], getDevicesWithHealthBetween);
|
||||
|
||||
83
src/hooks/Network/Secrets.ts
Normal file
83
src/hooks/Network/Secrets.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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']);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
@@ -21,6 +21,18 @@ 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;
|
||||
@@ -148,6 +160,11 @@ export type DeviceStatistics = {
|
||||
};
|
||||
};
|
||||
};
|
||||
gps?: {
|
||||
elevation: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
};
|
||||
version?: number;
|
||||
};
|
||||
const getLastStats = (serialNumber?: string) =>
|
||||
@@ -163,7 +180,7 @@ export const useGetDeviceLastStats = ({
|
||||
onError?: (e: AxiosError) => void;
|
||||
}) =>
|
||||
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
|
||||
enabled: serialNumber !== undefined && serialNumber !== '' && false,
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
staleTime: 1000 * 60,
|
||||
onError,
|
||||
});
|
||||
@@ -183,24 +200,12 @@ export const useGetDeviceNewestStats = ({
|
||||
serialNumber?: string;
|
||||
limit: number;
|
||||
onError?: (e: AxiosError) => void;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
|
||||
}) =>
|
||||
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<{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
|
||||
@@ -85,14 +86,44 @@ export const useTrace = ({ serialNumber, alertOnCompletion }: { serialNumber: st
|
||||
export const downloadTrace = (serialNumber: string, commandId: string) =>
|
||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||
|
||||
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
||||
useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
||||
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
||||
enabled: false,
|
||||
onSuccess: (response) => {
|
||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = `Trace_${commandId}.pcap`;
|
||||
const headerLine =
|
||||
(response.headers['content-disposition'] as string | undefined) ??
|
||||
(response.headers['content-disposition'] as string | undefined);
|
||||
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Trace_${commandId}.pcap`;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const bufferResponse = e.response?.data;
|
||||
let errorMessage = '';
|
||||
// If the response is a buffer, parse to JSON object
|
||||
if (bufferResponse instanceof ArrayBuffer) {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||
errorMessage = json.ErrorDescription;
|
||||
}
|
||||
|
||||
toast({
|
||||
id: `trace-download-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: errorMessage,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { axiosSec } from 'constants/axiosInstances';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
@@ -58,12 +58,24 @@ export type User = {
|
||||
waitingForEmailCheck: boolean;
|
||||
};
|
||||
|
||||
const getAvatarPromises = (userList: User[]) => {
|
||||
const getAvatarPromises = (userList: User[], queryClient: QueryClient) => {
|
||||
const promises = userList.map(async (user) => {
|
||||
if (user.avatar !== '' && user.avatar !== '0') {
|
||||
return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
// 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}`, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((response) => {
|
||||
queryClient.setQueryData(['avatar', user.id, user.avatar], response);
|
||||
return response;
|
||||
})
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return Promise.resolve('');
|
||||
});
|
||||
@@ -71,10 +83,35 @@ const getAvatarPromises = (userList: User[]) => {
|
||||
return promises;
|
||||
};
|
||||
|
||||
const getUsers = async () => {
|
||||
const users = await axiosSec.get('users').then(({ data }) => data.users as User[]);
|
||||
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 avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
|
||||
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) =>
|
||||
results.map((response) => {
|
||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||
const base64 = btoa(
|
||||
@@ -93,8 +130,10 @@ const getUsers = async () => {
|
||||
export const useGetUsers = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery(['users'], getUsers, {
|
||||
return useQuery(['users'], () => getUsers(queryClient), {
|
||||
staleTime: 30 * 1000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('users-fetching-error'))
|
||||
toast({
|
||||
@@ -118,7 +157,7 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-user', id],
|
||||
['users', id],
|
||||
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
||||
{
|
||||
enabled,
|
||||
@@ -173,16 +212,41 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useSuspendUser = ({ id }: { id: string }) =>
|
||||
useMutation((isSuspended: boolean) =>
|
||||
axiosSec.put(`user/${id}`, {
|
||||
suspended: isSuspended,
|
||||
}),
|
||||
);
|
||||
export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}));
|
||||
export const useSuspendUser = ({ id }: { id: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
export const useResetPassword = ({ id }: { id: string }) =>
|
||||
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
|
||||
return useMutation(
|
||||
(isSuspended: boolean) =>
|
||||
axiosSec.put(`user/${id}`, {
|
||||
suspended: isSuspended,
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
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']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
|
||||
export const useDeleteUser = () => {
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
CircularProgressLabel,
|
||||
Flex,
|
||||
Heading,
|
||||
Icon,
|
||||
Text,
|
||||
Tooltip,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowSquareDown, ArrowSquareUp, Clock } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
||||
import { bytesString } from 'helpers/stringHelper';
|
||||
import { useGetDevicesStats } from 'hooks/Network/Devices';
|
||||
|
||||
const SidebarDevices = () => {
|
||||
@@ -10,18 +23,19 @@ const SidebarDevices = () => {
|
||||
const [lastTime, setLastTime] = React.useState<Date | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
|
||||
|
||||
const getTime = () => {
|
||||
const time = React.useMemo(() => {
|
||||
if (lastTime === undefined || lastUpdate === undefined) return null;
|
||||
|
||||
const seconds = lastTime.getTime() - lastUpdate.getTime();
|
||||
|
||||
return Math.max(0, Math.floor(seconds / 1000));
|
||||
};
|
||||
}, [lastTime, lastUpdate]);
|
||||
|
||||
const refresh = () => {
|
||||
if (document.visibilityState !== 'hidden') {
|
||||
getStats.refetch();
|
||||
}
|
||||
const circleColor = () => {
|
||||
if (time === null) return 'gray.300';
|
||||
if (time < 10) return 'green.300';
|
||||
if (time < 30) return 'yellow.300';
|
||||
return 'red.300';
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -37,37 +51,60 @@ const SidebarDevices = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('visibilitychange', refresh);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', refresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!getStats.data) return null;
|
||||
|
||||
return (
|
||||
<VStack spacing={4}>
|
||||
<Flex flexDir="column" textAlign="center">
|
||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||
<Heading size="xs">
|
||||
{t('common.connected')} {t('devices.title')}
|
||||
</Heading>
|
||||
<Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400">
|
||||
({getStats.data.connectingDevices} {t('controller.devices.connecting')})
|
||||
</Heading>
|
||||
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
||||
<Heading size="md" textAlign="center" mt={2}>
|
||||
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
||||
<Card borderWidth="2px">
|
||||
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||
<CircularProgress
|
||||
isIndeterminate
|
||||
color={circleColor()}
|
||||
position="absolute"
|
||||
right="6px"
|
||||
top="6px"
|
||||
w="unset"
|
||||
size={6}
|
||||
thickness="14px"
|
||||
>
|
||||
<CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel>
|
||||
</CircularProgress>
|
||||
</Tooltip>
|
||||
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||
<Box position="absolute" right="8px" top="8px" w="unset" hidden>
|
||||
<Clock size={16} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<VStack mb={-1}>
|
||||
<Flex flexDir="column" textAlign="center">
|
||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||
<Heading size="xs" display="flex" justifyContent="center">
|
||||
<Text>
|
||||
{t('common.connected')} {t('devices.title')}{' '}
|
||||
</Text>{' '}
|
||||
</Heading>
|
||||
</Tooltip>
|
||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||
<Heading size="xs" mt={2} fontStyle="italic" fontWeight="normal" color="gray.400">
|
||||
{t('controller.stats.seconds_ago', { s: getTime() })}
|
||||
</Heading>
|
||||
</Flex>
|
||||
</VStack>
|
||||
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
||||
<Heading size="md" textAlign="center" mt={1}>
|
||||
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
||||
</Heading>
|
||||
</Tooltip>
|
||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||
<Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}>
|
||||
<Tooltip hasArrow label="Rx">
|
||||
<Flex alignItems="center" mr={1}>
|
||||
<Icon as={ArrowSquareUp} weight="bold" boxSize={5} mt="1px" color="blue.400" />{' '}
|
||||
{getStats.data.rx !== undefined ? bytesString(getStats.data.rx, 0) : '-'}
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Tooltip hasArrow label="Tx">
|
||||
<Flex alignItems="center">
|
||||
<Icon as={ArrowSquareDown} weight="bold" boxSize={5} mt="1px" color="purple.400" />{' '}
|
||||
{getStats.data.tx !== undefined ? bytesString(getStats.data.tx, 0) : '-'}
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface Column<T> {
|
||||
alwaysShow?: boolean;
|
||||
Footer?: string;
|
||||
accessor?: string;
|
||||
stopPropagation?: boolean;
|
||||
disableSortBy?: boolean;
|
||||
hasPopover?: boolean;
|
||||
customMaxWidth?: string;
|
||||
|
||||
@@ -99,13 +99,14 @@ const DefaultConfigurationsList = () => {
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<LoadingOverlay isLoading={getConfigs.isFetching}>
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
<DataTable<DefaultConfigurationResponse>
|
||||
columns={columns}
|
||||
saveSettingsId="firmware.table"
|
||||
data={getConfigs.data ?? []}
|
||||
obj={t('controller.configurations.title')}
|
||||
minHeight="200px"
|
||||
sortBy={[{ id: 'name', desc: true }]}
|
||||
onRowClick={onViewDetails}
|
||||
/>
|
||||
</LoadingOverlay>
|
||||
</Box>
|
||||
|
||||
75
src/pages/Device/LocationDisplayButton.tsx
Normal file
75
src/pages/Device/LocationDisplayButton.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
@@ -28,7 +28,7 @@ import { Modal } from 'components/Modals/Modal';
|
||||
import WifiScanResultDisplay from 'components/Modals/WifiScanModal/ResultDisplay';
|
||||
import { compactDate } from 'helpers/dateFormatting';
|
||||
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||
import { DeviceCommandHistory } from 'hooks/Network/Commands';
|
||||
import { DeviceCommandHistory, useGetSingleCommandHistory } from 'hooks/Network/Commands';
|
||||
import { WifiScanResult } from 'models/Device';
|
||||
|
||||
type Props = {
|
||||
@@ -39,9 +39,13 @@ type Props = {
|
||||
command?: DeviceCommandHistory;
|
||||
};
|
||||
|
||||
const CommandResultModal = ({ modalProps, command }: Props) => {
|
||||
const CommandResultModal = ({ modalProps, command: initialCommandInfo }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorMode } = useColorMode();
|
||||
const { data: command } = useGetSingleCommandHistory({
|
||||
commandId: initialCommandInfo?.UUID ?? '',
|
||||
serialNumber: initialCommandInfo?.serialNumber ?? '',
|
||||
});
|
||||
|
||||
if (!command) return null;
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ const CommandHistory = ({ serialNumber }: Props) => {
|
||||
<Box textAlign="right" display="flex">
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<ColumnPicker
|
||||
columns={columns as Column<unknown>[]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
preference="gateway.device.commandshistory.hiddenColumns"
|
||||
/>
|
||||
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<RefreshButton
|
||||
isCompact
|
||||
isFetching={getCommands.isFetching}
|
||||
|
||||
@@ -99,16 +99,6 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
|
||||
const actionCell = React.useCallback(
|
||||
(command: DeviceCommandHistory) => (
|
||||
<HStack>
|
||||
<Tooltip label={t('common.view_details')}>
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
onClick={onOpenDetails(command)}
|
||||
colorScheme="blue"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
size="sm"
|
||||
isLoading={loadingDeleteSerial === command.UUID}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('crud.delete')}>
|
||||
<IconButton
|
||||
aria-label={t('crud.delete')}
|
||||
@@ -119,6 +109,16 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
|
||||
isLoading={loadingDeleteSerial === command.UUID}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('common.view_details')}>
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
onClick={onOpenDetails(command)}
|
||||
colorScheme="blue"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
size="sm"
|
||||
isLoading={loadingDeleteSerial === command.UUID}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
),
|
||||
[loadingDeleteSerial],
|
||||
|
||||
94
src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx
Normal file
94
src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
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;
|
||||
@@ -16,8 +16,8 @@ const CustomInputButton = React.forwardRef(
|
||||
),
|
||||
);
|
||||
|
||||
type Props = { serialNumber: string };
|
||||
const DeleteLogModal = ({ serialNumber }: Props) => {
|
||||
type Props = { serialNumber: string; logType: 0 | 1 };
|
||||
const DeleteLogModal = ({ serialNumber, logType }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const modalProps = useDisclosure();
|
||||
@@ -26,7 +26,7 @@ const DeleteLogModal = ({ serialNumber }: Props) => {
|
||||
|
||||
const onDeleteClick = () => {
|
||||
deleteLogs.mutate(
|
||||
{ endDate: Math.floor(date.getTime() / 1000), serialNumber },
|
||||
{ endDate: Math.floor(date.getTime() / 1000), serialNumber, logType },
|
||||
{
|
||||
onSuccess: () => {
|
||||
modalProps.onClose();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
@@ -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 });
|
||||
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit, logType: 0 });
|
||||
|
||||
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} />
|
||||
<DeleteLogModal serialNumber={serialNumber} logType={0} />
|
||||
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { Box, IconButton, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import { MagnifyingGlass } from 'phosphor-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';
|
||||
@@ -8,18 +10,49 @@ import { Column } from 'models/Table';
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
limit: number;
|
||||
logType: 0 | 1;
|
||||
};
|
||||
|
||||
const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
|
||||
const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const getLogs = useGetDeviceLogs({ serialNumber, limit });
|
||||
const getLogs = useGetDeviceLogs({ serialNumber, limit, logType });
|
||||
const modalProps = useDisclosure();
|
||||
const [log, setLog] = React.useState<DeviceLog | undefined>();
|
||||
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>
|
||||
@@ -65,6 +98,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
|
||||
Footer: '',
|
||||
accessor: 'log',
|
||||
customWidth: '35px',
|
||||
Cell: (v) => logCell(v.cell.row.original),
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
@@ -85,6 +119,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
|
||||
getCustomLogs,
|
||||
time,
|
||||
setTime,
|
||||
modal: <DetailedLogViewModal modalProps={modalProps} log={log} />,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
|
||||
@@ -32,6 +33,9 @@ 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}>
|
||||
@@ -51,10 +55,12 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
|
||||
<TabPanel>
|
||||
<HealthCheckHistory serialNumber={serialNumber} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<LogHistory serialNumber={serialNumber} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<CrashLogs serialNumber={serialNumber} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
ListItem,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagLeftIcon,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnorderedList,
|
||||
} from '@chakra-ui/react';
|
||||
import { LockSimple, LockSimpleOpen } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
@@ -20,7 +31,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 };
|
||||
@@ -38,27 +49,52 @@ 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">{t('restrictions.title')}</Heading>
|
||||
<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}
|
||||
</CardHeader>
|
||||
<CardBody p={0} display="block">
|
||||
<Flex mt={2}>
|
||||
<Heading size="sm" mr={2}>
|
||||
<Heading size="sm" mr={2} my="auto">
|
||||
{t('restrictions.countries')}:
|
||||
</Heading>
|
||||
<Text>{restrictions.country.join(', ')}</Text>
|
||||
<Text my="auto">
|
||||
{restrictions.country?.length === 0 ? t('common.all') : restrictions.country.join(', ')}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Heading size="sm" mt={2}>
|
||||
{t('restrictions.key_verification')}
|
||||
</Heading>
|
||||
<UnorderedList>
|
||||
<Flex mt={2}>
|
||||
<Heading size="sm" mt={2} my="auto">
|
||||
{t('restrictions.key_verification')} {isMissingSigningInfo ? ':' : ''}
|
||||
</Heading>
|
||||
{isMissingSigningInfo ? (
|
||||
<Text my="auto" ml={2}>
|
||||
{t('common.none')}
|
||||
</Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
<UnorderedList hidden={isMissingSigningInfo}>
|
||||
<ListItem>
|
||||
{t('controller.wifi.vendor')}: {restrictions.key_info?.vendor}
|
||||
{t('controller.wifi.vendor')}:{' '}
|
||||
{restrictions.key_info?.vendor?.length > 0 ? restrictions.key_info?.vendor : '-'}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
{t('restrictions.algo')}: {restrictions.key_info?.algo}
|
||||
{t('restrictions.algo')}: {restrictions.key_info?.algo?.length > 0 ? restrictions.key_info?.algo : '-'}
|
||||
</ListItem>
|
||||
</UnorderedList>
|
||||
<Flex mt={2}>
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
ChartData,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler);
|
||||
|
||||
const getDivisionFactor = (maxBytes: number) => {
|
||||
if (maxBytes < 1024) {
|
||||
@@ -42,24 +44,30 @@ const InterfaceChart = ({ data }: Props) => {
|
||||
|
||||
const { factor, unit } = getDivisionFactor(data.maxTx);
|
||||
|
||||
const points = {
|
||||
const points: ChartData<'line', string[], string> = {
|
||||
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
|
||||
datasets: [
|
||||
{
|
||||
// Real 'Tx', but shown as 'Rx'
|
||||
label: 'Tx',
|
||||
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),
|
||||
borderColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
|
||||
backgroundColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
|
||||
},
|
||||
],
|
||||
{
|
||||
// Real 'Tx', but shown as 'Rx'
|
||||
label: 'Tx',
|
||||
data: data.rx.map((tx) => (Math.floor((tx / factor) * 100) / 100).toFixed(2)),
|
||||
borderColor: colorMode === 'light' ? 'rgba(99, 179, 237, 1)' : 'rgba(190, 227, 248, 1)', // blue-300 - blue-100
|
||||
backgroundColor: colorMode === 'light' ? 'rgba(99, 179, 237, 0.3)' : 'rgba(190, 227, 248, 0.3)', // blue-300 - blue-100
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: 'start',
|
||||
},
|
||||
{
|
||||
// Real 'Rx', but shown as 'Tx'
|
||||
label: 'Rx',
|
||||
data: data.tx.map((rx) => (Math.floor((rx / factor) * 100) / 100).toFixed(2)),
|
||||
borderColor: colorMode === 'light' ? 'rgba(72, 187, 120, 1)' : 'rgba(154, 230, 180, 1)', // green-400 - green-200
|
||||
backgroundColor: colorMode === 'light' ? 'rgba(72, 187, 120, 0.3)' : 'rgba(154, 230, 180, 0.3)', // green-400 - green-200
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: 'start',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,20 +34,29 @@ const DeviceMemoryChart = ({ data }: Props) => {
|
||||
{
|
||||
label: 'Free',
|
||||
data: data.free.map((free) => Math.floor(free / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
||||
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
||||
},
|
||||
{
|
||||
label: 'Buffered',
|
||||
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
|
||||
backgroundColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
|
||||
borderColor: colorMode === 'light' ? 'rgb(99, 179, 237, 1)' : 'rgb(190, 227, 248, 1)', // blue-300 - blue-100
|
||||
backgroundColor: colorMode === 'light' ? 'rgb(99, 179, 237, 0.3)' : 'rgb(190, 227, 248, 0.3)', // blue-300 - blue-100
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: '+1',
|
||||
},
|
||||
{
|
||||
label: 'Cached',
|
||||
data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
|
||||
backgroundColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
|
||||
borderColor: colorMode === 'light' ? 'rgb(237, 100, 166, 1)' : 'rgb(251, 182, 206, 1)', // pink-400 - pink-200
|
||||
backgroundColor: colorMode === 'light' ? 'rgb(237, 100, 166, 0.3)' : 'rgb(251, 182, 206, 0.3)', // pink-400 - pink-200
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: '+1',
|
||||
},
|
||||
{
|
||||
label: 'Buffered',
|
||||
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
|
||||
borderColor: colorMode === 'light' ? 'rgb(255, 240, 31, 1)' : 'rgb(250, 240, 137, 1)', // yellow-400 - yellow-200
|
||||
backgroundColor: colorMode === 'light' ? 'rgb(255, 240, 31, 0.3)' : 'rgb(250, 240, 137, 0.3)', // yellow-400 - yellow-200
|
||||
tension: 0.5,
|
||||
pointRadius: 0,
|
||||
fill: 'origin',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const ViewLastStatsModal = ({ serialNumber }: Props) => {
|
||||
if (getLastStats.data) {
|
||||
setValue(JSON.stringify(getLastStats.data, null, 2));
|
||||
}
|
||||
}, [getLastStats.data]);
|
||||
}, [getLastStats.data, isOpen]);
|
||||
return (
|
||||
<>
|
||||
<Tooltip label={t('statistics.last_stats')}>
|
||||
|
||||
@@ -52,8 +52,6 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
|
||||
<Heading size="md">{t('configurations.statistics')}</Heading>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<ViewLastStatsModal serialNumber={serialNumber} />
|
||||
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<Select value={selected} onChange={onSelectInterface}>
|
||||
{parsedData?.interfaces
|
||||
? Object.keys(parsedData.interfaces).map((v) => (
|
||||
@@ -64,6 +62,8 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
|
||||
: null}
|
||||
<option value="memory">{t('statistics.memory')}</option>
|
||||
</Select>
|
||||
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<ViewLastStatsModal serialNumber={serialNumber} />
|
||||
<RefreshButton
|
||||
size="sm"
|
||||
onClick={refresh}
|
||||
|
||||
@@ -16,6 +16,7 @@ type Props = {
|
||||
export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
const [selected, setSelected] = React.useState('memory');
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
const [hasSelectedNew, setHasSelectedNew] = React.useState(false);
|
||||
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
||||
const onProgressChange = React.useCallback((newProgress: number) => {
|
||||
setProgress(newProgress);
|
||||
@@ -29,13 +30,17 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
});
|
||||
|
||||
const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setHasSelectedNew(true);
|
||||
setSelected(event.target.value);
|
||||
};
|
||||
|
||||
const parsedData = React.useMemo(() => {
|
||||
if (!getStats.data && !getCustomStats.data) return undefined;
|
||||
|
||||
const data: Record<string, { tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number }> = {};
|
||||
const data: Record<
|
||||
string,
|
||||
{ tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number; removed?: boolean }
|
||||
> = {};
|
||||
const memoryData = {
|
||||
used: [] as number[],
|
||||
buffered: [] as number[],
|
||||
@@ -56,7 +61,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
if (index === 0) {
|
||||
let updated = false;
|
||||
for (const inter of stat.data.interfaces ?? []) {
|
||||
if (!updated && selected === 'memory') {
|
||||
if (!hasSelectedNew && !updated && selected === 'memory') {
|
||||
updated = true;
|
||||
setSelected(inter.name);
|
||||
}
|
||||
@@ -77,7 +82,10 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
let rx = inter.counters?.rx_bytes ?? 0;
|
||||
let tx = inter.counters?.tx_bytes ?? 0;
|
||||
|
||||
if (isInterUpstream) {
|
||||
if (inter['counters-aggregate']) {
|
||||
rx = inter['counters-aggregate'].rx_bytes;
|
||||
tx = inter['counters-aggregate'].tx_bytes;
|
||||
} else if (isInterUpstream) {
|
||||
for (const ssid of inter.ssids ?? []) {
|
||||
rx += ssid.counters?.rx_bytes ?? 0;
|
||||
tx += ssid.counters?.tx_bytes ?? 0;
|
||||
@@ -97,6 +105,18 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||
maxRx: rxDelta,
|
||||
};
|
||||
else {
|
||||
if (data[inter.name] && !data[inter.name]?.removed && data[inter.name]?.recorded.length === 1) {
|
||||
data[inter.name]?.tx.shift();
|
||||
data[inter.name]?.rx.shift();
|
||||
data[inter.name]?.recorded.shift();
|
||||
// @ts-ignore
|
||||
data[inter.name].maxRx = rxDelta;
|
||||
// @ts-ignore
|
||||
data[inter.name].maxTx = txDelta;
|
||||
// @ts-ignore
|
||||
data[inter.name].removed = true;
|
||||
}
|
||||
|
||||
data[inter.name]?.rx.push(rxDelta);
|
||||
data[inter.name]?.tx.push(txDelta);
|
||||
data[inter.name]?.recorded.push(stat.recorded);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react';
|
||||
import { Box, 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';
|
||||
@@ -90,11 +91,12 @@ 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>
|
||||
|
||||
@@ -9,12 +9,15 @@ import {
|
||||
Button,
|
||||
Center,
|
||||
Heading,
|
||||
IconButton,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
useClipboard,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { ListDashes } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
@@ -43,9 +46,15 @@ const ViewCapabilitiesModal = ({ serialNumber }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onOpen} colorScheme="pink" mr={2}>
|
||||
{t('controller.devices.capabilities')}
|
||||
</Button>
|
||||
<Tooltip label={t('controller.devices.capabilities')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('controller.devices.capabilities')}
|
||||
icon={<ListDashes size={20} />}
|
||||
onClick={onOpen}
|
||||
colorScheme="pink"
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={t('controller.devices.capabilities')}
|
||||
|
||||
@@ -7,11 +7,14 @@ import {
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useClipboard,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { Barcode } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { DeviceConfiguration } from 'models/Device';
|
||||
@@ -30,9 +33,14 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onOpen} isDisabled={!configuration} colorScheme="purple">
|
||||
{t('configurations.one')}
|
||||
</Button>
|
||||
<Tooltip label={t('configurations.one')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('configurations.one')}
|
||||
icon={<Barcode size={20} />}
|
||||
onClick={onOpen}
|
||||
colorScheme="purple"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={t('configurations.one')}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
Box,
|
||||
Button,
|
||||
Heading,
|
||||
HStack,
|
||||
Portal,
|
||||
@@ -12,10 +19,13 @@ import {
|
||||
useBreakpoint,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { Heart, HeartBreak, LockSimple, WifiHigh, WifiSlash } from 'phosphor-react';
|
||||
import axios from 'axios';
|
||||
import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Masonry from 'react-masonry-css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DeviceDetails from './Details';
|
||||
import DeviceLogsCard from './LogsCard';
|
||||
import DeviceNotes from './Notes';
|
||||
@@ -23,6 +33,7 @@ import RestrictionsCard from './RestrictionsCard';
|
||||
import DeviceStatisticsCard from './StatisticsCard';
|
||||
import DeviceSummary from './Summary';
|
||||
import WifiAnalysisCard from './WifiAnalysis';
|
||||
import { DeleteButton } from 'components/Buttons/DeleteButton';
|
||||
import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
@@ -33,11 +44,12 @@ 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';
|
||||
import { WifiScanModal } from 'components/Modals/WifiScanModal';
|
||||
import { useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices';
|
||||
import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices';
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
@@ -45,10 +57,17 @@ type Props = {
|
||||
|
||||
const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const breakpoint = useBreakpoint();
|
||||
const cancelRef = React.useRef(null);
|
||||
const navigate = useNavigate();
|
||||
const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({
|
||||
serialNumber,
|
||||
});
|
||||
const getDevice = useGetDevice({ serialNumber });
|
||||
const getStatus = useGetDeviceStatus({ serialNumber });
|
||||
const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 });
|
||||
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
|
||||
const scanModalProps = useDisclosure();
|
||||
const resetModalProps = useDisclosure();
|
||||
const eventQueueProps = useDisclosure();
|
||||
@@ -56,7 +75,40 @@ 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 handleDeleteClick = () =>
|
||||
deleteDevice(serialNumber, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `delete-device-success-${serialNumber}`,
|
||||
title: t('common.success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
navigate('/devices');
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
toast({
|
||||
id: `delete-device-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: e.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const connectedTag = React.useMemo(() => {
|
||||
if (!getStatus.data) return null;
|
||||
|
||||
@@ -100,9 +152,28 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
);
|
||||
}, [getStatus.data, getHealth.data]);
|
||||
|
||||
// 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 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]);
|
||||
|
||||
const refresh = () => {
|
||||
getDevice.refetch();
|
||||
@@ -119,16 +190,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
<Heading size="md">{serialNumber}</Heading>
|
||||
{connectedTag}
|
||||
{healthTag}
|
||||
{getDevice.data?.restrictedDevice && (
|
||||
<Tag size="lg" colorScheme="gray">
|
||||
<TagLeftIcon boxSize="18px" as={LockSimple} />
|
||||
<TagLabel>{t('devices.restricted')}</TagLabel>
|
||||
</Tag>
|
||||
)}
|
||||
{restrictedTag}
|
||||
</HStack>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />}
|
||||
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||
{getDevice?.data && (
|
||||
<DeviceActionDropdown
|
||||
// @ts-ignore
|
||||
@@ -142,6 +209,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
onOpenConfigureModal={configureModalProps.onOpen}
|
||||
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
||||
onOpenScriptModal={scriptModal.openModal}
|
||||
onOpenRebootModal={rebootModalProps.onOpen}
|
||||
size="md"
|
||||
isCompact
|
||||
/>
|
||||
@@ -172,16 +240,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
<Heading size="md">{serialNumber}</Heading>
|
||||
{connectedTag}
|
||||
{healthTag}
|
||||
{getDevice.data?.restrictedDevice && (
|
||||
<Tag size="lg" colorScheme="gray">
|
||||
<TagLeftIcon boxSize="18px" as={LockSimple} />
|
||||
<TagLabel>{t('devices.restricted')}</TagLabel>
|
||||
</Tag>
|
||||
)}
|
||||
{restrictedTag}
|
||||
</HStack>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<DeviceSearchBar />
|
||||
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||
{getDevice?.data && (
|
||||
<DeviceActionDropdown
|
||||
// @ts-ignore
|
||||
@@ -194,8 +258,10 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
onOpenEventQueue={eventQueueProps.onOpen}
|
||||
onOpenConfigureModal={configureModalProps.onOpen}
|
||||
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
||||
onOpenRebootModal={rebootModalProps.onOpen}
|
||||
onOpenScriptModal={scriptModal.openModal}
|
||||
size="md"
|
||||
isCompact
|
||||
/>
|
||||
)}
|
||||
<RefreshButton
|
||||
@@ -210,6 +276,24 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
</Card>
|
||||
</Portal>
|
||||
)}
|
||||
<AlertDialog isOpen={isDeleteOpen} leastDestructiveRef={cancelRef} onClose={onDeleteClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
{t('crud.delete')} {serialNumber}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button colorScheme="gray" mr="1" onClick={onDeleteClose} ref={cancelRef}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
|
||||
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
|
||||
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />
|
||||
@@ -217,6 +301,7 @@ 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
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Input,
|
||||
Textarea,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { 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>('');
|
||||
@@ -43,41 +32,50 @@ 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;
|
||||
|
||||
React.useEffect(() => {
|
||||
const onOpen = () => {
|
||||
setSerialNumber('');
|
||||
setReason('');
|
||||
}, [modalProps.isOpen]);
|
||||
modalProps.onOpen();
|
||||
setTimeout(() => {
|
||||
initialRef.current?.focus();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateButton onClick={modalProps.onOpen} isCompact ml={2} />
|
||||
<CreateButton onClick={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" />
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setSerialNumber(e.target.value)}
|
||||
value={serialNumber}
|
||||
w="140px"
|
||||
ref={initialRef}
|
||||
/>
|
||||
<FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { useColorMode } from '@chakra-ui/react';
|
||||
import { CopyIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
useColorMode,
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Center,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
ListItem,
|
||||
Spinner,
|
||||
UnorderedList,
|
||||
useClipboard,
|
||||
Tooltip as ChakraTooltip,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
@@ -11,20 +28,57 @@ import {
|
||||
Legend,
|
||||
ChartData,
|
||||
ArcElement,
|
||||
ChartTypeRegistry,
|
||||
ScatterDataPoint,
|
||||
BubbleDataPoint,
|
||||
} from 'chart.js';
|
||||
import { Pie } from 'react-chartjs-2';
|
||||
import { Pie, getElementAtEvent } from 'react-chartjs-2';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import GraphStatDisplay from 'components/Containers/GraphStatDisplay';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { ControllerDashboardHealth } from 'hooks/Network/Controller';
|
||||
import { useGetDevicesWithHealthBetween } from 'hooks/Network/HealthChecks';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
const LABEL_TO_LIMITS = {
|
||||
'100%': { lowerLimit: 100, upperLimit: 100, label: 'With 100% Health' },
|
||||
'>90%': { lowerLimit: 90, upperLimit: 99, label: 'Between 90% and 99%' },
|
||||
'>60%': { lowerLimit: 60, upperLimit: 89, label: 'Between 60% and 89%' },
|
||||
'<=60%': { lowerLimit: 0, upperLimit: 59, label: 'Between 0% and 59%' },
|
||||
} as const;
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement);
|
||||
|
||||
type Props = {
|
||||
data: ControllerDashboardHealth[];
|
||||
};
|
||||
|
||||
const OverallHealthPieChart = ({ data }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorMode } = useColorMode();
|
||||
const { hasCopied, onCopy, setValue } = useClipboard('');
|
||||
const modalProps = useDisclosure();
|
||||
const [deviceCategory, setDeviceCategory] = React.useState<{ lowerLimit: number; upperLimit: number; label: string }>(
|
||||
LABEL_TO_LIMITS['100%'],
|
||||
);
|
||||
const serialNumbersFromCategory = useGetDevicesWithHealthBetween(deviceCategory);
|
||||
const chartRef =
|
||||
React.useRef<ChartJS<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown>>(
|
||||
null,
|
||||
);
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (chartRef.current) {
|
||||
const element = getElementAtEvent(chartRef.current, event)?.[0];
|
||||
if (element && element.index !== undefined) {
|
||||
const label = chartRef.current?.data?.labels?.[element.index] as keyof typeof LABEL_TO_LIMITS | undefined;
|
||||
if (label && LABEL_TO_LIMITS[label]) {
|
||||
setDeviceCategory(LABEL_TO_LIMITS[label]);
|
||||
modalProps.onOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => {
|
||||
const totalDevices = data.reduce(
|
||||
@@ -85,7 +139,7 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
||||
}
|
||||
if (totalDevices['<60%'] > 0) {
|
||||
newData.push(totalDevices['<60%']);
|
||||
labels.push('<60%');
|
||||
labels.push('<=60%');
|
||||
const color = colorMode === 'light' ? '#FC8181' : '#FC8181';
|
||||
backgroundColor.push(color);
|
||||
borderColor.push(color);
|
||||
@@ -105,38 +159,108 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
||||
};
|
||||
}, [data, colorMode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (serialNumbersFromCategory.data) setValue(serialNumbersFromCategory.data.join(','));
|
||||
}, [serialNumbersFromCategory.data]);
|
||||
|
||||
return (
|
||||
<GraphStatDisplay
|
||||
title={t('controller.dashboard.overall_health')}
|
||||
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
||||
chart={
|
||||
<Pie
|
||||
data={parsedData}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
labels: {
|
||||
color: colorMode === 'dark' ? 'white' : undefined,
|
||||
<>
|
||||
<GraphStatDisplay
|
||||
title={t('controller.dashboard.overall_health')}
|
||||
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
||||
chart={
|
||||
<Pie
|
||||
// @ts-ignore
|
||||
ref={chartRef}
|
||||
data={parsedData}
|
||||
onClick={onClick}
|
||||
options={{
|
||||
onHover: (e, elements) => {
|
||||
const element = e.native?.target as unknown as { style: { cursor: string } };
|
||||
if (element && elements.length > 0) {
|
||||
element.style.cursor = 'pointer';
|
||||
} else if (element) {
|
||||
element.style.cursor = 'default';
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
labels: {
|
||||
color: colorMode === 'dark' ? 'white' : undefined,
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.label}: ${context.formattedValue} (${Math.round(
|
||||
// @ts-ignore
|
||||
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
|
||||
)}%)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) =>
|
||||
`${context.label}: ${context.formattedValue} (${Math.round(
|
||||
// @ts-ignore
|
||||
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
|
||||
)}%)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={t('controller.dashboard.overall_health')}
|
||||
{...modalProps}
|
||||
options={{
|
||||
modalSize: 'sm',
|
||||
}}
|
||||
topRightButtons={
|
||||
<ChakraTooltip label={hasCopied ? `${t('common.copied')}!` : t('common.copy')} hasArrow closeOnClick={false}>
|
||||
<IconButton
|
||||
aria-label={t('common.copy')}
|
||||
icon={<CopyIcon h={5} w={5} />}
|
||||
onClick={onCopy}
|
||||
colorScheme="teal"
|
||||
hidden={!serialNumbersFromCategory.data || serialNumbersFromCategory.data.length === 0}
|
||||
/>
|
||||
</ChakraTooltip>
|
||||
}
|
||||
>
|
||||
{serialNumbersFromCategory.isFetching ? (
|
||||
<Center my={8}>
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
) : (
|
||||
<Box>
|
||||
{serialNumbersFromCategory.error ? (
|
||||
<Alert mb={4} status="error">
|
||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{(serialNumbersFromCategory.error as AxiosError).response?.data.ErrorDescription}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{serialNumbersFromCategory.data ? (
|
||||
<Box>
|
||||
<Heading size="md" mb={4}>
|
||||
{serialNumbersFromCategory.data.length} {t('devices.title')} {deviceCategory.label}
|
||||
</Heading>
|
||||
<Box maxH="70vh" overflowY="auto" overflowX="hidden">
|
||||
<UnorderedList pl={2}>
|
||||
{serialNumbersFromCategory.data
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((device) => (
|
||||
<ListItem key={device} fontFamily="mono">
|
||||
<Link href={`#/devices/${device}`}>{device}</Link>
|
||||
</ListItem>
|
||||
))}
|
||||
</UnorderedList>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ interface Props {
|
||||
onOpenConfigureModal: (serialNumber: string) => void;
|
||||
onOpenTelemetryModal: (serialNumber: string) => void;
|
||||
onOpenScriptModal: (device: GatewayDevice) => void;
|
||||
onOpenRebootModal: (serialNumber: string) => void;
|
||||
}
|
||||
|
||||
const Actions: React.FC<Props> = ({
|
||||
@@ -47,6 +48,7 @@ const Actions: React.FC<Props> = ({
|
||||
onOpenConfigureModal,
|
||||
onOpenTelemetryModal,
|
||||
onOpenScriptModal,
|
||||
onOpenRebootModal,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
@@ -102,6 +104,7 @@ 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}`}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Heading, Image, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { LockSimple } from 'phosphor-react';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -21,6 +21,7 @@ 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';
|
||||
@@ -60,6 +61,7 @@ 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 +100,9 @@ const DeviceListCard = () => {
|
||||
setSerialNumber(serial);
|
||||
configureModalProps.onOpen();
|
||||
};
|
||||
|
||||
const goToSerial = (serial: string) => () => {
|
||||
navigate(`/devices/${serial}`);
|
||||
const onOpenReboot = (serial: string) => {
|
||||
setSerialNumber(serial);
|
||||
rebootModalProps.onOpen();
|
||||
};
|
||||
|
||||
const badgeCell = React.useCallback(
|
||||
@@ -160,9 +162,9 @@ const DeviceListCard = () => {
|
||||
|
||||
const serialCell = React.useCallback(
|
||||
(device: DeviceWithStatus) => (
|
||||
<Button variant="link" onClick={goToSerial(device.serialNumber)} fontSize="sm">
|
||||
<Link href={`#/devices/${device.serialNumber}`} fontSize="sm" my="auto" pt={1}>
|
||||
<pre>{device.serialNumber}</pre>
|
||||
</Button>
|
||||
</Link>
|
||||
),
|
||||
[],
|
||||
);
|
||||
@@ -216,6 +218,7 @@ const DeviceListCard = () => {
|
||||
onOpenConfigureModal={onOpenConfigure}
|
||||
onOpenTelemetryModal={onOpenTelemetry}
|
||||
onOpenScriptModal={scriptModal.openModal}
|
||||
onOpenRebootModal={onOpenReboot}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
@@ -251,6 +254,7 @@ const DeviceListCard = () => {
|
||||
Footer: '',
|
||||
accessor: 'firmware',
|
||||
Cell: (v) => firmwareCell(v.cell.row.original),
|
||||
stopPropagation: true,
|
||||
customWidth: '50px',
|
||||
disableSortBy: true,
|
||||
},
|
||||
@@ -388,7 +392,7 @@ const DeviceListCard = () => {
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable
|
||||
<DataTable<DeviceWithStatus>
|
||||
columns={
|
||||
columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as {
|
||||
id: string;
|
||||
@@ -406,7 +410,8 @@ const DeviceListCard = () => {
|
||||
// @ts-ignore
|
||||
setPageInfo={setPageInfo}
|
||||
saveSettingsId="gateway.devices.table"
|
||||
minHeight="600px"
|
||||
onRowClick={(device) => navigate(`devices/${device.serialNumber}`)}
|
||||
isRowClickable={() => true}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
@@ -417,6 +422,7 @@ const DeviceListCard = () => {
|
||||
<EventQueueModal modalProps={eventQueueProps} serialNumber={serialNumber} />
|
||||
<ConfigureModal modalProps={configureModalProps} serialNumber={serialNumber} />
|
||||
<TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} />
|
||||
<RebootModal modalProps={rebootModalProps} serialNumber={serialNumber} />
|
||||
{scriptModal.modal}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -191,6 +191,7 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
|
||||
ml={2}
|
||||
/>
|
||||
{isEditingDescription && (
|
||||
// @ts-ignore
|
||||
<SaveButton onClick={onSaveDescription} ml={2} isCompact size="sm" isLoading={updateFirmware.isLoading} />
|
||||
)}
|
||||
</FormLabel>
|
||||
@@ -202,48 +203,51 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
|
||||
isDisabled={!isEditingDescription}
|
||||
/>
|
||||
</FormControl>
|
||||
<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} />}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton alignContent="center" mt={1} />
|
||||
<PopoverHeader display="flex">{t('profile.add_new_note')}</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Box>
|
||||
<Textarea h="100px" placeholder="Your new note" value={newNote} onChange={onNoteChange} />
|
||||
</Box>
|
||||
<Center mt={2}>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
isDisabled={newNote.length === 0}
|
||||
onClick={onNoteSubmit(onClose)}
|
||||
isLoading={updateFirmware.isLoading}
|
||||
>
|
||||
{t('crud.add')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</FormLabel>
|
||||
<Box overflowX="auto" overflowY="auto" maxH="400px">
|
||||
<DataTable columns={columns as Column<object>[]} data={notes} obj={t('common.notes')} minHeight="200px" />
|
||||
</Box>
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
<FormControl mt={2}>
|
||||
<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} />} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton alignContent="center" mt={1} />
|
||||
<PopoverHeader display="flex">{t('profile.add_new_note')}</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Box>
|
||||
<Textarea h="100px" placeholder="Your new note" value={newNote} onChange={onNoteChange} />
|
||||
</Box>
|
||||
<Center mt={2}>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
isDisabled={newNote.length === 0}
|
||||
onClick={onNoteSubmit(onClose)}
|
||||
isLoading={updateFirmware.isLoading}
|
||||
>
|
||||
{t('crud.add')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
108
src/pages/Firmware/List/UpdateDbButton.tsx
Normal file
108
src/pages/Firmware/List/UpdateDbButton.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
IconButton,
|
||||
Tag,
|
||||
TagLabel,
|
||||
Text,
|
||||
Tooltip,
|
||||
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 (
|
||||
<>
|
||||
<Tooltip label={t('firmware.last_db_update_title')}>
|
||||
<IconButton
|
||||
aria-label={t('firmware.last_db_update_title')}
|
||||
colorScheme="teal"
|
||||
icon={<Database size={20} />}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
</Tooltip>
|
||||
<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;
|
||||
@@ -11,7 +11,15 @@ const UriCell = ({ uri }: Props) => {
|
||||
|
||||
return (
|
||||
<Box display="flex">
|
||||
<Button onClick={copy.onCopy} size="xs" colorScheme="teal" mr={2}>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
copy.onCopy();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
size="xs"
|
||||
colorScheme="teal"
|
||||
mr={2}
|
||||
>
|
||||
{copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')}
|
||||
</Button>
|
||||
<Text my="auto">{uri}</Text>
|
||||
|
||||
@@ -16,6 +16,7 @@ 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';
|
||||
@@ -143,6 +144,7 @@ const FirmwareListTable = () => {
|
||||
</Box>
|
||||
<Text>{t('controller.firmware.show_dev_releases')}</Text>
|
||||
<Switch isChecked={showDevFirmware} onChange={toggle} size="lg" />
|
||||
<UpdateDbButton />
|
||||
<RefreshButton
|
||||
onClick={() => {
|
||||
getDeviceTypes.refetch();
|
||||
@@ -156,13 +158,14 @@ const FirmwareListTable = () => {
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}>
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
<DataTable<Firmware>
|
||||
columns={columns}
|
||||
saveSettingsId="firmware.table"
|
||||
data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []}
|
||||
obj={t('analytics.firmware')}
|
||||
minHeight="200px"
|
||||
sortBy={[{ id: 'imageDate', desc: true }]}
|
||||
onRowClick={(firmw) => handleViewDetailsClick(firmw)()}
|
||||
/>
|
||||
</LoadingOverlay>
|
||||
</Box>
|
||||
|
||||
@@ -48,7 +48,10 @@ const _LoginForm: React.FC<_LoginFormProps> = ({ setActiveForm }) => {
|
||||
const displayError = useMemo(() => {
|
||||
const loginError: AxiosError = error as AxiosError;
|
||||
|
||||
if (loginError?.response?.data?.ErrorCode === 4) return t('login.waiting_for_email_verification');
|
||||
if (loginError?.response?.data?.ErrorCode === 5) return t('login.waiting_for_email_verification');
|
||||
if (loginError?.response?.data?.ErrorCode === 15) {
|
||||
return t('login.suspended_error');
|
||||
}
|
||||
return t('login.invalid_credentials');
|
||||
}, [t, error]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { Box, Flex, HStack, IconButton, Select, Spacer, Table, Text, Th, Thead, Tooltip, Tr } from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -127,9 +127,9 @@ const LogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -128,9 +142,9 @@ const FmsLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -128,9 +142,9 @@ const GeneralLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -128,9 +142,9 @@ const SecLogsCard = () => {
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import ApiKeyTable from './Table';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
@@ -10,7 +11,9 @@ const ApiKeysCard = () => {
|
||||
return (
|
||||
<Card p={4}>
|
||||
<CardBody>
|
||||
<ApiKeyTable userId={user?.id ?? ''} />
|
||||
<Box w="100%">
|
||||
<ApiKeyTable userId={user?.id ?? ''} />
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ScriptTableActions from './Actions';
|
||||
import CreateScriptButton from './CreateButton';
|
||||
import useScriptsTable from './useScriptsTable';
|
||||
@@ -21,6 +22,7 @@ type Props = {
|
||||
const ScriptTableCard = ({ onIdSelect }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { query, hiddenColumns } = useScriptsTable();
|
||||
const { id } = useParams();
|
||||
|
||||
const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []);
|
||||
const actionCell = React.useCallback(
|
||||
@@ -108,8 +110,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box w="100%" h="300px" overflowY="auto">
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
<DataTable<Script>
|
||||
columns={columns}
|
||||
saveSettingsId="apiKeys.profile.table"
|
||||
data={query.data ?? []}
|
||||
obj={t('script.other')}
|
||||
@@ -118,6 +120,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
|
||||
hiddenColumns={hiddenColumns[0]}
|
||||
showAllRows
|
||||
hideControls
|
||||
onRowClick={(script) => onIdSelect(script.id)}
|
||||
isRowClickable={(script) => script.id !== id}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
139
src/pages/SystemPage/SystemSecrets/Actions.tsx
Normal file
139
src/pages/SystemPage/SystemSecrets/Actions.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
118
src/pages/SystemPage/SystemSecrets/CreateButton.tsx
Normal file
118
src/pages/SystemPage/SystemSecrets/CreateButton.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
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;
|
||||
146
src/pages/SystemPage/SystemSecrets/EditButton.tsx
Normal file
146
src/pages/SystemPage/SystemSecrets/EditButton.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
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;
|
||||
70
src/pages/SystemPage/SystemSecrets/Table.tsx
Normal file
70
src/pages/SystemPage/SystemSecrets/Table.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
53
src/pages/SystemPage/SystemSecrets/index.tsx
Normal file
53
src/pages/SystemPage/SystemSecrets/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Article } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemLoggingModal from './Modal';
|
||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||
@@ -15,9 +16,17 @@ const SystemLoggingButton = ({ endpoint, token }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto">
|
||||
{t('system.logging')}
|
||||
</Button>
|
||||
<Tooltip label={t('system.logging')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('system.logging')}
|
||||
colorScheme="teal"
|
||||
type="button"
|
||||
my="auto"
|
||||
onClick={modalProps.onOpen}
|
||||
icon={<Article size={20} />}
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -19,11 +19,12 @@ 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 { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
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,21 +66,12 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<Card variant="widget">
|
||||
<Box display="flex" mb={2}>
|
||||
<Heading pt={0}>{endpoint.type}</Heading>
|
||||
<Spacer />
|
||||
<SystemLoggingButton endpoint={endpoint} token={token} />
|
||||
<Button
|
||||
mt={1}
|
||||
minWidth="112px"
|
||||
colorScheme="gray"
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
onClick={refresh}
|
||||
isLoading={isFetchingSystem || isFetchingSubsystems}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} />
|
||||
</Box>
|
||||
<CardBody>
|
||||
<VStack w="100%">
|
||||
@@ -179,7 +171,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
||||
ml={2}
|
||||
onClick={handleReloadClick}
|
||||
icon={<ArrowsClockwise size={20} />}
|
||||
colorScheme="gray"
|
||||
colorScheme="blue"
|
||||
isLoading={isReloading}
|
||||
isDisabled={subs.length === 0}
|
||||
/>
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
||||
import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } 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 SystemPage = () => {
|
||||
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 { t } = useTranslation();
|
||||
const { token } = useAuth();
|
||||
const { token, user } = 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 (!endpoints || !token) return null;
|
||||
if (!token || (!isOnlySec && !endpoints)) return null;
|
||||
|
||||
const endpointList = [...endpoints];
|
||||
const endpointList = endpoints ? [...endpoints] : [];
|
||||
endpointList.push({
|
||||
uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '',
|
||||
type: 'owsec',
|
||||
type: isOnlySec ? '' : 'owsec',
|
||||
id: 0,
|
||||
vendor: 'owsec',
|
||||
authenticationType: '',
|
||||
@@ -37,20 +57,51 @@ const SystemPage = () => {
|
||||
}, [endpoints, token]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card mb={4} py={2} px={4}>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto">
|
||||
{t('controller.firmware.endpoints')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<SimpleGrid minChildWidth="500px" spacing="20px" mb={3}>
|
||||
{endpointsList}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
<Card p={0}>
|
||||
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||
<TabList>
|
||||
<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}>
|
||||
<Spacer />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
||||
</CardHeader>
|
||||
)}
|
||||
<SimpleGrid minChildWidth="500px" spacing="20px" p={4}>
|
||||
{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;
|
||||
|
||||
@@ -12,9 +12,11 @@ interface Props {
|
||||
isSuspended: boolean;
|
||||
isWaitingForCheck: boolean;
|
||||
refresh: () => void;
|
||||
isDisabled?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refresh }) => {
|
||||
const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh });
|
||||
@@ -75,10 +77,17 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<MenuButton as={IconButton} aria-label="Commands" icon={<Wrench size={20} />} size="sm" ml={2} />
|
||||
<Tooltip label={t('common.actions')}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
icon={<Wrench size={20} />}
|
||||
size={size}
|
||||
ml={2}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList>
|
||||
<MenuList fontSize="md">
|
||||
<MenuItem onClick={handleSuspendClick}>
|
||||
{isSuspended ? t('users.reactivate_user') : t('users.suspend')}
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||
import { Modal } from '../../../../components/Modals/Modal';
|
||||
import CreateUserForm, { CreateUserFormValues } from './Form';
|
||||
import { CreateButton } from 'components/Buttons/CreateButton';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
|
||||
@@ -25,16 +25,7 @@ const CreateUserModal = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'csr'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
{user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
|
||||
@@ -62,7 +62,7 @@ const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Pro
|
||||
|
||||
useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isOpen]);
|
||||
}, [isOpen, editing]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Spinner, Center, useDisclosure, useBoolean } from '@chakra-ui/react';
|
||||
import { Spinner, Center, useDisclosure, useBoolean, Tag } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
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';
|
||||
|
||||
@@ -19,10 +21,11 @@ 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 } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
|
||||
const { data: user, isFetching, refetch } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
|
||||
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
@@ -31,6 +34,11 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
refetch();
|
||||
queryClient.invalidateQueries(['users']);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setEditing.off();
|
||||
}, [isOpen]);
|
||||
@@ -40,15 +48,40 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
title={t('crud.edit_obj', { obj: t('user.title') })}
|
||||
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}
|
||||
</>
|
||||
}
|
||||
topRightButtons={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||
hidden={!editing}
|
||||
/>
|
||||
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
|
||||
<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}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Avatar, Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { ArrowsClockwise } from 'phosphor-react';
|
||||
import { Avatar, Box, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
|
||||
@@ -9,6 +8,7 @@ import FormattedDate from '../../../components/InformationDisplays/FormattedDate
|
||||
import CreateUserModal from './CreateUserModal';
|
||||
import EditUserModal from './EditUserModal';
|
||||
import UserActions from './UserActions';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
@@ -25,10 +25,10 @@ const UserTable = () => {
|
||||
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
|
||||
|
||||
const openEditModal = (editUser: User) => {
|
||||
const openEditModal = React.useCallback((editUser: User) => {
|
||||
setEditId(editUser.id);
|
||||
openEdit();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const memoizedActions = useCallback(
|
||||
(userActions: User) => (
|
||||
@@ -99,7 +99,7 @@ const UserTable = () => {
|
||||
];
|
||||
if (user?.userRole !== 'csr')
|
||||
baseColumns.push({
|
||||
id: 'user',
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
@@ -125,28 +125,21 @@ const UserTable = () => {
|
||||
preference="provisioning.userTable.hiddenColumns"
|
||||
/>
|
||||
<CreateUserModal />
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={() => refreshUsers()}
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
ml={2}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<RefreshButton onClick={refreshUsers} isFetching={isFetching} ml={2} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
data={users?.filter((curr) => curr.email !== user?.email) ?? []}
|
||||
<DataTable<User>
|
||||
columns={columns}
|
||||
data={users ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('users.title')}
|
||||
sortBy={[{ id: 'email', desc: false }]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
fullScreen
|
||||
onRowClick={openEditModal}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
|
||||
import { extendTheme, Tooltip, type ThemeConfig } from '@chakra-ui/react';
|
||||
import CardComponent from './additions/card/Card';
|
||||
import CardBodyComponent from './additions/card/CardBody';
|
||||
import CardHeaderComponent from './additions/card/CardHeader';
|
||||
@@ -37,4 +37,6 @@ const theme = extendTheme({
|
||||
},
|
||||
});
|
||||
|
||||
Tooltip.defaultProps = { ...Tooltip.defaultProps, hasArrow: true };
|
||||
|
||||
export default theme;
|
||||
|
||||
@@ -1,49 +1,9 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
/* other options */
|
||||
},
|
||||
manifest: {
|
||||
name: 'OpenWiFi Controller App',
|
||||
short_name: 'OpenWiFiController',
|
||||
description: 'OpenWiFi Controller App',
|
||||
theme_color: '#000000',
|
||||
icons: [
|
||||
{
|
||||
src: 'android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-384x384.png',
|
||||
sizes: '384x384',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
build: {
|
||||
outDir: './build',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
|
||||
Reference in New Issue
Block a user