mirror of
				https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
				synced 2025-10-30 10:22:24 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			v2.9.0-RC1
			...
			v2.8.0-RC1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 7f0897d189 | 
| @@ -8,7 +8,7 @@ fullnameOverride: "" | |||||||
| images: | images: | ||||||
|   owgwui: |   owgwui: | ||||||
|     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui |     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui | ||||||
|     tag: v2.9.0-RC1 |     tag: v2.8.0-RC1 | ||||||
|     pullPolicy: Always |     pullPolicy: Always | ||||||
|  |  | ||||||
| services: | services: | ||||||
|   | |||||||
							
								
								
									
										97
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										97
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
|   "name": "ucentral-client", |   "name": "ucentral-client", | ||||||
|   "version": "2.9.0(13)", |   "version": "2.8.0(44)", | ||||||
|   "lockfileVersion": 2, |   "lockfileVersion": 2, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "ucentral-client", |       "name": "ucentral-client", | ||||||
|       "version": "2.9.0(13)", |       "version": "2.8.0(44)", | ||||||
|       "license": "ISC", |       "license": "ISC", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@chakra-ui/icons": "^2.0.11", |         "@chakra-ui/icons": "^2.0.11", | ||||||
| @@ -16,8 +16,6 @@ | |||||||
|         "@emotion/react": "^11.10.4", |         "@emotion/react": "^11.10.4", | ||||||
|         "@emotion/styled": "^11.10.4", |         "@emotion/styled": "^11.10.4", | ||||||
|         "@fontsource/inter": "^4.5.14", |         "@fontsource/inter": "^4.5.14", | ||||||
|         "@googlemaps/react-wrapper": "^1.1.35", |  | ||||||
|         "@googlemaps/typescript-guards": "^2.0.3", |  | ||||||
|         "@react-spring/web": "^9.5.5", |         "@react-spring/web": "^9.5.5", | ||||||
|         "@tanstack/react-query": "^4.12.0", |         "@tanstack/react-query": "^4.12.0", | ||||||
|         "@textea/json-viewer": "^2.10.0", |         "@textea/json-viewer": "^2.10.0", | ||||||
| @@ -26,7 +24,6 @@ | |||||||
|         "chakra-react-select": "^4.3.0", |         "chakra-react-select": "^4.3.0", | ||||||
|         "chart.js": "^3.9.1", |         "chart.js": "^3.9.1", | ||||||
|         "dagre": "^0.8.5", |         "dagre": "^0.8.5", | ||||||
|         "fast-equals": "^4.0.3", |  | ||||||
|         "formik": "^2.2.9", |         "formik": "^2.2.9", | ||||||
|         "framer-motion": "^7.6.1", |         "framer-motion": "^7.6.1", | ||||||
|         "i18next": "^22.0.0", |         "i18next": "^22.0.0", | ||||||
| @@ -57,7 +54,6 @@ | |||||||
|         "zustand": "^4.1.2" |         "zustand": "^4.1.2" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@types/google.maps": "^3.51.0", |  | ||||||
|         "@types/node": "^18.11.2", |         "@types/node": "^18.11.2", | ||||||
|         "@types/react": "^18.0.21", |         "@types/react": "^18.0.21", | ||||||
|         "@types/react-csv": "^1.1.3", |         "@types/react-csv": "^1.1.3", | ||||||
| @@ -2852,30 +2848,6 @@ | |||||||
|       "version": "4.5.14", |       "version": "4.5.14", | ||||||
|       "license": "MIT" |       "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": { |     "node_modules/@humanwhocodes/config-array": { | ||||||
|       "version": "0.10.7", |       "version": "0.10.7", | ||||||
|       "dev": true, |       "dev": true, | ||||||
| @@ -3529,12 +3501,6 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "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": { |     "node_modules/@types/json-schema": { | ||||||
|       "version": "7.0.11", |       "version": "7.0.11", | ||||||
|       "dev": true, |       "dev": true, | ||||||
| @@ -5563,6 +5529,7 @@ | |||||||
|     }, |     }, | ||||||
|     "node_modules/fast-deep-equal": { |     "node_modules/fast-deep-equal": { | ||||||
|       "version": "3.1.3", |       "version": "3.1.3", | ||||||
|  |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/fast-diff": { |     "node_modules/fast-diff": { | ||||||
| @@ -5570,11 +5537,6 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "Apache-2.0" |       "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": { |     "node_modules/fast-glob": { | ||||||
|       "version": "3.2.12", |       "version": "3.2.12", | ||||||
|       "dev": true, |       "dev": true, | ||||||
| @@ -6680,9 +6642,8 @@ | |||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/json5": { |     "node_modules/json5": { | ||||||
|       "version": "2.2.3", |       "version": "2.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", |       "license": "MIT", | ||||||
|       "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", |  | ||||||
|       "bin": { |       "bin": { | ||||||
|         "json5": "lib/cli.js" |         "json5": "lib/cli.js" | ||||||
|       }, |       }, | ||||||
| @@ -8892,10 +8853,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/tsconfig-paths/node_modules/json5": { |     "node_modules/tsconfig-paths/node_modules/json5": { | ||||||
|       "version": "1.0.2", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", |  | ||||||
|       "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", |  | ||||||
|       "dev": true, |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "minimist": "^1.2.0" |         "minimist": "^1.2.0" | ||||||
|       }, |       }, | ||||||
| @@ -11465,27 +11425,6 @@ | |||||||
|     "@fontsource/inter": { |     "@fontsource/inter": { | ||||||
|       "version": "4.5.14" |       "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": { |     "@humanwhocodes/config-array": { | ||||||
|       "version": "0.10.7", |       "version": "0.10.7", | ||||||
|       "dev": true, |       "dev": true, | ||||||
| @@ -11847,12 +11786,6 @@ | |||||||
|       "version": "0.0.39", |       "version": "0.0.39", | ||||||
|       "dev": true |       "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": { |     "@types/json-schema": { | ||||||
|       "version": "7.0.11", |       "version": "7.0.11", | ||||||
|       "dev": true |       "dev": true | ||||||
| @@ -13092,17 +13025,13 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "fast-deep-equal": { |     "fast-deep-equal": { | ||||||
|       "version": "3.1.3" |       "version": "3.1.3", | ||||||
|  |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "fast-diff": { |     "fast-diff": { | ||||||
|       "version": "1.2.0", |       "version": "1.2.0", | ||||||
|       "dev": true |       "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": { |     "fast-glob": { | ||||||
|       "version": "3.2.12", |       "version": "3.2.12", | ||||||
|       "dev": true, |       "dev": true, | ||||||
| @@ -13759,9 +13688,7 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "json5": { |     "json5": { | ||||||
|       "version": "2.2.3", |       "version": "2.2.1" | ||||||
|       "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", |  | ||||||
|       "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" |  | ||||||
|     }, |     }, | ||||||
|     "jsonfile": { |     "jsonfile": { | ||||||
|       "version": "6.1.0", |       "version": "6.1.0", | ||||||
| @@ -15054,9 +14981,7 @@ | |||||||
|       }, |       }, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "json5": { |         "json5": { | ||||||
|           "version": "1.0.2", |           "version": "1.0.1", | ||||||
|           "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", |  | ||||||
|           "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", |  | ||||||
|           "dev": true, |           "dev": true, | ||||||
|           "requires": { |           "requires": { | ||||||
|             "minimist": "^1.2.0" |             "minimist": "^1.2.0" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "ucentral-client", |   "name": "ucentral-client", | ||||||
|   "version": "2.9.0(13)", |   "version": "2.8.0(44)", | ||||||
|   "description": "", |   "description": "", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "main": "index.tsx", |   "main": "index.tsx", | ||||||
| @@ -22,15 +22,12 @@ | |||||||
|     "@emotion/react": "^11.10.4", |     "@emotion/react": "^11.10.4", | ||||||
|     "@emotion/styled": "^11.10.4", |     "@emotion/styled": "^11.10.4", | ||||||
|     "@fontsource/inter": "^4.5.14", |     "@fontsource/inter": "^4.5.14", | ||||||
|     "@googlemaps/react-wrapper": "^1.1.35", |  | ||||||
|     "@googlemaps/typescript-guards": "^2.0.3", |  | ||||||
|     "@react-spring/web": "^9.5.5", |     "@react-spring/web": "^9.5.5", | ||||||
|     "axios": "^1.1.3", |     "axios": "^1.1.3", | ||||||
|     "buffer": "^6.0.3", |     "buffer": "^6.0.3", | ||||||
|     "chakra-react-select": "^4.3.0", |     "chakra-react-select": "^4.3.0", | ||||||
|     "dagre": "^0.8.5", |     "dagre": "^0.8.5", | ||||||
|     "formik": "^2.2.9", |     "formik": "^2.2.9", | ||||||
|     "fast-equals": "^4.0.3", |  | ||||||
|     "framer-motion": "^7.6.1", |     "framer-motion": "^7.6.1", | ||||||
|     "i18next": "^22.0.0", |     "i18next": "^22.0.0", | ||||||
|     "i18next-browser-languagedetector": "^6.1.8", |     "i18next-browser-languagedetector": "^6.1.8", | ||||||
| @@ -63,7 +60,6 @@ | |||||||
|     "zustand": "^4.1.2" |     "zustand": "^4.1.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@types/google.maps": "^3.51.0", |  | ||||||
|     "@types/node": "^18.11.2", |     "@types/node": "^18.11.2", | ||||||
|     "@types/react": "^18.0.21", |     "@types/react": "^18.0.21", | ||||||
|     "@types/react-csv": "^1.1.3", |     "@types/react-csv": "^1.1.3", | ||||||
|   | |||||||
| @@ -79,11 +79,8 @@ | |||||||
| 		"live_view_help": "Hilfe zur Live-Ansicht", | 		"live_view_help": "Hilfe zur Live-Ansicht", | ||||||
| 		"memory": "Erinnerung", | 		"memory": "Erinnerung", | ||||||
| 		"memory_used": "Verwendeter Speicher", | 		"memory_used": "Verwendeter Speicher", | ||||||
| 		"missing_board": "Analytics-Überwachung an diesem Ort ist nicht mehr aktiv. Klicken Sie hier, um die Überwachung neu zu starten", | 		"missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu", | ||||||
| 		"mode": "Modus", | 		"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", | 		"noise": "Lärm", | ||||||
| 		"packets": "Pakete", | 		"packets": "Pakete", | ||||||
| 		"radio": "RADIO", | 		"radio": "RADIO", | ||||||
| @@ -94,8 +91,6 @@ | |||||||
| 		"retries": "Wiederholungen", | 		"retries": "Wiederholungen", | ||||||
| 		"search_serials": "Zeitschriften suchen", | 		"search_serials": "Zeitschriften suchen", | ||||||
| 		"stop_monitoring": "Beenden Sie die Überwachung", | 		"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", | 		"temperature": "Temperatur", | ||||||
| 		"title": "ANALYTICS", | 		"title": "ANALYTICS", | ||||||
| 		"total_data": "Gesamtdaten", | 		"total_data": "Gesamtdaten", | ||||||
| @@ -180,7 +175,6 @@ | |||||||
| 		"other": "Befehle", | 		"other": "Befehle", | ||||||
| 		"override_dfs": "DFS überschreiben", | 		"override_dfs": "DFS überschreiben", | ||||||
| 		"reboot": "Starten Sie neu", | 		"reboot": "Starten Sie neu", | ||||||
| 		"reboot_description": "Möchten Sie dieses Gerät neu starten?", |  | ||||||
| 		"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}", | 		"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}", | ||||||
| 		"reboot_success": "Neustartbefehl erfolgreich gesendet!", | 		"reboot_success": "Neustartbefehl erfolgreich gesendet!", | ||||||
| 		"revision": "Revision", | 		"revision": "Revision", | ||||||
| @@ -397,7 +391,6 @@ | |||||||
| 		"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}", | 		"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}", | ||||||
| 		"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}", | 		"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}", | ||||||
| 		"weight": "Gewicht", | 		"weight": "Gewicht", | ||||||
| 		"wifi_bands_max": "Es können nicht mehr als 8 SSIDs dieses WLAN-Band verwenden", |  | ||||||
| 		"wifi_frames": "WiFi-Frames" | 		"wifi_frames": "WiFi-Frames" | ||||||
| 	}, | 	}, | ||||||
| 	"contacts": { | 	"contacts": { | ||||||
| @@ -607,7 +600,6 @@ | |||||||
| 		"certificate_expires_in": "Zertifikat läuft ab in", | 		"certificate_expires_in": "Zertifikat läuft ab in", | ||||||
| 		"certificate_expiry": "Zert. Läuft ab in", | 		"certificate_expiry": "Zert. Läuft ab in", | ||||||
| 		"connected": "In Verbindung gebracht", | 		"connected": "In Verbindung gebracht", | ||||||
| 		"crash_logs": "Absturzprotokolle", |  | ||||||
| 		"create_errors": "Fehler beim Versuch, Geräte zu erstellen", | 		"create_errors": "Fehler beim Versuch, Geräte zu erstellen", | ||||||
| 		"create_success": " Geräte erfolgreich erstellt", | 		"create_success": " Geräte erfolgreich erstellt", | ||||||
| 		"current_firmware": "Aktuelle Firmware", | 		"current_firmware": "Aktuelle Firmware", | ||||||
| @@ -621,7 +613,6 @@ | |||||||
| 		"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts", | 		"import_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", | 		"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)", | 		"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)", | ||||||
| 		"logs_one": "Log", |  | ||||||
| 		"new_devices": "Neue Geräte", | 		"new_devices": "Neue Geräte", | ||||||
| 		"no_model_image": "Kein Modellbild gefunden", | 		"no_model_image": "Kein Modellbild gefunden", | ||||||
| 		"not_connected": "Nicht verbunden", | 		"not_connected": "Nicht verbunden", | ||||||
| @@ -630,8 +621,6 @@ | |||||||
| 		"one": "Gerät", | 		"one": "Gerät", | ||||||
| 		"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?", | 		"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?", | ||||||
| 		"restricted": "Beschränkt", | 		"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", | 		"sanity": "Gesundheit", | ||||||
| 		"start_import": "Geräteimport starten", | 		"start_import": "Geräteimport starten", | ||||||
| 		"test_batch": "Testen Sie Importdaten", | 		"test_batch": "Testen Sie Importdaten", | ||||||
| @@ -682,15 +671,7 @@ | |||||||
| 		"test_digicert_creds": "Anmeldeinformationen testen", | 		"test_digicert_creds": "Anmeldeinformationen testen", | ||||||
| 		"title": "Entitäten", | 		"title": "Entitäten", | ||||||
| 		"tree": "Entitätsbaum", | 		"tree": "Entitätsbaum", | ||||||
| 		"update_success": "Entität aktualisiert!", | 		"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." | ||||||
| 		"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": { | 	"footer": { | ||||||
| 		"powered_by": "Unterstützt von", | 		"powered_by": "Unterstützt von", | ||||||
| @@ -784,17 +765,13 @@ | |||||||
| 		"city": "Stadt", | 		"city": "Stadt", | ||||||
| 		"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden", | 		"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden", | ||||||
| 		"country": "Land", | 		"country": "Land", | ||||||
| 		"elevation": "Elevation", |  | ||||||
| 		"geocode": "Geo-Code", | 		"geocode": "Geo-Code", | ||||||
| 		"lat": "Breite", |  | ||||||
| 		"longitude": "Längengrad", |  | ||||||
| 		"one": "Ort", | 		"one": "Ort", | ||||||
| 		"other": "Standorte", | 		"other": "Standorte", | ||||||
| 		"postal": "Postleitzahl", | 		"postal": "Postleitzahl", | ||||||
| 		"state": "Bundesstaat / Provinz", | 		"state": "Bundesstaat / Provinz", | ||||||
| 		"title": "Standorte", | 		"title": "Standorte", | ||||||
| 		"to_claim": "Standorte zu beanspruchen", | 		"to_claim": "Standorte zu beanspruchen" | ||||||
| 		"view_gps": "" |  | ||||||
| 	}, | 	}, | ||||||
| 	"login": { | 	"login": { | ||||||
| 		"access_policy": "Zugangsrichtlinien", | 		"access_policy": "Zugangsrichtlinien", | ||||||
| @@ -820,7 +797,6 @@ | |||||||
| 		"reset_password": "Passwort zurücksetzen", | 		"reset_password": "Passwort zurücksetzen", | ||||||
| 		"sign_in": "Einloggen", | 		"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", | 		"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", | 		"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", | 		"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!", | 		"welcome_back": "Willkommen zurück!", | ||||||
| @@ -926,7 +902,7 @@ | |||||||
| 		"dfs": "DFS-Überschreibung", | 		"dfs": "DFS-Überschreibung", | ||||||
| 		"gw_commands": "Gateway-Befehle", | 		"gw_commands": "Gateway-Befehle", | ||||||
| 		"identifier": "Identifikator", | 		"identifier": "Identifikator", | ||||||
| 		"key_verification": "Signieren von Schlüsselinformationen", | 		"key_verification": "Überprüfung des Signaturschlüssels", | ||||||
| 		"restricted": "Beschränkt", | 		"restricted": "Beschränkt", | ||||||
| 		"signed_upgrade": "Nur signiertes Upgrade", | 		"signed_upgrade": "Nur signiertes Upgrade", | ||||||
| 		"title": "Beschränkungen", | 		"title": "Beschränkungen", | ||||||
| @@ -1046,7 +1022,6 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
| 		"backend_logs": "Back-End-Protokolle", | 		"backend_logs": "Back-End-Protokolle", | ||||||
| 		"configuration": "Aufbau", |  | ||||||
| 		"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden", | 		"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden", | ||||||
| 		"endpoint": "Endpunkt", | 		"endpoint": "Endpunkt", | ||||||
| 		"hostname": "Hostname", | 		"hostname": "Hostname", | ||||||
| @@ -1057,10 +1032,6 @@ | |||||||
| 		"os": "Betriebssystem", | 		"os": "Betriebssystem", | ||||||
| 		"processors": "Prozessoren", | 		"processors": "Prozessoren", | ||||||
| 		"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden", | 		"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden", | ||||||
| 		"secrets": "Geheimnisse", |  | ||||||
| 		"secrets_create": "Geheimnis erstellen", |  | ||||||
| 		"secrets_one": "Geheimnis", |  | ||||||
| 		"services": "dienstleistungen", |  | ||||||
| 		"start": "Start", | 		"start": "Start", | ||||||
| 		"subsystems": "Subsysteme", | 		"subsystems": "Subsysteme", | ||||||
| 		"success_reload": "Reload-Befehl erfolgreich gesendet!", | 		"success_reload": "Reload-Befehl erfolgreich gesendet!", | ||||||
| @@ -1082,11 +1053,9 @@ | |||||||
| 		"previous_page": "Vorherige Seite" | 		"previous_page": "Vorherige Seite" | ||||||
| 	}, | 	}, | ||||||
| 	"user": { | 	"user": { | ||||||
| 		"email_not_validated": "E-Mail nicht validiert", |  | ||||||
| 		"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}", | 		"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}", | ||||||
| 		"password": "Passwort", | 		"password": "Passwort", | ||||||
| 		"role": "Rolle", | 		"role": "Rolle", | ||||||
| 		"suspended": "Suspendiert", |  | ||||||
| 		"title": "Nutzer" | 		"title": "Nutzer" | ||||||
| 	}, | 	}, | ||||||
| 	"users": { | 	"users": { | ||||||
| @@ -1131,11 +1100,9 @@ | |||||||
| 		"successfully_update_devices": " {{num}} Geräte werden aktualisiert!", | 		"successfully_update_devices": " {{num}} Geräte werden aktualisiert!", | ||||||
| 		"title": "Veranstaltungsorte", | 		"title": "Veranstaltungsorte", | ||||||
| 		"update_all_devices": "Alle Gerätekonfigurationen aktualisieren", | 		"update_all_devices": "Alle Gerätekonfigurationen aktualisieren", | ||||||
| 		"update_success": "Veranstaltungsort aktualisiert!", | 		"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware", | ||||||
| 		"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte", |  | ||||||
| 		"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}", | 		"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}", | ||||||
| 		"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!", | 		"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": "Benutze existierendes", | ||||||
| 		"use_existing_contacts": "Verwenden Sie vorhandene Kontakte", | 		"use_existing_contacts": "Verwenden Sie vorhandene Kontakte", | ||||||
| 		"use_this_contact": "Verwenden Sie diesen Kontakt" | 		"use_this_contact": "Verwenden Sie diesen Kontakt" | ||||||
|   | |||||||
| @@ -79,11 +79,8 @@ | |||||||
| 		"live_view_help": "Live View Help", | 		"live_view_help": "Live View Help", | ||||||
| 		"memory": "Memory", | 		"memory": "Memory", | ||||||
| 		"memory_used": "Memory Used", | 		"memory_used": "Memory Used", | ||||||
| 		"missing_board": "Analytics monitoring on this venue is no longer active. Click here to restart monitoring", | 		"missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu", | ||||||
| 		"mode": "Mode", | 		"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", | 		"noise": "Noise", | ||||||
| 		"packets": "Packets", | 		"packets": "Packets", | ||||||
| 		"radio": "Radio", | 		"radio": "Radio", | ||||||
| @@ -94,8 +91,6 @@ | |||||||
| 		"retries": "Retries", | 		"retries": "Retries", | ||||||
| 		"search_serials": "Search Serials", | 		"search_serials": "Search Serials", | ||||||
| 		"stop_monitoring": "Stop Monitoring", | 		"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", | 		"temperature": "Temperature", | ||||||
| 		"title": "Analytics", | 		"title": "Analytics", | ||||||
| 		"total_data": "Total Data", | 		"total_data": "Total Data", | ||||||
| @@ -180,7 +175,6 @@ | |||||||
| 		"other": "Commands", | 		"other": "Commands", | ||||||
| 		"override_dfs": "Override DFS", | 		"override_dfs": "Override DFS", | ||||||
| 		"reboot": "Reboot", | 		"reboot": "Reboot", | ||||||
| 		"reboot_description": "Do you want to reboot this device?", |  | ||||||
| 		"reboot_error": "Error while sending reboot command: {{e}}", | 		"reboot_error": "Error while sending reboot command: {{e}}", | ||||||
| 		"reboot_success": "Successfully sent reboot command!", | 		"reboot_success": "Successfully sent reboot command!", | ||||||
| 		"revision": "Revision", | 		"revision": "Revision", | ||||||
| @@ -397,7 +391,6 @@ | |||||||
| 		"warning_pushes_one": "Waiting for devices to connect: {{count}}", | 		"warning_pushes_one": "Waiting for devices to connect: {{count}}", | ||||||
| 		"warning_pushes_other": "Waiting for devices to connect: {{count}}", | 		"warning_pushes_other": "Waiting for devices to connect: {{count}}", | ||||||
| 		"weight": "Weight", | 		"weight": "Weight", | ||||||
| 		"wifi_bands_max": "There cannot be more than 8 SSIDs using this wifi-band", |  | ||||||
| 		"wifi_frames": "WiFi Frames" | 		"wifi_frames": "WiFi Frames" | ||||||
| 	}, | 	}, | ||||||
| 	"contacts": { | 	"contacts": { | ||||||
| @@ -607,7 +600,6 @@ | |||||||
| 		"certificate_expires_in": "Certificate Expiry", | 		"certificate_expires_in": "Certificate Expiry", | ||||||
| 		"certificate_expiry": "Cert. Expires In", | 		"certificate_expiry": "Cert. Expires In", | ||||||
| 		"connected": "Connected", | 		"connected": "Connected", | ||||||
| 		"crash_logs": "Crash Logs", |  | ||||||
| 		"create_errors": "errors while trying to create devices", | 		"create_errors": "errors while trying to create devices", | ||||||
| 		"create_success": " devices successfully created", | 		"create_success": " devices successfully created", | ||||||
| 		"current_firmware": "Current Firmware", | 		"current_firmware": "Current Firmware", | ||||||
| @@ -621,7 +613,6 @@ | |||||||
| 		"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired", | 		"import_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", | 		"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)", | 		"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)", | ||||||
| 		"logs_one": "Log", |  | ||||||
| 		"new_devices": "new devices", | 		"new_devices": "new devices", | ||||||
| 		"no_model_image": "No Model Image Found", | 		"no_model_image": "No Model Image Found", | ||||||
| 		"not_connected": "Not Connected", | 		"not_connected": "Not Connected", | ||||||
| @@ -630,8 +621,6 @@ | |||||||
| 		"one": "Device", | 		"one": "Device", | ||||||
| 		"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?", | 		"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?", | ||||||
| 		"restricted": "Restricted", | 		"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", | 		"sanity": "Sanity", | ||||||
| 		"start_import": "Start Device Importation", | 		"start_import": "Start Device Importation", | ||||||
| 		"test_batch": "Test Import Data", | 		"test_batch": "Test Import Data", | ||||||
| @@ -682,15 +671,7 @@ | |||||||
| 		"test_digicert_creds": "Test Credentials", | 		"test_digicert_creds": "Test Credentials", | ||||||
| 		"title": "Entities", | 		"title": "Entities", | ||||||
| 		"tree": "Entity Tree", | 		"tree": "Entity Tree", | ||||||
| 		"update_success": "Entity updated!", | 		"venues_under_root": "Venues cannot be created directly under the root entity. Please create new entities and create venues under these." | ||||||
| 		"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": { | 	"footer": { | ||||||
| 		"powered_by": "Powered By", | 		"powered_by": "Powered By", | ||||||
| @@ -784,17 +765,13 @@ | |||||||
| 		"city": "City", | 		"city": "City", | ||||||
| 		"claim_explanation": "To claim locations you can use the table below", | 		"claim_explanation": "To claim locations you can use the table below", | ||||||
| 		"country": "Country", | 		"country": "Country", | ||||||
| 		"elevation": "Elevation", |  | ||||||
| 		"geocode": "Geo Code", | 		"geocode": "Geo Code", | ||||||
| 		"lat": "Latitude", |  | ||||||
| 		"longitude": "Longitude", |  | ||||||
| 		"one": "Location", | 		"one": "Location", | ||||||
| 		"other": "Locations", | 		"other": "Locations", | ||||||
| 		"postal": "ZIP/Postal Code", | 		"postal": "ZIP/Postal Code", | ||||||
| 		"state": "State/Province", | 		"state": "State/Province", | ||||||
| 		"title": "Locations", | 		"title": "Locations", | ||||||
| 		"to_claim": "Locations to claim", | 		"to_claim": "Locations to claim" | ||||||
| 		"view_gps": "View GPS Location" |  | ||||||
| 	}, | 	}, | ||||||
| 	"login": { | 	"login": { | ||||||
| 		"access_policy": "Access Policy", | 		"access_policy": "Access Policy", | ||||||
| @@ -820,7 +797,6 @@ | |||||||
| 		"reset_password": "Reset Password", | 		"reset_password": "Reset Password", | ||||||
| 		"sign_in": "Sign In", | 		"sign_in": "Sign In", | ||||||
| 		"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login", | 		"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", | 		"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", | 		"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!", | 		"welcome_back": "Welcome Back!", | ||||||
| @@ -926,7 +902,7 @@ | |||||||
| 		"dfs": "DFS Override", | 		"dfs": "DFS Override", | ||||||
| 		"gw_commands": "Gateway Commands", | 		"gw_commands": "Gateway Commands", | ||||||
| 		"identifier": "Identifier", | 		"identifier": "Identifier", | ||||||
| 		"key_verification": "Signing Key Information", | 		"key_verification": "Signing Key Verification", | ||||||
| 		"restricted": "Restricted", | 		"restricted": "Restricted", | ||||||
| 		"signed_upgrade": "Signed Upgrade Only", | 		"signed_upgrade": "Signed Upgrade Only", | ||||||
| 		"title": "Restrictions", | 		"title": "Restrictions", | ||||||
| @@ -1046,7 +1022,6 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
| 		"backend_logs": "Back-End Logs", | 		"backend_logs": "Back-End Logs", | ||||||
| 		"configuration": "Configuration", |  | ||||||
| 		"could_not_retrieve": "Error: could not retrieve {{name}} system information", | 		"could_not_retrieve": "Error: could not retrieve {{name}} system information", | ||||||
| 		"endpoint": "Endpoint", | 		"endpoint": "Endpoint", | ||||||
| 		"hostname": "Host Name", | 		"hostname": "Host Name", | ||||||
| @@ -1057,10 +1032,6 @@ | |||||||
| 		"os": "Operating System", | 		"os": "Operating System", | ||||||
| 		"processors": "Processors", | 		"processors": "Processors", | ||||||
| 		"reload_chosen_subsystems": "Reload Chosen Subsystems", | 		"reload_chosen_subsystems": "Reload Chosen Subsystems", | ||||||
| 		"secrets": "Secrets", |  | ||||||
| 		"secrets_create": "Create Secret", |  | ||||||
| 		"secrets_one": "Secret", |  | ||||||
| 		"services": "Services", |  | ||||||
| 		"start": "Start", | 		"start": "Start", | ||||||
| 		"subsystems": "Subsystems", | 		"subsystems": "Subsystems", | ||||||
| 		"success_reload": "Successfully sent reload command!", | 		"success_reload": "Successfully sent reload command!", | ||||||
| @@ -1082,11 +1053,9 @@ | |||||||
| 		"previous_page": "Previous Page" | 		"previous_page": "Previous Page" | ||||||
| 	}, | 	}, | ||||||
| 	"user": { | 	"user": { | ||||||
| 		"email_not_validated": "email not validated", |  | ||||||
| 		"error_fetching": "Error fetching user information: {{e}}", | 		"error_fetching": "Error fetching user information: {{e}}", | ||||||
| 		"password": "Password", | 		"password": "Password", | ||||||
| 		"role": "Role", | 		"role": "Role", | ||||||
| 		"suspended": "suspended", |  | ||||||
| 		"title": "User" | 		"title": "User" | ||||||
| 	}, | 	}, | ||||||
| 	"users": { | 	"users": { | ||||||
| @@ -1131,11 +1100,9 @@ | |||||||
| 		"successfully_update_devices": "Updating {{num}} devices!", | 		"successfully_update_devices": "Updating {{num}} devices!", | ||||||
| 		"title": "Venues", | 		"title": "Venues", | ||||||
| 		"update_all_devices": "Update All Device Configurations", | 		"update_all_devices": "Update All Device Configurations", | ||||||
| 		"update_success": "Venue updated!", | 		"upgrade_all_devices": "Upgrade All Devices to Latest Firmware", | ||||||
| 		"upgrade_all_devices": "Upgrade All Devices Firmware", |  | ||||||
| 		"upgrade_all_devices_error": "Error upgrading devices: {{e}}", | 		"upgrade_all_devices_error": "Error upgrading devices: {{e}}", | ||||||
| 		"upgrade_all_devices_success": "Successfully started upgrading devices!", | 		"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": "Use Existing", | ||||||
| 		"use_existing_contacts": "Use Existing Contacts", | 		"use_existing_contacts": "Use Existing Contacts", | ||||||
| 		"use_this_contact": "Use this contact" | 		"use_this_contact": "Use this contact" | ||||||
|   | |||||||
| @@ -79,11 +79,8 @@ | |||||||
| 		"live_view_help": "Ayuda de visualización en vivo", | 		"live_view_help": "Ayuda de visualización en vivo", | ||||||
| 		"memory": "Memoria", | 		"memory": "Memoria", | ||||||
| 		"memory_used": "Memoria usada", | 		"memory_used": "Memoria usada", | ||||||
| 		"missing_board": "El monitoreo analítico en este lugar ya no está activo. Haga clic aquí para reiniciar el monitoreo", | 		"missing_board": "El monitoreo analítico en este lugar ya no está activo, reinicie el monitoreo usando el menú superior", | ||||||
| 		"mode": "Modo", | 		"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", | 		"noise": "Ruido", | ||||||
| 		"packets": "Paquetes", | 		"packets": "Paquetes", | ||||||
| 		"radio": "RADIO", | 		"radio": "RADIO", | ||||||
| @@ -94,8 +91,6 @@ | |||||||
| 		"retries": "Reintentos", | 		"retries": "Reintentos", | ||||||
| 		"search_serials": "Buscar seriales", | 		"search_serials": "Buscar seriales", | ||||||
| 		"stop_monitoring": "Dejar de monitorear", | 		"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", | 		"temperature": "temperatura", | ||||||
| 		"title": "ANALÍTICA", | 		"title": "ANALÍTICA", | ||||||
| 		"total_data": "Datos totales", | 		"total_data": "Datos totales", | ||||||
| @@ -180,7 +175,6 @@ | |||||||
| 		"other": "comandos", | 		"other": "comandos", | ||||||
| 		"override_dfs": "Anular DFS", | 		"override_dfs": "Anular DFS", | ||||||
| 		"reboot": "Reiniciar", | 		"reboot": "Reiniciar", | ||||||
| 		"reboot_description": "¿Quieres reiniciar este dispositivo?", |  | ||||||
| 		"reboot_error": "Error al enviar el comando de reinicio: {{e}}", | 		"reboot_error": "Error al enviar el comando de reinicio: {{e}}", | ||||||
| 		"reboot_success": "¡Comando de reinicio enviado con éxito!", | 		"reboot_success": "¡Comando de reinicio enviado con éxito!", | ||||||
| 		"revision": "revisión", | 		"revision": "revisión", | ||||||
| @@ -397,7 +391,6 @@ | |||||||
| 		"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}", | 		"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}", | ||||||
| 		"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}", | 		"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}", | ||||||
| 		"weight": "Peso", | 		"weight": "Peso", | ||||||
| 		"wifi_bands_max": "No puede haber más de 8 SSID usando esta banda wifi", |  | ||||||
| 		"wifi_frames": "Marcos WiFi" | 		"wifi_frames": "Marcos WiFi" | ||||||
| 	}, | 	}, | ||||||
| 	"contacts": { | 	"contacts": { | ||||||
| @@ -607,7 +600,6 @@ | |||||||
| 		"certificate_expires_in": "El certificado caduca en", | 		"certificate_expires_in": "El certificado caduca en", | ||||||
| 		"certificate_expiry": "Cert. Expira en", | 		"certificate_expiry": "Cert. Expira en", | ||||||
| 		"connected": "Conectado", | 		"connected": "Conectado", | ||||||
| 		"crash_logs": "Registros de fallas", |  | ||||||
| 		"create_errors": "errores al intentar crear dispositivos", | 		"create_errors": "errores al intentar crear dispositivos", | ||||||
| 		"create_success": " dispositivos creados con éxito", | 		"create_success": " dispositivos creados con éxito", | ||||||
| 		"current_firmware": "Firmware actual", | 		"current_firmware": "Firmware actual", | ||||||
| @@ -621,7 +613,6 @@ | |||||||
| 		"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado", | 		"import_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", | 		"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)", | 		"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)", | ||||||
| 		"logs_one": "Iniciar sesión", |  | ||||||
| 		"new_devices": "Nuevos dispositivos", | 		"new_devices": "Nuevos dispositivos", | ||||||
| 		"no_model_image": "No se encontró ninguna imagen de modelo", | 		"no_model_image": "No se encontró ninguna imagen de modelo", | ||||||
| 		"not_connected": "No conectado", | 		"not_connected": "No conectado", | ||||||
| @@ -630,8 +621,6 @@ | |||||||
| 		"one": "Dispositivo", | 		"one": "Dispositivo", | ||||||
| 		"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?", | 		"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?", | ||||||
| 		"restricted": "Restringido", | 		"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", | 		"sanity": "Cordura", | ||||||
| 		"start_import": "Iniciar la importación de dispositivos", | 		"start_import": "Iniciar la importación de dispositivos", | ||||||
| 		"test_batch": "Datos de importación de prueba", | 		"test_batch": "Datos de importación de prueba", | ||||||
| @@ -682,15 +671,7 @@ | |||||||
| 		"test_digicert_creds": "Credenciales de prueba", | 		"test_digicert_creds": "Credenciales de prueba", | ||||||
| 		"title": "entidades", | 		"title": "entidades", | ||||||
| 		"tree": "Árbol de entidades", | 		"tree": "Árbol de entidades", | ||||||
| 		"update_success": "¡Entidad actualizada!", | 		"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz. Cree nuevas entidades y cree lugares bajo estas." | ||||||
| 		"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": { | 	"footer": { | ||||||
| 		"powered_by": "energizado por", | 		"powered_by": "energizado por", | ||||||
| @@ -784,17 +765,13 @@ | |||||||
| 		"city": "ciudad", | 		"city": "ciudad", | ||||||
| 		"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación", | 		"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación", | ||||||
| 		"country": "País", | 		"country": "País", | ||||||
| 		"elevation": "Elevación", |  | ||||||
| 		"geocode": "Código geográfico", | 		"geocode": "Código geográfico", | ||||||
| 		"lat": "Latitud", |  | ||||||
| 		"longitude": "Longitud", |  | ||||||
| 		"one": "Ubicación", | 		"one": "Ubicación", | ||||||
| 		"other": "Ubicaciones", | 		"other": "Ubicaciones", | ||||||
| 		"postal": "código postal", | 		"postal": "código postal", | ||||||
| 		"state": "Provincia del estado", | 		"state": "Provincia del estado", | ||||||
| 		"title": "Ubicaciones", | 		"title": "Ubicaciones", | ||||||
| 		"to_claim": "Ubicaciones para reclamar", | 		"to_claim": "Ubicaciones para reclamar" | ||||||
| 		"view_gps": "" |  | ||||||
| 	}, | 	}, | ||||||
| 	"login": { | 	"login": { | ||||||
| 		"access_policy": "Política de acceso", | 		"access_policy": "Política de acceso", | ||||||
| @@ -820,7 +797,6 @@ | |||||||
| 		"reset_password": "Restablecer la contraseña", | 		"reset_password": "Restablecer la contraseña", | ||||||
| 		"sign_in": "Registrarse", | 		"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", | 		"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", | 		"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.", | 		"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!", | 		"welcome_back": "¡Dar una buena acogida!", | ||||||
| @@ -926,7 +902,7 @@ | |||||||
| 		"dfs": "Anulación de DFS", | 		"dfs": "Anulación de DFS", | ||||||
| 		"gw_commands": "Comandos de puerta de enlace", | 		"gw_commands": "Comandos de puerta de enlace", | ||||||
| 		"identifier": "Identificador", | 		"identifier": "Identificador", | ||||||
| 		"key_verification": "Información clave de firma", | 		"key_verification": "Verificación de clave de firma", | ||||||
| 		"restricted": "Restringido", | 		"restricted": "Restringido", | ||||||
| 		"signed_upgrade": "Solo actualización firmada", | 		"signed_upgrade": "Solo actualización firmada", | ||||||
| 		"title": "Las restricciones", | 		"title": "Las restricciones", | ||||||
| @@ -1046,7 +1022,6 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
| 		"backend_logs": "Registros de back-end", | 		"backend_logs": "Registros de back-end", | ||||||
| 		"configuration": "Configuración", |  | ||||||
| 		"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ", | 		"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ", | ||||||
| 		"endpoint": "punto final", | 		"endpoint": "punto final", | ||||||
| 		"hostname": "Nombre de host", | 		"hostname": "Nombre de host", | ||||||
| @@ -1057,10 +1032,6 @@ | |||||||
| 		"os": "sistema operativo", | 		"os": "sistema operativo", | ||||||
| 		"processors": "Procesadores", | 		"processors": "Procesadores", | ||||||
| 		"reload_chosen_subsystems": "Recargar subsistemas elegidos", | 		"reload_chosen_subsystems": "Recargar subsistemas elegidos", | ||||||
| 		"secrets": "Misterios", |  | ||||||
| 		"secrets_create": "Crear secreto", |  | ||||||
| 		"secrets_one": "secreto", |  | ||||||
| 		"services": "Servicios", |  | ||||||
| 		"start": "comienzo", | 		"start": "comienzo", | ||||||
| 		"subsystems": "Subsistemas", | 		"subsystems": "Subsistemas", | ||||||
| 		"success_reload": "¡Comando de recarga enviado con éxito!", | 		"success_reload": "¡Comando de recarga enviado con éxito!", | ||||||
| @@ -1082,11 +1053,9 @@ | |||||||
| 		"previous_page": "Página anterior" | 		"previous_page": "Página anterior" | ||||||
| 	}, | 	}, | ||||||
| 	"user": { | 	"user": { | ||||||
| 		"email_not_validated": "correo electrónico no validado", |  | ||||||
| 		"error_fetching": "Error al obtener la información del usuario: {{e}}", | 		"error_fetching": "Error al obtener la información del usuario: {{e}}", | ||||||
| 		"password": "Contraseña", | 		"password": "Contraseña", | ||||||
| 		"role": "papel", | 		"role": "papel", | ||||||
| 		"suspended": "Suspendido", |  | ||||||
| 		"title": "Usuario" | 		"title": "Usuario" | ||||||
| 	}, | 	}, | ||||||
| 	"users": { | 	"users": { | ||||||
| @@ -1131,11 +1100,9 @@ | |||||||
| 		"successfully_update_devices": "¡Actualizando {{num}} dispositivos!", | 		"successfully_update_devices": "¡Actualizando {{num}} dispositivos!", | ||||||
| 		"title": "Sedes", | 		"title": "Sedes", | ||||||
| 		"update_all_devices": "Actualizar todas las configuraciones de dispositivos", | 		"update_all_devices": "Actualizar todas las configuraciones de dispositivos", | ||||||
| 		"update_success": "Lugar actualizado!", | 		"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente", | ||||||
| 		"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos", |  | ||||||
| 		"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}", | 		"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}", | ||||||
| 		"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!", | 		"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": "Utilizar existente", | ||||||
| 		"use_existing_contacts": "Usar contactos existentes", | 		"use_existing_contacts": "Usar contactos existentes", | ||||||
| 		"use_this_contact": "Usa este contacto" | 		"use_this_contact": "Usa este contacto" | ||||||
|   | |||||||
| @@ -79,11 +79,8 @@ | |||||||
| 		"live_view_help": "Aide sur l'affichage en direct", | 		"live_view_help": "Aide sur l'affichage en direct", | ||||||
| 		"memory": "mémoire", | 		"memory": "mémoire", | ||||||
| 		"memory_used": "Mémoire utilisée", | 		"memory_used": "Mémoire utilisée", | ||||||
| 		"missing_board": "La surveillance analytique sur ce site n'est plus active. Cliquez ici pour redémarrer la surveillance", | 		"missing_board": "La surveillance analytique sur ce lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut", | ||||||
| 		"mode": "Mode", | 		"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", | 		"noise": "Bruit", | ||||||
| 		"packets": "Paquets", | 		"packets": "Paquets", | ||||||
| 		"radio": "Radio", | 		"radio": "Radio", | ||||||
| @@ -94,8 +91,6 @@ | |||||||
| 		"retries": "Tentatives", | 		"retries": "Tentatives", | ||||||
| 		"search_serials": "Rechercher des publications en série", | 		"search_serials": "Rechercher des publications en série", | ||||||
| 		"stop_monitoring": "Arrêter la surveillance", | 		"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", | 		"temperature": "Température", | ||||||
| 		"title": "ANALYTIQUE", | 		"title": "ANALYTIQUE", | ||||||
| 		"total_data": "Données totales", | 		"total_data": "Données totales", | ||||||
| @@ -180,7 +175,6 @@ | |||||||
| 		"other": "Les commandes", | 		"other": "Les commandes", | ||||||
| 		"override_dfs": "Remplacer DFS", | 		"override_dfs": "Remplacer DFS", | ||||||
| 		"reboot": "Redémarrer", | 		"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_error": "Erreur lors de l'envoi de la commande de redémarrage : {{e}}", | ||||||
| 		"reboot_success": "Commande de redémarrage envoyée avec succès !", | 		"reboot_success": "Commande de redémarrage envoyée avec succès !", | ||||||
| 		"revision": "Révision", | 		"revision": "Révision", | ||||||
| @@ -397,7 +391,6 @@ | |||||||
| 		"warning_pushes_one": "En attente de connexion des appareils : {{count}}", | 		"warning_pushes_one": "En attente de connexion des appareils : {{count}}", | ||||||
| 		"warning_pushes_other": "En attente de connexion des appareils : {{count}}", | 		"warning_pushes_other": "En attente de connexion des appareils : {{count}}", | ||||||
| 		"weight": "Poids", | 		"weight": "Poids", | ||||||
| 		"wifi_bands_max": "Il ne peut y avoir plus de 8 SSID utilisant cette bande wifi", |  | ||||||
| 		"wifi_frames": "Cadres Wi-Fi" | 		"wifi_frames": "Cadres Wi-Fi" | ||||||
| 	}, | 	}, | ||||||
| 	"contacts": { | 	"contacts": { | ||||||
| @@ -607,7 +600,6 @@ | |||||||
| 		"certificate_expires_in": "Le certificat expire dans", | 		"certificate_expires_in": "Le certificat expire dans", | ||||||
| 		"certificate_expiry": "Cert. Expire dans", | 		"certificate_expiry": "Cert. Expire dans", | ||||||
| 		"connected": "Connecté", | 		"connected": "Connecté", | ||||||
| 		"crash_logs": "Journaux des plantages", |  | ||||||
| 		"create_errors": "erreurs lors de la tentative de création d'appareils", | 		"create_errors": "erreurs lors de la tentative de création d'appareils", | ||||||
| 		"create_success": " appareils créés avec succès", | 		"create_success": " appareils créés avec succès", | ||||||
| 		"current_firmware": "Firmware actuel", | 		"current_firmware": "Firmware actuel", | ||||||
| @@ -621,7 +613,6 @@ | |||||||
| 		"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée", | 		"import_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", | 		"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)", | 		"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", | 		"new_devices": "nouveaux appareils", | ||||||
| 		"no_model_image": "Aucune image de modèle trouvée", | 		"no_model_image": "Aucune image de modèle trouvée", | ||||||
| 		"not_connected": "Pas connecté", | 		"not_connected": "Pas connecté", | ||||||
| @@ -630,8 +621,6 @@ | |||||||
| 		"one": "Dispositif", | 		"one": "Dispositif", | ||||||
| 		"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?", | 		"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?", | ||||||
| 		"restricted": "Limité", | 		"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", | 		"sanity": "Santé mentale", | ||||||
| 		"start_import": "Démarrer l'importation de l'appareil", | 		"start_import": "Démarrer l'importation de l'appareil", | ||||||
| 		"test_batch": "Tester les données d'importation", | 		"test_batch": "Tester les données d'importation", | ||||||
| @@ -682,15 +671,7 @@ | |||||||
| 		"test_digicert_creds": "Tester les informations d'identification", | 		"test_digicert_creds": "Tester les informations d'identification", | ||||||
| 		"title": "Entités", | 		"title": "Entités", | ||||||
| 		"tree": "Arborescence des entités", | 		"tree": "Arborescence des entités", | ||||||
| 		"update_success": "Entité mise à jour !", | 		"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." | ||||||
| 		"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": { | 	"footer": { | ||||||
| 		"powered_by": "Alimenté par", | 		"powered_by": "Alimenté par", | ||||||
| @@ -784,17 +765,13 @@ | |||||||
| 		"city": "Ville", | 		"city": "Ville", | ||||||
| 		"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous", | 		"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous", | ||||||
| 		"country": "Pays", | 		"country": "Pays", | ||||||
| 		"elevation": "Élévation", |  | ||||||
| 		"geocode": "Geo code", | 		"geocode": "Geo code", | ||||||
| 		"lat": "Latitude", |  | ||||||
| 		"longitude": "Longitude", |  | ||||||
| 		"one": "Emplacement", | 		"one": "Emplacement", | ||||||
| 		"other": "Emplacements", | 		"other": "Emplacements", | ||||||
| 		"postal": "Zip / code postal", | 		"postal": "Zip / code postal", | ||||||
| 		"state": "Etat / Province", | 		"state": "Etat / Province", | ||||||
| 		"title": "Emplacements", | 		"title": "Emplacements", | ||||||
| 		"to_claim": "Emplacements à réclamer", | 		"to_claim": "Emplacements à réclamer" | ||||||
| 		"view_gps": "" |  | ||||||
| 	}, | 	}, | ||||||
| 	"login": { | 	"login": { | ||||||
| 		"access_policy": "Politique d'accès", | 		"access_policy": "Politique d'accès", | ||||||
| @@ -820,7 +797,6 @@ | |||||||
| 		"reset_password": "Réinitialiser le mot de passe", | 		"reset_password": "Réinitialiser le mot de passe", | ||||||
| 		"sign_in": "se connecter", | 		"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", | 		"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", | 		"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", | 		"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!", | 		"welcome_back": "Nous saluons le retour!", | ||||||
| @@ -926,7 +902,7 @@ | |||||||
| 		"dfs": "Remplacement DFS", | 		"dfs": "Remplacement DFS", | ||||||
| 		"gw_commands": "Commandes de passerelle", | 		"gw_commands": "Commandes de passerelle", | ||||||
| 		"identifier": "Identifiant", | 		"identifier": "Identifiant", | ||||||
| 		"key_verification": "Signature des informations clés", | 		"key_verification": "Vérification de la clé de signature", | ||||||
| 		"restricted": "Limité", | 		"restricted": "Limité", | ||||||
| 		"signed_upgrade": "Mise à niveau signée uniquement", | 		"signed_upgrade": "Mise à niveau signée uniquement", | ||||||
| 		"title": "Restrictions", | 		"title": "Restrictions", | ||||||
| @@ -1046,7 +1022,6 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
| 		"backend_logs": "Journaux principaux", | 		"backend_logs": "Journaux principaux", | ||||||
| 		"configuration": "Configuration", |  | ||||||
| 		"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ", | 		"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ", | ||||||
| 		"endpoint": "Point final", | 		"endpoint": "Point final", | ||||||
| 		"hostname": "nom d'hôte", | 		"hostname": "nom d'hôte", | ||||||
| @@ -1057,10 +1032,6 @@ | |||||||
| 		"os": "Système opérateur", | 		"os": "Système opérateur", | ||||||
| 		"processors": "Processeurs", | 		"processors": "Processeurs", | ||||||
| 		"reload_chosen_subsystems": "Recharger les sous-systèmes choisis", | 		"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", | 		"start": "Début", | ||||||
| 		"subsystems": "Sous-systèmes", | 		"subsystems": "Sous-systèmes", | ||||||
| 		"success_reload": "Commande de rechargement envoyée avec succès !", | 		"success_reload": "Commande de rechargement envoyée avec succès !", | ||||||
| @@ -1082,11 +1053,9 @@ | |||||||
| 		"previous_page": "Page précédente" | 		"previous_page": "Page précédente" | ||||||
| 	}, | 	}, | ||||||
| 	"user": { | 	"user": { | ||||||
| 		"email_not_validated": "Mail non valide", |  | ||||||
| 		"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}", | 		"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}", | ||||||
| 		"password": "Mot de passe", | 		"password": "Mot de passe", | ||||||
| 		"role": "Rôle", | 		"role": "Rôle", | ||||||
| 		"suspended": "Suspendu", |  | ||||||
| 		"title": "Utilisateur" | 		"title": "Utilisateur" | ||||||
| 	}, | 	}, | ||||||
| 	"users": { | 	"users": { | ||||||
| @@ -1131,11 +1100,9 @@ | |||||||
| 		"successfully_update_devices": "Mise à jour de {{num}}  appareils !", | 		"successfully_update_devices": "Mise à jour de {{num}}  appareils !", | ||||||
| 		"title": "Les lieux", | 		"title": "Les lieux", | ||||||
| 		"update_all_devices": "Mettre à jour toutes les configurations de périphérique", | 		"update_all_devices": "Mettre à jour toutes les configurations de périphérique", | ||||||
| 		"update_success": "Lieu mis à jour !", | 		"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel", | ||||||
| 		"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_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_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": "Utiliser l'existant", | ||||||
| 		"use_existing_contacts": "Utiliser les contacts existants", | 		"use_existing_contacts": "Utiliser les contacts existants", | ||||||
| 		"use_this_contact": "Utilisez ce contact" | 		"use_this_contact": "Utilisez ce contact" | ||||||
|   | |||||||
| @@ -79,11 +79,8 @@ | |||||||
| 		"live_view_help": "Ajuda da visualização ao vivo", | 		"live_view_help": "Ajuda da visualização ao vivo", | ||||||
| 		"memory": "Memória", | 		"memory": "Memória", | ||||||
| 		"memory_used": "Memória Usada", | 		"memory_used": "Memória Usada", | ||||||
| 		"missing_board": "O monitoramento analítico neste local não está mais ativo. Clique aqui para reiniciar o monitoramento", | 		"missing_board": "O monitoramento analítico neste local não está mais ativo, reinicie o monitoramento usando o menu superior", | ||||||
| 		"mode": "Modo", | 		"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", | 		"noise": "Barulho", | ||||||
| 		"packets": "Pacotes", | 		"packets": "Pacotes", | ||||||
| 		"radio": "Rádio", | 		"radio": "Rádio", | ||||||
| @@ -94,8 +91,6 @@ | |||||||
| 		"retries": "Novas tentativas", | 		"retries": "Novas tentativas", | ||||||
| 		"search_serials": "Pesquisar séries", | 		"search_serials": "Pesquisar séries", | ||||||
| 		"stop_monitoring": "Parar o monitoramento", | 		"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", | 		"temperature": "Temperatura", | ||||||
| 		"title": "Analytics", | 		"title": "Analytics", | ||||||
| 		"total_data": "Dados totais", | 		"total_data": "Dados totais", | ||||||
| @@ -180,7 +175,6 @@ | |||||||
| 		"other": "comandos", | 		"other": "comandos", | ||||||
| 		"override_dfs": "Substituir DFS", | 		"override_dfs": "Substituir DFS", | ||||||
| 		"reboot": "Reiniciar", | 		"reboot": "Reiniciar", | ||||||
| 		"reboot_description": "Deseja reiniciar este dispositivo?", |  | ||||||
| 		"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}", | 		"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}", | ||||||
| 		"reboot_success": "Comando de reinicialização enviado com sucesso!", | 		"reboot_success": "Comando de reinicialização enviado com sucesso!", | ||||||
| 		"revision": "revisão", | 		"revision": "revisão", | ||||||
| @@ -397,7 +391,6 @@ | |||||||
| 		"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}", | 		"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}", | ||||||
| 		"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}", | 		"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}", | ||||||
| 		"weight": "Peso", | 		"weight": "Peso", | ||||||
| 		"wifi_bands_max": "Não pode haver mais de 8 SSIDs usando esta banda wi-fi", |  | ||||||
| 		"wifi_frames": "Quadros WiFi" | 		"wifi_frames": "Quadros WiFi" | ||||||
| 	}, | 	}, | ||||||
| 	"contacts": { | 	"contacts": { | ||||||
| @@ -607,7 +600,6 @@ | |||||||
| 		"certificate_expires_in": "Certificado expira em", | 		"certificate_expires_in": "Certificado expira em", | ||||||
| 		"certificate_expiry": "Certificado expira em", | 		"certificate_expiry": "Certificado expira em", | ||||||
| 		"connected": "Conectado", | 		"connected": "Conectado", | ||||||
| 		"crash_logs": "Registros de falhas", |  | ||||||
| 		"create_errors": "erros ao tentar criar dispositivos", | 		"create_errors": "erros ao tentar criar dispositivos", | ||||||
| 		"create_success": " dispositivos criados com sucesso", | 		"create_success": " dispositivos criados com sucesso", | ||||||
| 		"current_firmware": "Firmware atual", | 		"current_firmware": "Firmware atual", | ||||||
| @@ -621,7 +613,6 @@ | |||||||
| 		"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado", | 		"import_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", | 		"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)", | 		"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)", | ||||||
| 		"logs_one": "Registro", |  | ||||||
| 		"new_devices": "novos dispositivos", | 		"new_devices": "novos dispositivos", | ||||||
| 		"no_model_image": "Nenhuma imagem de modelo encontrada", | 		"no_model_image": "Nenhuma imagem de modelo encontrada", | ||||||
| 		"not_connected": "Não conectado", | 		"not_connected": "Não conectado", | ||||||
| @@ -630,8 +621,6 @@ | |||||||
| 		"one": "Dispositivo", | 		"one": "Dispositivo", | ||||||
| 		"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?", | 		"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?", | ||||||
| 		"restricted": "Restrito", | 		"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", | 		"sanity": "Sanidade", | ||||||
| 		"start_import": "Iniciar importação de dispositivos", | 		"start_import": "Iniciar importação de dispositivos", | ||||||
| 		"test_batch": "Dados de importação de teste", | 		"test_batch": "Dados de importação de teste", | ||||||
| @@ -682,15 +671,7 @@ | |||||||
| 		"test_digicert_creds": "Credenciais de teste", | 		"test_digicert_creds": "Credenciais de teste", | ||||||
| 		"title": "Entidades", | 		"title": "Entidades", | ||||||
| 		"tree": "Árvore de entidades", | 		"tree": "Árvore de entidades", | ||||||
| 		"update_success": "Entidade atualizada!", | 		"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz. Por favor, crie novas entidades e crie locais sob elas." | ||||||
| 		"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": { | 	"footer": { | ||||||
| 		"powered_by": "Distribuído por", | 		"powered_by": "Distribuído por", | ||||||
| @@ -784,17 +765,13 @@ | |||||||
| 		"city": "Cidade", | 		"city": "Cidade", | ||||||
| 		"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo", | 		"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo", | ||||||
| 		"country": "País", | 		"country": "País", | ||||||
| 		"elevation": "elevação", |  | ||||||
| 		"geocode": "Código geográfico", | 		"geocode": "Código geográfico", | ||||||
| 		"lat": "Latitude", |  | ||||||
| 		"longitude": "Longitude", |  | ||||||
| 		"one": "Localização", | 		"one": "Localização", | ||||||
| 		"other": "Localizações", | 		"other": "Localizações", | ||||||
| 		"postal": "CEP / Código Postal", | 		"postal": "CEP / Código Postal", | ||||||
| 		"state": "Estado / Província", | 		"state": "Estado / Província", | ||||||
| 		"title": "Localizações", | 		"title": "Localizações", | ||||||
| 		"to_claim": "Locais para reivindicar", | 		"to_claim": "Locais para reivindicar" | ||||||
| 		"view_gps": "" |  | ||||||
| 	}, | 	}, | ||||||
| 	"login": { | 	"login": { | ||||||
| 		"access_policy": "Política de Acesso", | 		"access_policy": "Política de Acesso", | ||||||
| @@ -820,7 +797,6 @@ | |||||||
| 		"reset_password": "Redefinir senha", | 		"reset_password": "Redefinir senha", | ||||||
| 		"sign_in": "assinar em", | 		"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", | 		"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", | 		"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", | 		"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!", | 		"welcome_back": "Bem vindo de volta!", | ||||||
| @@ -926,7 +902,7 @@ | |||||||
| 		"dfs": "Substituição DFS", | 		"dfs": "Substituição DFS", | ||||||
| 		"gw_commands": "Comandos de gateway", | 		"gw_commands": "Comandos de gateway", | ||||||
| 		"identifier": "Identificador", | 		"identifier": "Identificador", | ||||||
| 		"key_verification": "Informações Chave de Assinatura", | 		"key_verification": "Verificação da chave de assinatura", | ||||||
| 		"restricted": "Restrito", | 		"restricted": "Restrito", | ||||||
| 		"signed_upgrade": "Somente atualização assinada", | 		"signed_upgrade": "Somente atualização assinada", | ||||||
| 		"title": "RESTRIÇÕES", | 		"title": "RESTRIÇÕES", | ||||||
| @@ -1046,7 +1022,6 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
| 		"backend_logs": "Registros de back-end", | 		"backend_logs": "Registros de back-end", | ||||||
| 		"configuration": "Configuração", |  | ||||||
| 		"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema", | 		"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema", | ||||||
| 		"endpoint": "Ponto final", | 		"endpoint": "Ponto final", | ||||||
| 		"hostname": "Nome de anfitrião", | 		"hostname": "Nome de anfitrião", | ||||||
| @@ -1057,10 +1032,6 @@ | |||||||
| 		"os": "Sistema Operacional", | 		"os": "Sistema Operacional", | ||||||
| 		"processors": "Processadores", | 		"processors": "Processadores", | ||||||
| 		"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos", | 		"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos", | ||||||
| 		"secrets": "Segredos", |  | ||||||
| 		"secrets_create": "Criar Segredo", |  | ||||||
| 		"secrets_one": "Segredo", |  | ||||||
| 		"services": "Serviços", |  | ||||||
| 		"start": "Começar", | 		"start": "Começar", | ||||||
| 		"subsystems": "Subsistemas", | 		"subsystems": "Subsistemas", | ||||||
| 		"success_reload": "Comando de recarga enviado com sucesso!", | 		"success_reload": "Comando de recarga enviado com sucesso!", | ||||||
| @@ -1082,11 +1053,9 @@ | |||||||
| 		"previous_page": "Página anterior" | 		"previous_page": "Página anterior" | ||||||
| 	}, | 	}, | ||||||
| 	"user": { | 	"user": { | ||||||
| 		"email_not_validated": "e-mail não validado", |  | ||||||
| 		"error_fetching": "Erro ao buscar informações do usuário: {{e}}", | 		"error_fetching": "Erro ao buscar informações do usuário: {{e}}", | ||||||
| 		"password": "Senha", | 		"password": "Senha", | ||||||
| 		"role": "Função", | 		"role": "Função", | ||||||
| 		"suspended": "Suspenso", |  | ||||||
| 		"title": "Do utilizador" | 		"title": "Do utilizador" | ||||||
| 	}, | 	}, | ||||||
| 	"users": { | 	"users": { | ||||||
| @@ -1131,11 +1100,9 @@ | |||||||
| 		"successfully_update_devices": "Atualizando {{num}} dispositivos!", | 		"successfully_update_devices": "Atualizando {{num}} dispositivos!", | ||||||
| 		"title": "Locais", | 		"title": "Locais", | ||||||
| 		"update_all_devices": "Atualizar todas as configurações do dispositivo", | 		"update_all_devices": "Atualizar todas as configurações do dispositivo", | ||||||
| 		"update_success": "Local atualizado!", | 		"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente", | ||||||
| 		"upgrade_all_devices": "Atualize o firmware de todos os dispositivos", |  | ||||||
| 		"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}", | 		"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}", | ||||||
| 		"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!", | 		"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": "Usar existente", | ||||||
| 		"use_existing_contacts": "Usar contatos existentes", | 		"use_existing_contacts": "Usar contatos existentes", | ||||||
| 		"use_this_contact": "Use este contato" | 		"use_this_contact": "Use este contato" | ||||||
|   | |||||||
| @@ -1,42 +1,40 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Alert, AlertIcon, Box, Button, Center, useToast } from '@chakra-ui/react'; | import { MenuItem, useToast } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { Modal } from '../Modal'; |  | ||||||
| import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore'; | import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore'; | ||||||
| import { useRebootDevice } from 'hooks/Network/Devices'; | import { useRebootDevice } from 'hooks/Network/Devices'; | ||||||
| import { useMutationResult } from 'hooks/useMutationResult'; | import { useMutationResult } from 'hooks/useMutationResult'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
|  | import { GatewayDevice } from 'models/Device'; | ||||||
| 
 | 
 | ||||||
| export type RebootModalProps = { | type Props = { | ||||||
|   serialNumber: string; |   device: GatewayDevice; | ||||||
|   modalProps: { |   refresh: () => void; | ||||||
|     isOpen: boolean; |  | ||||||
|     onClose: () => void; |  | ||||||
|   }; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => { | const RebootMenuItem = ({ device, refresh }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const addEventListeners = useControllerStore((state) => state.addEventListeners); |   const addEventListeners = useControllerStore((state) => state.addEventListeners); | ||||||
|   const { mutateAsync: reboot, isLoading } = useRebootDevice({ serialNumber }); |   const { mutateAsync: reboot } = useRebootDevice({ serialNumber: device.serialNumber }); | ||||||
|   const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({ |   const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({ | ||||||
|     objName: t('devices.one'), |     objName: t('devices.one'), | ||||||
|     operationType: 'reboot', |     operationType: 'reboot', | ||||||
|     refresh: () => { |     refresh: () => { | ||||||
|  |       refresh(); | ||||||
|       addEventListeners([ |       addEventListeners([ | ||||||
|         { |         { | ||||||
|           id: `device-connection-${serialNumber}`, |           id: `device-connection-${device.serialNumber}`, | ||||||
|           type: 'DEVICE_CONNECTION', |           type: 'DEVICE_CONNECTION', | ||||||
|           serialNumber, |           serialNumber: device.serialNumber, | ||||||
|           callback: () => { |           callback: () => { | ||||||
|             const id = `device-connection-notification-${serialNumber}`; |             const id = `device-connection-notification-${device.serialNumber}`; | ||||||
| 
 | 
 | ||||||
|             if (!toast.isActive(id)) { |             if (!toast.isActive(id)) { | ||||||
|               toast({ |               toast({ | ||||||
|                 id, |                 id, | ||||||
|                 title: t('common.success'), |                 title: t('common.success'), | ||||||
|                 description: t('controller.devices.finished_reboot', { serialNumber }), |                 description: t('controller.devices.finished_reboot', { serialNumber: device.serialNumber }), | ||||||
|                 status: 'success', |                 status: 'success', | ||||||
|                 duration: 5000, |                 duration: 5000, | ||||||
|                 isClosable: true, |                 isClosable: true, | ||||||
| @@ -46,17 +44,17 @@ export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => { | |||||||
|           }, |           }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           id: `device-disconnected-${serialNumber}`, |           id: `device-disconnected-${device.serialNumber}`, | ||||||
|           type: 'DEVICE_DISCONNECTION', |           type: 'DEVICE_DISCONNECTION', | ||||||
|           serialNumber, |           serialNumber: device.serialNumber, | ||||||
|           callback: () => { |           callback: () => { | ||||||
|             const id = `device-disconnection-notification-${serialNumber}`; |             const id = `device-disconnection-notification-${device.serialNumber}`; | ||||||
| 
 | 
 | ||||||
|             if (!toast.isActive(id)) { |             if (!toast.isActive(id)) { | ||||||
|               toast({ |               toast({ | ||||||
|                 id, |                 id, | ||||||
|                 title: t('common.success'), |                 title: t('common.success'), | ||||||
|                 description: t('controller.devices.started_reboot', { serialNumber }), |                 description: t('controller.devices.started_reboot', { serialNumber: device.serialNumber }), | ||||||
|                 status: 'success', |                 status: 'success', | ||||||
|                 duration: 5000, |                 duration: 5000, | ||||||
|                 isClosable: true, |                 isClosable: true, | ||||||
| @@ -68,39 +66,17 @@ export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => { | |||||||
|       ]); |       ]); | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| 
 |  | ||||||
|   const handleRebootClick = () => |   const handleRebootClick = () => | ||||||
|     reboot(undefined, { |     reboot(undefined, { | ||||||
|       onSuccess: () => { |       onSuccess: () => { | ||||||
|         onRebootSuccess(); |         onRebootSuccess(); | ||||||
|         modalProps.onClose(); |  | ||||||
|       }, |       }, | ||||||
|       onError: (e) => { |       onError: (e) => { | ||||||
|         onRebootError(e as AxiosError); |         onRebootError(e as AxiosError); | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|   return ( |   return <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>; | ||||||
|     <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; | ||||||
| @@ -1,19 +1,9 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { | import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip, useToast } from '@chakra-ui/react'; | ||||||
|   Button, |  | ||||||
|   IconButton, |  | ||||||
|   Menu, |  | ||||||
|   MenuButton, |  | ||||||
|   MenuItem, |  | ||||||
|   MenuList, |  | ||||||
|   Portal, |  | ||||||
|   Spinner, |  | ||||||
|   Tooltip, |  | ||||||
|   useToast, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| import { Wrench } from 'phosphor-react'; | import { Wrench } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import RebootMenuItem from './RebootButton'; | ||||||
| import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore'; | import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore'; | ||||||
| import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices'; | import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices'; | ||||||
| import { useUpdateDeviceToLatest } from 'hooks/Network/Firmware'; | import { useUpdateDeviceToLatest } from 'hooks/Network/Firmware'; | ||||||
| @@ -32,7 +22,6 @@ interface Props { | |||||||
|   onOpenConfigureModal: (serialNumber: string) => void; |   onOpenConfigureModal: (serialNumber: string) => void; | ||||||
|   onOpenTelemetryModal: (serialNumber: string) => void; |   onOpenTelemetryModal: (serialNumber: string) => void; | ||||||
|   onOpenScriptModal: (device: GatewayDevice) => void; |   onOpenScriptModal: (device: GatewayDevice) => void; | ||||||
|   onOpenRebootModal: (serialNumber: string) => void; |  | ||||||
|   size?: 'sm' | 'md' | 'lg'; |   size?: 'sm' | 'md' | 'lg'; | ||||||
|   isCompact?: boolean; |   isCompact?: boolean; | ||||||
| } | } | ||||||
| @@ -49,7 +38,6 @@ const DeviceActionDropdown = ({ | |||||||
|   onOpenTelemetryModal, |   onOpenTelemetryModal, | ||||||
|   onOpenConfigureModal, |   onOpenConfigureModal, | ||||||
|   onOpenScriptModal, |   onOpenScriptModal, | ||||||
|   onOpenRebootModal, |  | ||||||
|   size, |   size, | ||||||
|   isCompact, |   isCompact, | ||||||
| }: Props) => { | }: Props) => { | ||||||
| @@ -157,9 +145,7 @@ const DeviceActionDropdown = ({ | |||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleConnectClick = () => getRtty(); |   const handleConnectClick = () => getRtty(); | ||||||
|   const handleRebootClick = () => onOpenRebootModal(device.serialNumber); |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Menu> |     <Menu> | ||||||
| @@ -186,7 +172,6 @@ const DeviceActionDropdown = ({ | |||||||
|           </MenuButton> |           </MenuButton> | ||||||
|         )} |         )} | ||||||
|       </Tooltip> |       </Tooltip> | ||||||
|       <Portal> |  | ||||||
|       <MenuList> |       <MenuList> | ||||||
|         <MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem> |         <MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem> | ||||||
|         <MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem> |         <MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem> | ||||||
| @@ -194,7 +179,7 @@ const DeviceActionDropdown = ({ | |||||||
|         <MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem> |         <MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem> | ||||||
|         <MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem> |         <MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem> | ||||||
|         <MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem> |         <MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem> | ||||||
|           <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem> |         <RebootMenuItem device={device} refresh={refresh} /> | ||||||
|         <MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem> |         <MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem> | ||||||
|         <MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem> |         <MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem> | ||||||
|         <MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem> |         <MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem> | ||||||
| @@ -203,7 +188,6 @@ const DeviceActionDropdown = ({ | |||||||
|         </MenuItem> |         </MenuItem> | ||||||
|         <MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem> |         <MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem> | ||||||
|       </MenuList> |       </MenuList> | ||||||
|       </Portal> |  | ||||||
|     </Menu> |     </Menu> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -53,7 +53,6 @@ export type DataTableProps = { | |||||||
|   obj?: string; |   obj?: string; | ||||||
|   sortBy?: { id: string; desc: boolean }[]; |   sortBy?: { id: string; desc: boolean }[]; | ||||||
|   hiddenColumns?: string[]; |   hiddenColumns?: string[]; | ||||||
|   hideEmptyListText?: boolean; |  | ||||||
|   hideControls?: boolean; |   hideControls?: boolean; | ||||||
|   minHeight?: string | number; |   minHeight?: string | number; | ||||||
|   fullScreen?: boolean; |   fullScreen?: boolean; | ||||||
| @@ -78,7 +77,6 @@ const _DataTable = ({ | |||||||
|   sortBy, |   sortBy, | ||||||
|   hiddenColumns, |   hiddenColumns, | ||||||
|   hideControls, |   hideControls, | ||||||
|   hideEmptyListText, |  | ||||||
|   count, |   count, | ||||||
|   setPageInfo, |   setPageInfo, | ||||||
|   isManual, |   isManual, | ||||||
| @@ -88,7 +86,6 @@ const _DataTable = ({ | |||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|   const textColor = useColorModeValue('gray.700', 'white'); |   const textColor = useColorModeValue('gray.700', 'white'); | ||||||
|   const hoveredRowBg = useColorModeValue('gray.100', 'gray.600'); |  | ||||||
|   const getPageSize = () => { |   const getPageSize = () => { | ||||||
|     try { |     try { | ||||||
|       if (showAllRows) return 1000000; |       if (showAllRows) return 1000000; | ||||||
| @@ -145,10 +142,6 @@ const _DataTable = ({ | |||||||
|     usePagination, |     usePagination, | ||||||
|   ) as TableInstanceWithHooks<object>; |   ) as TableInstanceWithHooks<object>; | ||||||
|  |  | ||||||
|   const handleGoToPage = (newPage: number) => { |  | ||||||
|     if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage)); |  | ||||||
|     gotoPage(newPage); |  | ||||||
|   }; |  | ||||||
|   const handleNextPage = () => { |   const handleNextPage = () => { | ||||||
|     nextPage(); |     nextPage(); | ||||||
|     if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1)); |     if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1)); | ||||||
| @@ -263,13 +256,7 @@ const _DataTable = ({ | |||||||
|                 {page.map((row: Row) => { |                 {page.map((row: Row) => { | ||||||
|                   prepareRow(row); |                   prepareRow(row); | ||||||
|                   return ( |                   return ( | ||||||
|                     <Tr |                     <Tr {...row.getRowProps()} key={uuid()}> | ||||||
|                       {...row.getRowProps()} |  | ||||||
|                       key={uuid()} |  | ||||||
|                       _hover={{ |  | ||||||
|                         backgroundColor: hoveredRowBg, |  | ||||||
|                       }} |  | ||||||
|                     > |  | ||||||
|                       { |                       { | ||||||
|                         // @ts-ignore |                         // @ts-ignore | ||||||
|                         row.cells.map((cell) => ( |                         row.cells.map((cell) => ( | ||||||
| @@ -301,7 +288,7 @@ const _DataTable = ({ | |||||||
|               </Tbody> |               </Tbody> | ||||||
|             )} |             )} | ||||||
|           </Table> |           </Table> | ||||||
|           {!isLoading && data.length === 0 && !hideEmptyListText && ( |           {!isLoading && data.length === 0 && ( | ||||||
|             <Center> |             <Center> | ||||||
|               {obj ? ( |               {obj ? ( | ||||||
|                 <Heading size="md" pt={12}> |                 <Heading size="md" pt={12}> | ||||||
| @@ -322,7 +309,7 @@ const _DataTable = ({ | |||||||
|             <Tooltip label={t('table.first_page')}> |             <Tooltip label={t('table.first_page')}> | ||||||
|               <IconButton |               <IconButton | ||||||
|                 aria-label="Go to first page" |                 aria-label="Go to first page" | ||||||
|                 onClick={() => handleGoToPage(0)} |                 onClick={() => gotoPage(0)} | ||||||
|                 isDisabled={!canPreviousPage} |                 isDisabled={!canPreviousPage} | ||||||
|                 icon={<ArrowLeftIcon h={3} w={3} />} |                 icon={<ArrowLeftIcon h={3} w={3} />} | ||||||
|                 mr={4} |                 mr={4} | ||||||
| @@ -360,7 +347,7 @@ const _DataTable = ({ | |||||||
|                   max={pageOptions.length} |                   max={pageOptions.length} | ||||||
|                   onChange={(_: unknown, numberValue: number) => { |                   onChange={(_: unknown, numberValue: number) => { | ||||||
|                     const newPage = numberValue ? numberValue - 1 : 0; |                     const newPage = numberValue ? numberValue - 1 : 0; | ||||||
|                     handleGoToPage(newPage); |                     gotoPage(newPage); | ||||||
|                   }} |                   }} | ||||||
|                   defaultValue={pageIndex + 1} |                   defaultValue={pageIndex + 1} | ||||||
|                 > |                 > | ||||||
| @@ -399,7 +386,7 @@ const _DataTable = ({ | |||||||
|             <Tooltip label={t('table.last_page')}> |             <Tooltip label={t('table.last_page')}> | ||||||
|               <IconButton |               <IconButton | ||||||
|                 aria-label="Go to last page" |                 aria-label="Go to last page" | ||||||
|                 onClick={() => handleGoToPage(pageCount - 1)} |                 onClick={() => gotoPage(pageCount - 1)} | ||||||
|                 isDisabled={!canNextPage} |                 isDisabled={!canNextPage} | ||||||
|                 icon={<ArrowRightIcon h={3} w={3} />} |                 icon={<ArrowRightIcon h={3} w={3} />} | ||||||
|                 ml={4} |                 ml={4} | ||||||
|   | |||||||
| @@ -100,7 +100,6 @@ const SortableDataTable: React.FC<Props> = ({ | |||||||
| }) => { | }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|   const hoveredRowBg = useColorModeValue('gray.100', 'gray.600'); |  | ||||||
|   const textColor = useColorModeValue('gray.700', 'white'); |   const textColor = useColorModeValue('gray.700', 'white'); | ||||||
|   const getPageSize = () => { |   const getPageSize = () => { | ||||||
|     const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined; |     const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined; | ||||||
| @@ -224,13 +223,7 @@ const SortableDataTable: React.FC<Props> = ({ | |||||||
|                 {page.map((row: Row) => { |                 {page.map((row: Row) => { | ||||||
|                   prepareRow(row); |                   prepareRow(row); | ||||||
|                   return ( |                   return ( | ||||||
|                     <Tr |                     <Tr {...row.getRowProps()} key={uuid()}> | ||||||
|                       {...row.getRowProps()} |  | ||||||
|                       key={uuid()} |  | ||||||
|                       _hover={{ |  | ||||||
|                         backgroundColor: hoveredRowBg, |  | ||||||
|                       }} |  | ||||||
|                     > |  | ||||||
|                       { |                       { | ||||||
|                         // @ts-ignore |                         // @ts-ignore | ||||||
|                         row.cells.map((cell) => ( |                         row.cells.map((cell) => ( | ||||||
|   | |||||||
| @@ -1,27 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
|  |  | ||||||
| const _GoogleMapMarker = (options: google.maps.MarkerOptions) => { |  | ||||||
|   const [marker, setMarker] = React.useState<google.maps.Marker>(); |  | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     if (!marker) { |  | ||||||
|       setMarker(new google.maps.Marker()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return () => { |  | ||||||
|       if (marker) { |  | ||||||
|         marker.setMap(null); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|   }, [marker]); |  | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     if (marker) { |  | ||||||
|       marker.setOptions(options); |  | ||||||
|     } |  | ||||||
|   }, [marker, options]); |  | ||||||
|  |  | ||||||
|   return null; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const GoogleMapMarker = React.memo(_GoogleMapMarker); |  | ||||||
| @@ -1,89 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { isLatLngLiteral } from '@googlemaps/typescript-guards'; |  | ||||||
| import { createCustomEqual } from 'fast-equals'; |  | ||||||
|  |  | ||||||
| const deepCompareEqualsForMaps = createCustomEqual((deepEqual) => |  | ||||||
|   // @ts-ignore |  | ||||||
|   (a: number | google.maps.LatLng | google.maps.LatLngLiteral, b: number | google.maps.LatLng | google.maps.LatLngLiteral) => { |  | ||||||
|     if ( |  | ||||||
|       isLatLngLiteral(a) || |  | ||||||
|       a instanceof google.maps.LatLng || |  | ||||||
|       isLatLngLiteral(b) || |  | ||||||
|       b instanceof google.maps.LatLng |  | ||||||
|     ) { |  | ||||||
|       return new google.maps.LatLng(a).equals(new google.maps.LatLng(b)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // @ts-ignore |  | ||||||
|     return deepEqual(a, b); |  | ||||||
|   }, |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| const useDeepCompareMemoize = (value: unknown) => { |  | ||||||
|   const ref = React.useRef<unknown>(); |  | ||||||
|  |  | ||||||
|   if (!deepCompareEqualsForMaps(value, ref.current)) { |  | ||||||
|     ref.current = value; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ref.current; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const useDeepCompareEffectForMaps = (callback: React.EffectCallback, dependencies: unknown[]) => { |  | ||||||
|   React.useEffect(callback, dependencies.map(useDeepCompareMemoize)); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export interface GoogleMapProps extends google.maps.MapOptions { |  | ||||||
|   style: { [key: string]: string }; |  | ||||||
|   onClick?: (e: google.maps.MapMouseEvent) => void; |  | ||||||
|   onIdle?: (map: google.maps.Map) => void; |  | ||||||
|   children?: React.ReactNode; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const _GoogleMap = ({ style, onClick, onIdle, children, ...options }: GoogleMapProps) => { |  | ||||||
|   const ref = React.useRef<HTMLDivElement>(null); |  | ||||||
|   const [map, setMap] = React.useState<google.maps.Map>(); |  | ||||||
|  |  | ||||||
|   // because React does not do deep comparisons, a custom hook is used |  | ||||||
|   useDeepCompareEffectForMaps(() => { |  | ||||||
|     if (map) { |  | ||||||
|       map.setOptions(options); |  | ||||||
|     } |  | ||||||
|   }, [map, options]); |  | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     if (ref.current && !map) { |  | ||||||
|       setMap(new window.google.maps.Map(ref.current, {})); |  | ||||||
|     } |  | ||||||
|   }, [ref, map]); |  | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     if (map) { |  | ||||||
|       ['click', 'idle'].forEach((eventName) => google.maps.event.clearListeners(map, eventName)); |  | ||||||
|  |  | ||||||
|       if (onClick) { |  | ||||||
|         map.addListener('click', onClick); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (onIdle) { |  | ||||||
|         map.addListener('idle', () => onIdle(map)); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }, [map, onClick, onIdle]); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <div ref={ref} style={style} /> |  | ||||||
|       {React.Children.map(children, (child) => { |  | ||||||
|         if (React.isValidElement(child)) { |  | ||||||
|           // set the map prop on the child component |  | ||||||
|           // @ts-ignore |  | ||||||
|           return React.cloneElement(child, { map }); |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|       })} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const GoogleMap = React.memo(_GoogleMap); |  | ||||||
| @@ -57,8 +57,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu | |||||||
|     upgrade({ |     upgrade({ | ||||||
|       keepRedirector: isRedirector, |       keepRedirector: isRedirector, | ||||||
|       uri, |       uri, | ||||||
|       signature: |       signature: device?.restrictedDevice ? ref.current?.values?.signature : undefined, | ||||||
|         device?.restrictedDevice && !device?.restrictionDetails?.developer ? ref.current?.values?.signature : undefined, |  | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -90,7 +89,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu | |||||||
|                 </FormLabel> |                 </FormLabel> | ||||||
|                 <Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" /> |                 <Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" /> | ||||||
|               </FormControl> |               </FormControl> | ||||||
|               {device?.restrictedDevice && !device?.restrictionDetails?.developer && ( |               {device?.restrictedDevice && ( | ||||||
|                 <Formik<{ signature?: string }> |                 <Formik<{ signature?: string }> | ||||||
|                   innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined} |                   innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined} | ||||||
|                   key={formKey} |                   key={formKey} | ||||||
|   | |||||||
| @@ -1,19 +1,15 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react'; | import { Flex, ModalHeader as Header, Spacer } from '@chakra-ui/react'; | ||||||
|  |  | ||||||
| export interface ModalHeaderProps { | export interface ModalHeaderProps { | ||||||
|   title: string; |   title: string; | ||||||
|   left?: React.ReactNode; |  | ||||||
|   right: React.ReactNode; |   right: React.ReactNode; | ||||||
| } | } | ||||||
|  |  | ||||||
| const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => ( | const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, right }) => ( | ||||||
|   <Header> |   <Header> | ||||||
|     <Flex justifyContent="center" alignItems="center" maxW="100%" px={1}> |     <Flex justifyContent="center" alignItems="center" maxW="100%" px={1}> | ||||||
|       {title} |       {title} | ||||||
|       <HStack spacing={2} ml={2}> |  | ||||||
|         {left ?? null} |  | ||||||
|       </HStack> |  | ||||||
|       <Spacer /> |       <Spacer /> | ||||||
|       {right} |       {right} | ||||||
|     </Flex> |     </Flex> | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ export type ModalProps = { | |||||||
|   onClose: () => void; |   onClose: () => void; | ||||||
|   title: string; |   title: string; | ||||||
|   topRightButtons?: React.ReactNode; |   topRightButtons?: React.ReactNode; | ||||||
|   tags?: React.ReactNode; |  | ||||||
|   options?: { |   options?: { | ||||||
|     modalSize?: 'sm' | 'md' | 'lg'; |     modalSize?: 'sm' | 'md' | 'lg'; | ||||||
|     maxWidth?: LayoutProps['maxWidth']; |     maxWidth?: LayoutProps['maxWidth']; | ||||||
| @@ -16,7 +15,7 @@ export type ModalProps = { | |||||||
|   children: React.ReactElement; |   children: React.ReactElement; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, children }: ModalProps) => { | const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => { | ||||||
|   const maxWidth = React.useMemo(() => { |   const maxWidth = React.useMemo(() => { | ||||||
|     if (options?.maxWidth) return options.maxWidth; |     if (options?.maxWidth) return options.maxWidth; | ||||||
|     if (options?.modalSize === 'sm') return undefined; |     if (options?.modalSize === 'sm') return undefined; | ||||||
| @@ -33,7 +32,6 @@ const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, childr | |||||||
|       <ModalContent maxWidth={maxWidth}> |       <ModalContent maxWidth={maxWidth}> | ||||||
|         <ModalHeader |         <ModalHeader | ||||||
|           title={title} |           title={title} | ||||||
|           left={tags} |  | ||||||
|           right={ |           right={ | ||||||
|             <HStack spacing={2}> |             <HStack spacing={2}> | ||||||
|               {topRightButtons} |               {topRightButtons} | ||||||
|   | |||||||
| @@ -183,9 +183,7 @@ const CustomScriptForm = ({ | |||||||
|             <> |             <> | ||||||
|               <Flex> |               <Flex> | ||||||
|                 <Box> |                 <Box> | ||||||
|                   {device?.restrictedDevice && !device?.restrictionDetails?.developer && ( |                   {device?.restrictedDevice && <SignatureField name="signature" isDisabled={areFieldsDisabled} />} | ||||||
|                     <SignatureField name="signature" isDisabled={areFieldsDisabled} /> |  | ||||||
|                   )} |  | ||||||
|                 </Box> |                 </Box> | ||||||
|               </Flex> |               </Flex> | ||||||
|               <SelectField |               <SelectField | ||||||
|   | |||||||
| @@ -76,7 +76,7 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => { | |||||||
|         when: 0, |         when: 0, | ||||||
|         deferred: data.deferred, |         deferred: data.deferred, | ||||||
|         timeout: data.timeout, |         timeout: data.timeout, | ||||||
|         signature: device?.restrictedDevice && !device?.restrictionDetails?.developer ? data.signature : undefined, |         signature: device?.restrictedDevice ? data.signature : undefined, | ||||||
|         uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined, |         uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined, | ||||||
|         scriptId: selectedScript, |         scriptId: selectedScript, | ||||||
|         type: data.type, |         type: data.type, | ||||||
|   | |||||||
| @@ -149,8 +149,6 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({ | |||||||
|               connectedDevices: msg.statistics.numberOfDevices, |               connectedDevices: msg.statistics.numberOfDevices, | ||||||
|               connectingDevices: msg.statistics.numberOfConnectingDevices, |               connectingDevices: msg.statistics.numberOfConnectingDevices, | ||||||
|               averageConnectionTime: msg.statistics.averageConnectedTime, |               averageConnectionTime: msg.statistics.averageConnectedTime, | ||||||
|               tx: msg.statistics.tx, |  | ||||||
|               rx: msg.statistics.rx, |  | ||||||
|             }, |             }, | ||||||
|             queryClient, |             queryClient, | ||||||
|           ); |           ); | ||||||
|   | |||||||
| @@ -52,8 +52,6 @@ type ConnectionStatisticsMessage = { | |||||||
|       numberOfDevices: number; |       numberOfDevices: number; | ||||||
|       numberOfConnectingDevices: number; |       numberOfConnectingDevices: number; | ||||||
|       averageConnectedTime: number; |       averageConnectedTime: number; | ||||||
|       tx: number; |  | ||||||
|       rx: number; |  | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   serialNumbers?: undefined; |   serialNumbers?: undefined; | ||||||
| @@ -87,8 +85,6 @@ export type SocketWebSocketNotificationData = | |||||||
|         numberOfDevices: number; |         numberOfDevices: number; | ||||||
|         numberOfConnectingDevices: number; |         numberOfConnectingDevices: number; | ||||||
|         averageConnectedTime: number; |         averageConnectedTime: number; | ||||||
|         rx: number; |  | ||||||
|         tx: number; |  | ||||||
|       }; |       }; | ||||||
|       serialNumber?: undefined; |       serialNumber?: undefined; | ||||||
|       log?: undefined; |       log?: undefined; | ||||||
|   | |||||||
| @@ -11,10 +11,8 @@ export type DeviceLog = { | |||||||
|   severity: number; |   severity: number; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1) => async () => | const getDeviceLogs = (limit: number, serialNumber?: string) => async () => | ||||||
|   axiosGw |   axiosGw.get(`device/${serialNumber}/logs?newest=true&limit=${limit}`).then((response) => response.data) as Promise<{ | ||||||
|     .get(`device/${serialNumber}/logs?newest=true&limit=${limit}&logType=${logType}`) |  | ||||||
|     .then((response) => response.data) as Promise<{ |  | ||||||
|     values: DeviceLog[]; |     values: DeviceLog[]; | ||||||
|     serialNumber: string; |     serialNumber: string; | ||||||
|   }>; |   }>; | ||||||
| @@ -23,29 +21,20 @@ export const useGetDeviceLogs = ({ | |||||||
|   serialNumber, |   serialNumber, | ||||||
|   limit, |   limit, | ||||||
|   onError, |   onError, | ||||||
|   logType, |  | ||||||
| }: { | }: { | ||||||
|   serialNumber?: string; |   serialNumber?: string; | ||||||
|   limit: number; |   limit: number; | ||||||
|   onError?: (e: AxiosError) => void; |   onError?: (e: AxiosError) => void; | ||||||
|   logType?: 0 | 1; |  | ||||||
| }) => | }) => | ||||||
|   useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), { |   useQuery(['devicelogs', serialNumber, { limit }], getDeviceLogs(limit, serialNumber), { | ||||||
|     keepPreviousData: true, |     keepPreviousData: true, | ||||||
|     enabled: serialNumber !== undefined && serialNumber !== '', |     enabled: serialNumber !== undefined && serialNumber !== '', | ||||||
|     staleTime: 30000, |     staleTime: 30000, | ||||||
|     onError, |     onError, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| const deleteLogs = async ({ | const deleteLogs = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) => | ||||||
|   serialNumber, |   axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}`); | ||||||
|   endDate, |  | ||||||
|   logType, |  | ||||||
| }: { |  | ||||||
|   serialNumber: string; |  | ||||||
|   endDate: number; |  | ||||||
|   logType: 0 | 1; |  | ||||||
| }) => axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}&logType=${logType}`); |  | ||||||
| export const useDeleteLogs = () => { | export const useDeleteLogs = () => { | ||||||
|   const queryClient = useQueryClient(); |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
| @@ -56,25 +45,15 @@ export const useDeleteLogs = () => { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getLogsBatch = ( | const getLogsBatch = (serialNumber?: string, start?: number, end?: number, limit?: number, offset?: number) => | ||||||
|   serialNumber?: string, |  | ||||||
|   start?: number, |  | ||||||
|   end?: number, |  | ||||||
|   limit?: number, |  | ||||||
|   offset?: number, |  | ||||||
|   logType?: 0 | 1, |  | ||||||
| ) => |  | ||||||
|   axiosGw |   axiosGw | ||||||
|     .get( |     .get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}`) | ||||||
|       `device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}&logType=${logType}`, |  | ||||||
|     ) |  | ||||||
|     .then((response) => response.data) as Promise<{ |     .then((response) => response.data) as Promise<{ | ||||||
|     values: DeviceLog[]; |     values: DeviceLog[]; | ||||||
|     serialNumber: string; |     serialNumber: string; | ||||||
|   }>; |   }>; | ||||||
|  |  | ||||||
| const getDeviceLogsWithTimestamps = | const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () => { | ||||||
|   (serialNumber?: string, start?: number, end?: number, logType?: 0 | 1) => async () => { |  | ||||||
|   let offset = 0; |   let offset = 0; | ||||||
|   const limit = 100; |   const limit = 100; | ||||||
|   let logs: DeviceLog[] = []; |   let logs: DeviceLog[] = []; | ||||||
| @@ -84,34 +63,28 @@ const getDeviceLogsWithTimestamps = | |||||||
|   }; |   }; | ||||||
|   do { |   do { | ||||||
|     // eslint-disable-next-line no-await-in-loop |     // eslint-disable-next-line no-await-in-loop | ||||||
|       latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset, logType); |     latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset); | ||||||
|     logs = logs.concat(latestResponse.values); |     logs = logs.concat(latestResponse.values); | ||||||
|     offset += limit; |     offset += limit; | ||||||
|   } while (latestResponse.values.length === limit); |   } while (latestResponse.values.length === limit); | ||||||
|   return { |   return { | ||||||
|     values: logs, |     values: logs, | ||||||
|   }; |   }; | ||||||
|   }; | }; | ||||||
|  |  | ||||||
| export const useGetDeviceLogsWithTimestamps = ({ | export const useGetDeviceLogsWithTimestamps = ({ | ||||||
|   serialNumber, |   serialNumber, | ||||||
|   start, |   start, | ||||||
|   end, |   end, | ||||||
|   onError, |   onError, | ||||||
|   logType, |  | ||||||
| }: { | }: { | ||||||
|   serialNumber?: string; |   serialNumber?: string; | ||||||
|   start?: number; |   start?: number; | ||||||
|   end?: number; |   end?: number; | ||||||
|   onError?: (e: AxiosError) => void; |   onError?: (e: AxiosError) => void; | ||||||
|   logType?: 0 | 1; |  | ||||||
| }) => | }) => | ||||||
|   useQuery( |   useQuery(['devicelogs', serialNumber, { start, end }], getDeviceLogsWithTimestamps(serialNumber, start, end), { | ||||||
|     ['devicelogs', serialNumber, { start, end, logType }], |  | ||||||
|     getDeviceLogsWithTimestamps(serialNumber, start, end, logType ?? 0), |  | ||||||
|     { |  | ||||||
|     enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined, |     enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined, | ||||||
|     staleTime: 1000 * 60, |     staleTime: 1000 * 60, | ||||||
|     onError, |     onError, | ||||||
|     }, |   }); | ||||||
|   ); |  | ||||||
|   | |||||||
| @@ -165,10 +165,7 @@ export type DevicesStats = { | |||||||
|   averageConnectionTime: number; |   averageConnectionTime: number; | ||||||
|   connectedDevices: number; |   connectedDevices: number; | ||||||
|   connectingDevices: number; |   connectingDevices: number; | ||||||
|   tx: number; |  | ||||||
|   rx: number; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getInitialStats = async () => | const getInitialStats = async () => | ||||||
|   axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data); |   axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data); | ||||||
| export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => { | export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => { | ||||||
|   | |||||||
| @@ -7,32 +7,17 @@ import { AxiosError } from 'models/Axios'; | |||||||
| import { Firmware } from 'models/Firmware'; | import { Firmware } from 'models/Firmware'; | ||||||
| import { Note } from 'models/Note'; | 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 }) => { | export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |  | ||||||
|   return useQuery(['get-device-profile'], () => getAllAvailableFirmware(deviceType), { |   return useQuery( | ||||||
|  |     ['get-device-profile'], | ||||||
|  |     () => | ||||||
|  |       axiosFms | ||||||
|  |         .get(`firmwares?deviceType=${deviceType}&limit=10000&offset=0`) | ||||||
|  |         .then(({ data }: { data: { firmwares: Firmware[] } }) => data), | ||||||
|  |     { | ||||||
|       enabled: deviceType !== '', |       enabled: deviceType !== '', | ||||||
|       onError: (e: AxiosError) => { |       onError: (e: AxiosError) => { | ||||||
|         if (!toast.isActive('firmware-fetching-error')) |         if (!toast.isActive('firmware-fetching-error')) | ||||||
| @@ -49,7 +34,8 @@ export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) | |||||||
|             position: 'top-right', |             position: 'top-right', | ||||||
|           }); |           }); | ||||||
|       }, |       }, | ||||||
|   }); |     }, | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) => | export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) => | ||||||
| @@ -70,13 +56,7 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe | |||||||
|  |  | ||||||
|   return useMutation( |   return useMutation( | ||||||
|     ({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) => |     ({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) => | ||||||
|       axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, { |       axiosGw.post(`device/${serialNumber}/upgrade`, { serialNumber, when: 0, keepRedirector, uri, signature }), | ||||||
|         serialNumber, |  | ||||||
|         when: 0, |  | ||||||
|         keepRedirector, |  | ||||||
|         uri, |  | ||||||
|         signature, |  | ||||||
|       }), |  | ||||||
|     { |     { | ||||||
|       onSuccess: () => { |       onSuccess: () => { | ||||||
|         toast({ |         toast({ | ||||||
| @@ -242,23 +222,3 @@ export const useGetFirmwareDashboard = () => | |||||||
|     keepPreviousData: true, |     keepPreviousData: true, | ||||||
|     refetchInterval: 30000, |     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,83 +0,0 @@ | |||||||
| import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; |  | ||||||
| import { axiosSec } from 'constants/axiosInstances'; |  | ||||||
|  |  | ||||||
| export type SecretName = 'google.maps.apikey' | string; |  | ||||||
|  |  | ||||||
| export type Secret = { |  | ||||||
|   key: SecretName; |  | ||||||
|   value: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type SecretDictionaryValue = { |  | ||||||
|   key: SecretName; |  | ||||||
|   description: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const getSecret = async (context: QueryFunctionContext<string[], unknown>) => |  | ||||||
|   axiosSec.get(`/systemSecret/${context.queryKey[1]}`).then(({ data }: { data: Secret }) => data); |  | ||||||
|  |  | ||||||
| export const useGetSystemSecret = ({ secret }: { secret: SecretName }) => |  | ||||||
|   useQuery(['secrets', secret], getSecret, { |  | ||||||
|     staleTime: 1000 * 60 * 10, |  | ||||||
|     refetchInterval: 1000 * 60 * 10, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| const getAllSecrets = async () => |  | ||||||
|   axiosSec.get('/systemSecret/0?all=true').then(({ data }: { data: { secrets: Secret[] } }) => data.secrets); |  | ||||||
|  |  | ||||||
| export const useGetAllSystemSecrets = () => { |  | ||||||
|   const queryClient = useQueryClient(); |  | ||||||
|  |  | ||||||
|   return useQuery(['secrets'], getAllSecrets, { |  | ||||||
|     staleTime: 1000 * 60 * 10, |  | ||||||
|     refetchInterval: 1000 * 60 * 10, |  | ||||||
|     onSuccess: (data) => { |  | ||||||
|       for (const secret of data) { |  | ||||||
|         queryClient.setQueryData(['secrets', secret.key], secret); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const getSecretsDictionary = async () => |  | ||||||
|   axiosSec |  | ||||||
|     .get('/systemSecret/0?dictionary=true') |  | ||||||
|     .then(({ data }: { data: { knownKeys: SecretDictionaryValue[] } }) => data.knownKeys); |  | ||||||
|  |  | ||||||
| export const useGetSystemSecretsDictionary = () => |  | ||||||
|   useQuery(['secrets', 'dictionary'], getSecretsDictionary, { |  | ||||||
|     staleTime: 1000 * 60 * 10, |  | ||||||
|     refetchInterval: 1000 * 60 * 10, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| const updateSecret = async ({ key, value }: { key: string; value: string }) => |  | ||||||
|   axiosSec.put(`/systemSecret/${key}?value=${value}`, { key, value }); |  | ||||||
|  |  | ||||||
| export const useUpdateSystemSecret = () => { |  | ||||||
|   const queryClient = useQueryClient(); |  | ||||||
|   return useMutation(updateSecret, { |  | ||||||
|     onSuccess: () => { |  | ||||||
|       queryClient.invalidateQueries(['secrets']); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const useCreateSystemSecret = () => { |  | ||||||
|   const queryClient = useQueryClient(); |  | ||||||
|   return useMutation(updateSecret, { |  | ||||||
|     onSuccess: () => { |  | ||||||
|       queryClient.invalidateQueries(['secrets']); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const deleteSecret = async (key: string) => axiosSec.delete(`/systemSecret/${key}`); |  | ||||||
|  |  | ||||||
| export const useDeleteSystemSecret = () => { |  | ||||||
|   const queryClient = useQueryClient(); |  | ||||||
|   return useMutation(deleteSecret, { |  | ||||||
|     onSuccess: () => { |  | ||||||
|       queryClient.invalidateQueries(['secrets']); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { useQuery } from '@tanstack/react-query'; | import { useQuery, useQueryClient } from '@tanstack/react-query'; | ||||||
| import { axiosGw } from 'constants/axiosInstances'; | import { axiosGw } from 'constants/axiosInstances'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
| @@ -21,18 +21,6 @@ type DeviceInterfaceStatistics = { | |||||||
|     tx_errors: number; |     tx_errors: number; | ||||||
|     tx_packets: 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?: { |   ssids?: { | ||||||
|     associations?: { |     associations?: { | ||||||
|       ack_signal: number; |       ack_signal: number; | ||||||
| @@ -160,11 +148,6 @@ export type DeviceStatistics = { | |||||||
|       }; |       }; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   gps?: { |  | ||||||
|     elevation: string; |  | ||||||
|     latitude: string; |  | ||||||
|     longitude: string; |  | ||||||
|   }; |  | ||||||
|   version?: number; |   version?: number; | ||||||
| }; | }; | ||||||
| const getLastStats = (serialNumber?: string) => | const getLastStats = (serialNumber?: string) => | ||||||
| @@ -180,7 +163,7 @@ export const useGetDeviceLastStats = ({ | |||||||
|   onError?: (e: AxiosError) => void; |   onError?: (e: AxiosError) => void; | ||||||
| }) => | }) => | ||||||
|   useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), { |   useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), { | ||||||
|     enabled: serialNumber !== undefined && serialNumber !== '', |     enabled: serialNumber !== undefined && serialNumber !== '' && false, | ||||||
|     staleTime: 1000 * 60, |     staleTime: 1000 * 60, | ||||||
|     onError, |     onError, | ||||||
|   }); |   }); | ||||||
| @@ -200,12 +183,24 @@ export const useGetDeviceNewestStats = ({ | |||||||
|   serialNumber?: string; |   serialNumber?: string; | ||||||
|   limit: number; |   limit: number; | ||||||
|   onError?: (e: AxiosError) => void; |   onError?: (e: AxiosError) => void; | ||||||
| }) => | }) => { | ||||||
|   useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), { |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), { | ||||||
|     enabled: serialNumber !== undefined && serialNumber !== '', |     enabled: serialNumber !== undefined && serialNumber !== '', | ||||||
|     staleTime: 1000 * 60, |     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, |     onError, | ||||||
|   }); |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
| const getOuis = (macs?: string[]) => async () => | const getOuis = (macs?: string[]) => async () => | ||||||
|   axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{ |   axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{ | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { useToast } from '@chakra-ui/react'; | import { useToast } from '@chakra-ui/react'; | ||||||
| import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { axiosSec } from 'constants/axiosInstances'; | import { axiosSec } from 'constants/axiosInstances'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
| @@ -58,23 +58,11 @@ export type User = { | |||||||
|   waitingForEmailCheck: boolean; |   waitingForEmailCheck: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getAvatarPromises = (userList: User[], queryClient: QueryClient) => { | const getAvatarPromises = (userList: User[]) => { | ||||||
|   const promises = userList.map(async (user) => { |   const promises = userList.map(async (user) => { | ||||||
|     if (user.avatar !== '' && user.avatar !== '0') { |     if (user.avatar !== '' && user.avatar !== '0') { | ||||||
|       // If the avatar is already in the cache, return it |       return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, { | ||||||
|       const cachedAvatar = queryClient.getQueryData(['avatar', user.id, user.avatar]); |  | ||||||
|       if (cachedAvatar) return cachedAvatar; |  | ||||||
|  |  | ||||||
|       return axiosSec |  | ||||||
|         .get(`avatar/${user.id}?cache=${user.avatar}`, { |  | ||||||
|         responseType: 'arraybuffer', |         responseType: 'arraybuffer', | ||||||
|         }) |  | ||||||
|         .then((response) => { |  | ||||||
|           queryClient.setQueryData(['avatar', user.id, user.avatar], response); |  | ||||||
|           return response; |  | ||||||
|         }) |  | ||||||
|         .catch((e) => { |  | ||||||
|           throw e; |  | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     return Promise.resolve(''); |     return Promise.resolve(''); | ||||||
| @@ -83,35 +71,10 @@ const getAvatarPromises = (userList: User[], queryClient: QueryClient) => { | |||||||
|   return promises; |   return promises; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getBatchUsers = async (offset: number, limit: number) => { | const getUsers = async () => { | ||||||
|   const users = await axiosSec |   const users = await axiosSec.get('users').then(({ data }) => data.users as User[]); | ||||||
|     .get(`users?offset=${offset}&limit=${limit}&withExtendedInfo=true`) |  | ||||||
|     .then(({ data }) => data.users as User[]); |  | ||||||
|  |  | ||||||
|   return users; |   const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) => | ||||||
| }; |  | ||||||
|  |  | ||||||
| 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) => { |     results.map((response) => { | ||||||
|       if (response.status === 'fulfilled' && response?.value !== '') { |       if (response.status === 'fulfilled' && response?.value !== '') { | ||||||
|         const base64 = btoa( |         const base64 = btoa( | ||||||
| @@ -130,10 +93,8 @@ const getUsers = async (queryClient: QueryClient) => { | |||||||
| export const useGetUsers = () => { | export const useGetUsers = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const queryClient = useQueryClient(); |  | ||||||
|  |  | ||||||
|   return useQuery(['users'], () => getUsers(queryClient), { |   return useQuery(['users'], getUsers, { | ||||||
|     staleTime: 30 * 1000, |  | ||||||
|     onError: (e: AxiosError) => { |     onError: (e: AxiosError) => { | ||||||
|       if (!toast.isActive('users-fetching-error')) |       if (!toast.isActive('users-fetching-error')) | ||||||
|         toast({ |         toast({ | ||||||
| @@ -157,7 +118,7 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) => | |||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |  | ||||||
|   return useQuery( |   return useQuery( | ||||||
|     ['users', id], |     ['get-user', id], | ||||||
|     () => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User), |     () => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User), | ||||||
|     { |     { | ||||||
|       enabled, |       enabled, | ||||||
| @@ -212,41 +173,16 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| export const useSuspendUser = ({ id }: { id: string }) => { | export const useSuspendUser = ({ id }: { id: string }) => | ||||||
|   const queryClient = useQueryClient(); |   useMutation((isSuspended: boolean) => | ||||||
|  |  | ||||||
|   return useMutation( |  | ||||||
|     (isSuspended: boolean) => |  | ||||||
|     axiosSec.put(`user/${id}`, { |     axiosSec.put(`user/${id}`, { | ||||||
|       suspended: isSuspended, |       suspended: isSuspended, | ||||||
|     }), |     }), | ||||||
|     { |  | ||||||
|       onSuccess: () => { |  | ||||||
|         queryClient.invalidateQueries(['users']); |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   ); |   ); | ||||||
| }; | export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {})); | ||||||
|  |  | ||||||
| export const useResetMfa = ({ id }: { id: string }) => { | export const useResetPassword = ({ id }: { id: string }) => | ||||||
|   const queryClient = useQueryClient(); |   useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {})); | ||||||
|  |  | ||||||
|   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}`); | const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`); | ||||||
| export const useDeleteUser = () => { | export const useDeleteUser = () => { | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import * as React from 'react'; | |||||||
| import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react'; | import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting'; | import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting'; | ||||||
| import { bytesString } from 'helpers/stringHelper'; |  | ||||||
| import { useGetDevicesStats } from 'hooks/Network/Devices'; | import { useGetDevicesStats } from 'hooks/Network/Devices'; | ||||||
|  |  | ||||||
| const SidebarDevices = () => { | const SidebarDevices = () => { | ||||||
| @@ -49,7 +48,7 @@ const SidebarDevices = () => { | |||||||
|   if (!getStats.data) return null; |   if (!getStats.data) return null; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <VStack mb={-1}> |     <VStack spacing={4}> | ||||||
|       <Flex flexDir="column" textAlign="center"> |       <Flex flexDir="column" textAlign="center"> | ||||||
|         <Heading size="md">{getStats.data.connectedDevices}</Heading> |         <Heading size="md">{getStats.data.connectedDevices}</Heading> | ||||||
|         <Heading size="xs"> |         <Heading size="xs"> | ||||||
| @@ -58,16 +57,6 @@ const SidebarDevices = () => { | |||||||
|         <Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400"> |         <Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400"> | ||||||
|           ({getStats.data.connectingDevices} {t('controller.devices.connecting')}) |           ({getStats.data.connectingDevices} {t('controller.devices.connecting')}) | ||||||
|         </Heading> |         </Heading> | ||||||
|         <Heading |  | ||||||
|           size="xs" |  | ||||||
|           mt={1} |  | ||||||
|           fontStyle="italic" |  | ||||||
|           fontWeight="normal" |  | ||||||
|           color="gray.400" |  | ||||||
|           hidden={getStats.data.rx === undefined || getStats.data.tx === undefined} |  | ||||||
|         > |  | ||||||
|           Rx: {bytesString(getStats.data.rx)}, Tx: {bytesString(getStats.data.tx)} |  | ||||||
|         </Heading> |  | ||||||
|         <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> |         <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> | ||||||
|           <Heading size="md" textAlign="center" mt={2}> |           <Heading size="md" textAlign="center" mt={2}> | ||||||
|             {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} |             {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} | ||||||
|   | |||||||
| @@ -1,75 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { Box, Button, Flex, FormControl, FormLabel, useDisclosure } from '@chakra-ui/react'; |  | ||||||
| import { Wrapper } from '@googlemaps/react-wrapper'; |  | ||||||
| import { Globe } from 'phosphor-react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { GoogleMap } from 'components/Maps/GoogleMap'; |  | ||||||
| import { GoogleMapMarker } from 'components/Maps/GoogleMap/Marker'; |  | ||||||
| import { Modal } from 'components/Modals/Modal'; |  | ||||||
| import { useGetSystemSecret } from 'hooks/Network/Secrets'; |  | ||||||
| import { useGetDeviceLastStats } from 'hooks/Network/Statistics'; |  | ||||||
|  |  | ||||||
| type Props = { |  | ||||||
|   serialNumber: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const LocationDisplayButton = ({ serialNumber }: Props) => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); |  | ||||||
|   const getGoogleApiKey = useGetSystemSecret({ secret: 'google.maps.apikey' }); |  | ||||||
|   const getLastStats = useGetDeviceLastStats({ serialNumber }); |  | ||||||
|  |  | ||||||
|   const location: google.maps.LatLngLiteral | undefined = React.useMemo(() => { |  | ||||||
|     if (!getLastStats.data?.gps) return undefined; |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       return { |  | ||||||
|         lat: Number.parseFloat(getLastStats.data.gps.latitude), |  | ||||||
|         lng: Number.parseFloat(getLastStats.data.gps.longitude), |  | ||||||
|       }; |  | ||||||
|     } catch (e) { |  | ||||||
|       return undefined; |  | ||||||
|     } |  | ||||||
|   }, [getLastStats.data?.gps]); |  | ||||||
|  |  | ||||||
|   if (!location) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue"> |  | ||||||
|         {t('locations.view_gps')} |  | ||||||
|       </Button> |  | ||||||
|       <Modal isOpen={isOpen} onClose={onClose} title={t('locations.one')}> |  | ||||||
|         <Box w="100%" h="100%"> |  | ||||||
|           <Flex mb={4}> |  | ||||||
|             <FormControl w="unset"> |  | ||||||
|               <FormLabel>{t('locations.lat')}</FormLabel> |  | ||||||
|               <pre>{location.lat}</pre> |  | ||||||
|             </FormControl> |  | ||||||
|             <FormControl w="unset" mx={4}> |  | ||||||
|               <FormLabel>{t('locations.longitude')}</FormLabel> |  | ||||||
|               <pre>{location.lng}</pre> |  | ||||||
|             </FormControl> |  | ||||||
|             <FormControl w="unset"> |  | ||||||
|               <FormLabel>{t('locations.elevation')}</FormLabel> |  | ||||||
|               <pre>{getLastStats.data?.gps?.elevation}</pre> |  | ||||||
|             </FormControl> |  | ||||||
|           </Flex> |  | ||||||
|           {getGoogleApiKey.data ? ( |  | ||||||
|             <Box h="500px"> |  | ||||||
|               <Wrapper apiKey={getGoogleApiKey.data.value}> |  | ||||||
|                 <GoogleMap center={location} style={{ flexGrow: '1', height: '100%' }} zoom={10}> |  | ||||||
|                   <GoogleMapMarker position={location} /> |  | ||||||
|                 </GoogleMap> |  | ||||||
|               </Wrapper> |  | ||||||
|             </Box> |  | ||||||
|           ) : null} |  | ||||||
|         </Box> |  | ||||||
|       </Modal> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default LocationDisplayButton; |  | ||||||
| @@ -1,94 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import HistoryDatePickers from '../DatePickers'; |  | ||||||
| import DeleteLogModal from './DeleteModal'; |  | ||||||
| import useDeviceLogsTable from './useDeviceLogsTable'; |  | ||||||
| import { RefreshButton } from 'components/Buttons/RefreshButton'; |  | ||||||
| import { ColumnPicker } from 'components/DataTables/ColumnPicker'; |  | ||||||
| import { DataTable } from 'components/DataTables/DataTable'; |  | ||||||
| import { Column } from 'models/Table'; |  | ||||||
|  |  | ||||||
| type Props = { |  | ||||||
|   serialNumber: string; |  | ||||||
| }; |  | ||||||
| const CrashLogs = ({ serialNumber }: Props) => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const [limit, setLimit] = React.useState(25); |  | ||||||
|   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); |  | ||||||
|   const { time, setTime, getCustomLogs, getLogs, columns, modal } = useDeviceLogsTable({ |  | ||||||
|     serialNumber, |  | ||||||
|     limit, |  | ||||||
|     logType: 1, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const setNewTime = (start: Date, end: Date) => { |  | ||||||
|     setTime({ start, end }); |  | ||||||
|   }; |  | ||||||
|   const onClear = () => { |  | ||||||
|     setTime(undefined); |  | ||||||
|   }; |  | ||||||
|   const raiseLimit = () => { |  | ||||||
|     setLimit(limit + 25); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const noMoreAvailable = getLogs.data !== undefined && getLogs.data.values.length < limit; |  | ||||||
|  |  | ||||||
|   const data = React.useMemo(() => { |  | ||||||
|     if (getCustomLogs.data) return getCustomLogs.data.values.sort((a, b) => b.recorded - a.recorded); |  | ||||||
|     if (getLogs.data) return getLogs.data.values; |  | ||||||
|     return []; |  | ||||||
|   }, [getLogs.data, getCustomLogs.data]); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Box> |  | ||||||
|       <Flex> |  | ||||||
|         <Spacer /> |  | ||||||
|         <HStack> |  | ||||||
|           <HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} /> |  | ||||||
|           <ColumnPicker |  | ||||||
|             columns={columns as Column<unknown>[]} |  | ||||||
|             hiddenColumns={hiddenColumns} |  | ||||||
|             setHiddenColumns={setHiddenColumns} |  | ||||||
|             preference="gateway.device.logs.hiddenColumns" |  | ||||||
|           /> |  | ||||||
|           <DeleteLogModal serialNumber={serialNumber} logType={0} /> |  | ||||||
|           <RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" /> |  | ||||||
|         </HStack> |  | ||||||
|       </Flex> |  | ||||||
|       <Box overflowY="auto" h="300px"> |  | ||||||
|         <DataTable |  | ||||||
|           columns={ |  | ||||||
|             columns as { |  | ||||||
|               id: string; |  | ||||||
|               Header: string; |  | ||||||
|               Footer: string; |  | ||||||
|               accessor: string; |  | ||||||
|             }[] |  | ||||||
|           } |  | ||||||
|           data={data} |  | ||||||
|           isLoading={getLogs.isFetching || getCustomLogs.isFetching} |  | ||||||
|           hiddenColumns={hiddenColumns} |  | ||||||
|           obj={t('controller.devices.logs')} |  | ||||||
|           // @ts-ignore |  | ||||||
|           hideControls |  | ||||||
|           showAllRows |  | ||||||
|         /> |  | ||||||
|         {getLogs.data !== undefined && ( |  | ||||||
|           <Center mt={1} hidden={getCustomLogs.data !== undefined}> |  | ||||||
|             {!noMoreAvailable || getLogs.isFetching ? ( |  | ||||||
|               <Button colorScheme="blue" onClick={raiseLimit} isLoading={getLogs.isFetching}> |  | ||||||
|                 {t('controller.devices.show_more')} |  | ||||||
|               </Button> |  | ||||||
|             ) : ( |  | ||||||
|               <Heading size="sm">{t('controller.devices.no_more_available')}!</Heading> |  | ||||||
|             )} |  | ||||||
|           </Center> |  | ||||||
|         )} |  | ||||||
|       </Box> |  | ||||||
|       {modal} |  | ||||||
|     </Box> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default CrashLogs; |  | ||||||
| @@ -16,8 +16,8 @@ const CustomInputButton = React.forwardRef( | |||||||
|   ), |   ), | ||||||
| ); | ); | ||||||
|  |  | ||||||
| type Props = { serialNumber: string; logType: 0 | 1 }; | type Props = { serialNumber: string }; | ||||||
| const DeleteLogModal = ({ serialNumber, logType }: Props) => { | const DeleteLogModal = ({ serialNumber }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const modalProps = useDisclosure(); |   const modalProps = useDisclosure(); | ||||||
| @@ -26,7 +26,7 @@ const DeleteLogModal = ({ serialNumber, logType }: Props) => { | |||||||
|  |  | ||||||
|   const onDeleteClick = () => { |   const onDeleteClick = () => { | ||||||
|     deleteLogs.mutate( |     deleteLogs.mutate( | ||||||
|       { endDate: Math.floor(date.getTime() / 1000), serialNumber, logType }, |       { endDate: Math.floor(date.getTime() / 1000), serialNumber }, | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           modalProps.onClose(); |           modalProps.onClose(); | ||||||
|   | |||||||
| @@ -1,55 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { Box, Button, Code, Heading, useClipboard } from '@chakra-ui/react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import FormattedDate from 'components/InformationDisplays/FormattedDate'; |  | ||||||
| import { Modal } from 'components/Modals/Modal'; |  | ||||||
| import { DeviceLog } from 'hooks/Network/DeviceLogs'; |  | ||||||
|  |  | ||||||
| type Props = { |  | ||||||
|   modalProps: { |  | ||||||
|     isOpen: boolean; |  | ||||||
|     onClose: () => void; |  | ||||||
|   }; |  | ||||||
|   log?: DeviceLog; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const DetailedLogViewModal = ({ modalProps, log }: Props) => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(log?.log ?? {}, null, 2)); |  | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     setValue(JSON.stringify(log?.log ?? {}, null, 2)); |  | ||||||
|   }, [log]); |  | ||||||
|  |  | ||||||
|   if (!log) return null; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Modal |  | ||||||
|       isOpen={modalProps.isOpen} |  | ||||||
|       onClose={modalProps.onClose} |  | ||||||
|       title={t('devices.logs_one')} |  | ||||||
|       topRightButtons={ |  | ||||||
|         <Button onClick={onCopy} size="md" colorScheme="teal"> |  | ||||||
|           {hasCopied ? `${t('common.copied')}!` : t('common.copy')} |  | ||||||
|         </Button> |  | ||||||
|       } |  | ||||||
|     > |  | ||||||
|       <Box> |  | ||||||
|         <Heading size="sm"> |  | ||||||
|           <FormattedDate date={log.recorded} /> |  | ||||||
|         </Heading> |  | ||||||
|         <Heading size="sm"> |  | ||||||
|           {t('controller.devices.severity')}: {log.severity} |  | ||||||
|         </Heading> |  | ||||||
|         <Heading size="sm"> |  | ||||||
|           {t('controller.devices.config_id')}: {log.UUID} |  | ||||||
|         </Heading> |  | ||||||
|         <Code whiteSpace="pre-line" mt={2}> |  | ||||||
|           {log.log} |  | ||||||
|         </Code> |  | ||||||
|       </Box> |  | ||||||
|     </Modal> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default DetailedLogViewModal; |  | ||||||
| @@ -16,7 +16,7 @@ const LogHistory = ({ serialNumber }: Props) => { | |||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const [limit, setLimit] = React.useState(25); |   const [limit, setLimit] = React.useState(25); | ||||||
|   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); |   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); | ||||||
|   const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit, logType: 0 }); |   const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit }); | ||||||
|  |  | ||||||
|   const setNewTime = (start: Date, end: Date) => { |   const setNewTime = (start: Date, end: Date) => { | ||||||
|     setTime({ start, end }); |     setTime({ start, end }); | ||||||
| @@ -48,7 +48,7 @@ const LogHistory = ({ serialNumber }: Props) => { | |||||||
|             setHiddenColumns={setHiddenColumns} |             setHiddenColumns={setHiddenColumns} | ||||||
|             preference="gateway.device.logs.hiddenColumns" |             preference="gateway.device.logs.hiddenColumns" | ||||||
|           /> |           /> | ||||||
|           <DeleteLogModal serialNumber={serialNumber} logType={0} /> |           <DeleteLogModal serialNumber={serialNumber} /> | ||||||
|           <RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" /> |           <RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" /> | ||||||
|         </HStack> |         </HStack> | ||||||
|       </Flex> |       </Flex> | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, IconButton, Text, useDisclosure } from '@chakra-ui/react'; | import { Box } from '@chakra-ui/react'; | ||||||
| import { MagnifyingGlass } from 'phosphor-react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import DetailedLogViewModal from './DetailedLogViewModal'; |  | ||||||
| import FormattedDate from 'components/InformationDisplays/FormattedDate'; | import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||||
| import { DeviceLog, useGetDeviceLogs, useGetDeviceLogsWithTimestamps } from 'hooks/Network/DeviceLogs'; | import { DeviceLog, useGetDeviceLogs, useGetDeviceLogsWithTimestamps } from 'hooks/Network/DeviceLogs'; | ||||||
| import { Column } from 'models/Table'; | import { Column } from 'models/Table'; | ||||||
| @@ -10,49 +8,18 @@ import { Column } from 'models/Table'; | |||||||
| type Props = { | type Props = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
|   limit: number; |   limit: number; | ||||||
|   logType: 0 | 1; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => { | const useDeviceLogsTable = ({ serialNumber, limit }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const getLogs = useGetDeviceLogs({ serialNumber, limit, logType }); |   const getLogs = useGetDeviceLogs({ serialNumber, limit }); | ||||||
|   const modalProps = useDisclosure(); |  | ||||||
|   const [log, setLog] = React.useState<DeviceLog | undefined>(); |  | ||||||
|   const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>(); |   const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>(); | ||||||
|   const getCustomLogs = useGetDeviceLogsWithTimestamps({ |   const getCustomLogs = useGetDeviceLogsWithTimestamps({ | ||||||
|     serialNumber, |     serialNumber, | ||||||
|     start: time ? Math.floor(time.start.getTime() / 1000) : undefined, |     start: time ? Math.floor(time.start.getTime() / 1000) : undefined, | ||||||
|     end: time ? Math.floor(time.end.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( |   const dateCell = React.useCallback( | ||||||
|     (v: number) => ( |     (v: number) => ( | ||||||
|       <Box> |       <Box> | ||||||
| @@ -98,7 +65,6 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => { | |||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'log', |         accessor: 'log', | ||||||
|         customWidth: '35px', |         customWidth: '35px', | ||||||
|         Cell: (v) => logCell(v.cell.row.original), |  | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -119,7 +85,6 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => { | |||||||
|     getCustomLogs, |     getCustomLogs, | ||||||
|     time, |     time, | ||||||
|     setTime, |     setTime, | ||||||
|     modal: <DetailedLogViewModal modalProps={modalProps} log={log} />, |  | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; | |||||||
| import CommandHistory from './CommandHistory'; | import CommandHistory from './CommandHistory'; | ||||||
| import HealthCheckHistory from './HealthCheckHistory'; | import HealthCheckHistory from './HealthCheckHistory'; | ||||||
| import LogHistory from './LogHistory'; | import LogHistory from './LogHistory'; | ||||||
| import CrashLogs from './LogHistory/CrashLogs'; |  | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| import { CardBody } from 'components/Containers/Card/CardBody'; | import { CardBody } from 'components/Containers/Card/CardBody'; | ||||||
|  |  | ||||||
| @@ -33,9 +32,6 @@ const DeviceLogsCard = ({ serialNumber }: Props) => { | |||||||
|             <Tab fontSize="lg" fontWeight="bold"> |             <Tab fontSize="lg" fontWeight="bold"> | ||||||
|               {t('controller.devices.logs')} |               {t('controller.devices.logs')} | ||||||
|             </Tab> |             </Tab> | ||||||
|             <Tab fontSize="lg" fontWeight="bold"> |  | ||||||
|               {t('devices.crash_logs')} |  | ||||||
|             </Tab> |  | ||||||
|           </TabList> |           </TabList> | ||||||
|           <TabPanels> |           <TabPanels> | ||||||
|             <TabPanel p={0}> |             <TabPanel p={0}> | ||||||
| @@ -55,12 +51,10 @@ const DeviceLogsCard = ({ serialNumber }: Props) => { | |||||||
|             <TabPanel> |             <TabPanel> | ||||||
|               <HealthCheckHistory serialNumber={serialNumber} /> |               <HealthCheckHistory serialNumber={serialNumber} /> | ||||||
|             </TabPanel> |             </TabPanel> | ||||||
|  |  | ||||||
|             <TabPanel> |             <TabPanel> | ||||||
|               <LogHistory serialNumber={serialNumber} /> |               <LogHistory serialNumber={serialNumber} /> | ||||||
|             </TabPanel> |             </TabPanel> | ||||||
|             <TabPanel> |  | ||||||
|               <CrashLogs serialNumber={serialNumber} /> |  | ||||||
|             </TabPanel> |  | ||||||
|           </TabPanels> |           </TabPanels> | ||||||
|         </Tabs> |         </Tabs> | ||||||
|       </CardBody> |       </CardBody> | ||||||
|   | |||||||
| @@ -1,16 +1,5 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { Box, Flex, Heading, ListItem, Text, UnorderedList } from '@chakra-ui/react'; | ||||||
|   Box, |  | ||||||
|   Flex, |  | ||||||
|   Heading, |  | ||||||
|   ListItem, |  | ||||||
|   Tag, |  | ||||||
|   TagLabel, |  | ||||||
|   TagLeftIcon, |  | ||||||
|   Text, |  | ||||||
|   Tooltip, |  | ||||||
|   UnorderedList, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import { LockSimple, LockSimpleOpen } from 'phosphor-react'; | import { LockSimple, LockSimpleOpen } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| @@ -31,7 +20,7 @@ const RestrictionsCard = ({ serialNumber }: Props) => { | |||||||
|     ssh: 'SSH', |     ssh: 'SSH', | ||||||
|     rtty: 'RTTY', |     rtty: 'RTTY', | ||||||
|     tty: t('restrictions.tty'), |     tty: t('restrictions.tty'), | ||||||
|     // developer: t('restrictions.developer'), |     developer: t('restrictions.developer'), | ||||||
|     upgrade: t('restrictions.signed_upgrade'), |     upgrade: t('restrictions.signed_upgrade'), | ||||||
|     commands: t('restrictions.gw_commands'), |     commands: t('restrictions.gw_commands'), | ||||||
|   } as { [key: string]: string }; |   } as { [key: string]: string }; | ||||||
| @@ -49,52 +38,27 @@ const RestrictionsCard = ({ serialNumber }: Props) => { | |||||||
|     return restrictedKeys.map(([k]) => <ListItem key={k}>{LABELS[k]}</ListItem>); |     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 ( |   return ( | ||||||
|     <Card mb={4}> |     <Card mb={4}> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
|         <Heading size="md" my="auto" mr={2}> |         <Heading size="md">{t('restrictions.title')}</Heading> | ||||||
|           {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> |       </CardHeader> | ||||||
|       <CardBody p={0} display="block"> |       <CardBody p={0} display="block"> | ||||||
|         <Flex mt={2}> |         <Flex mt={2}> | ||||||
|           <Heading size="sm" mr={2} my="auto"> |           <Heading size="sm" mr={2}> | ||||||
|             {t('restrictions.countries')}: |             {t('restrictions.countries')}: | ||||||
|           </Heading> |           </Heading> | ||||||
|           <Text my="auto"> |           <Text>{restrictions.country.join(', ')}</Text> | ||||||
|             {restrictions.country?.length === 0 ? t('common.all') : restrictions.country.join(', ')} |  | ||||||
|           </Text> |  | ||||||
|         </Flex> |         </Flex> | ||||||
|         <Flex mt={2}> |         <Heading size="sm" mt={2}> | ||||||
|           <Heading size="sm" mt={2} my="auto"> |           {t('restrictions.key_verification')} | ||||||
|             {t('restrictions.key_verification')} {isMissingSigningInfo ? ':' : ''} |  | ||||||
|         </Heading> |         </Heading> | ||||||
|           {isMissingSigningInfo ? ( |         <UnorderedList> | ||||||
|             <Text my="auto" ml={2}> |  | ||||||
|               {t('common.none')} |  | ||||||
|             </Text> |  | ||||||
|           ) : null} |  | ||||||
|         </Flex> |  | ||||||
|         <UnorderedList hidden={isMissingSigningInfo}> |  | ||||||
|           <ListItem> |           <ListItem> | ||||||
|             {t('controller.wifi.vendor')}:{' '} |             {t('controller.wifi.vendor')}: {restrictions.key_info?.vendor} | ||||||
|             {restrictions.key_info?.vendor?.length > 0 ? restrictions.key_info?.vendor : '-'} |  | ||||||
|           </ListItem> |           </ListItem> | ||||||
|           <ListItem> |           <ListItem> | ||||||
|             {t('restrictions.algo')}: {restrictions.key_info?.algo?.length > 0 ? restrictions.key_info?.algo : '-'} |             {t('restrictions.algo')}: {restrictions.key_info?.algo} | ||||||
|           </ListItem> |           </ListItem> | ||||||
|         </UnorderedList> |         </UnorderedList> | ||||||
|         <Flex mt={2}> |         <Flex mt={2}> | ||||||
|   | |||||||
| @@ -48,14 +48,14 @@ const InterfaceChart = ({ data }: Props) => { | |||||||
|         { |         { | ||||||
|           // Real 'Tx', but shown as 'Rx' |           // Real 'Tx', but shown as 'Rx' | ||||||
|           label: 'Tx', |           label: 'Tx', | ||||||
|         data: data.rx.map((tx) => (Math.floor((tx / factor) * 100) / 100).toFixed(2)), |           data: data.rx.map((tx) => Math.floor((tx / factor) * 100) / 100), | ||||||
|           borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 |           borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 | ||||||
|           backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 |           backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           // Real 'Rx', but shown as 'Tx' |           // Real 'Rx', but shown as 'Tx' | ||||||
|           label: 'Rx', |           label: 'Rx', | ||||||
|         data: data.tx.map((rx) => (Math.floor((rx / factor) * 100) / 100).toFixed(2)), |           data: data.tx.map((rx) => Math.floor((rx / factor) * 100) / 100), | ||||||
|           borderColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200 |           borderColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200 | ||||||
|           backgroundColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200 |           backgroundColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200 | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ const ViewLastStatsModal = ({ serialNumber }: Props) => { | |||||||
|     if (getLastStats.data) { |     if (getLastStats.data) { | ||||||
|       setValue(JSON.stringify(getLastStats.data, null, 2)); |       setValue(JSON.stringify(getLastStats.data, null, 2)); | ||||||
|     } |     } | ||||||
|   }, [getLastStats.data, isOpen]); |   }, [getLastStats.data]); | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Tooltip label={t('statistics.last_stats')}> |       <Tooltip label={t('statistics.last_stats')}> | ||||||
|   | |||||||
| @@ -77,10 +77,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|           let rx = inter.counters?.rx_bytes ?? 0; |           let rx = inter.counters?.rx_bytes ?? 0; | ||||||
|           let tx = inter.counters?.tx_bytes ?? 0; |           let tx = inter.counters?.tx_bytes ?? 0; | ||||||
|  |  | ||||||
|           if (inter['counters-aggregate']) { |           if (isInterUpstream) { | ||||||
|             rx = inter['counters-aggregate'].rx_bytes; |  | ||||||
|             tx = inter['counters-aggregate'].tx_bytes; |  | ||||||
|           } else if (isInterUpstream) { |  | ||||||
|             for (const ssid of inter.ssids ?? []) { |             for (const ssid of inter.ssids ?? []) { | ||||||
|               rx += ssid.counters?.rx_bytes ?? 0; |               rx += ssid.counters?.rx_bytes ?? 0; | ||||||
|               tx += ssid.counters?.tx_bytes ?? 0; |               tx += ssid.counters?.tx_bytes ?? 0; | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react'; | import { Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react'; | ||||||
| import ReactCountryFlag from 'react-country-flag'; | import ReactCountryFlag from 'react-country-flag'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import LocationDisplayButton from './LocationDisplayButton'; |  | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| import { CardBody } from 'components/Containers/Card/CardBody'; | import { CardBody } from 'components/Containers/Card/CardBody'; | ||||||
| import FormattedDate from 'components/InformationDisplays/FormattedDate'; | import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||||
| @@ -91,12 +90,11 @@ const DeviceSummary = ({ serialNumber }: Props) => { | |||||||
|               {!getDevice.data?.locale || getDevice.data?.locale === '' ? ( |               {!getDevice.data?.locale || getDevice.data?.locale === '' ? ( | ||||||
|                 '-' |                 '-' | ||||||
|               ) : ( |               ) : ( | ||||||
|                 <Box mr={2}> |                 <> | ||||||
|                   <ReactCountryFlag style={ICON_STYLE} countryCode={getDevice.data.locale} svg /> |                   <ReactCountryFlag style={ICON_STYLE} countryCode={getDevice.data.locale} svg /> | ||||||
|                   {COUNTRY_LIST.find(({ value }) => value === getDevice.data.locale)?.label} |                   {COUNTRY_LIST.find(({ value }) => value === getDevice.data.locale)?.label} | ||||||
|                 </Box> |                 </> | ||||||
|               )} |               )} | ||||||
|               <LocationDisplayButton serialNumber={serialNumber} /> |  | ||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1} alignContent="center" alignItems="center"> |             <GridItem colSpan={1} alignContent="center" alignItems="center"> | ||||||
|               <Heading size="sm">{t('analytics.last_contact')}:</Heading> |               <Heading size="sm">{t('analytics.last_contact')}:</Heading> | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ import { | |||||||
|   useColorModeValue, |   useColorModeValue, | ||||||
|   useDisclosure, |   useDisclosure, | ||||||
| } from '@chakra-ui/react'; | } from '@chakra-ui/react'; | ||||||
| import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react'; | import { Heart, HeartBreak, LockSimple, WifiHigh, WifiSlash } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import Masonry from 'react-masonry-css'; | import Masonry from 'react-masonry-css'; | ||||||
| import DeviceDetails from './Details'; | import DeviceDetails from './Details'; | ||||||
| @@ -33,7 +33,6 @@ import { ConfigureModal } from 'components/Modals/ConfigureModal'; | |||||||
| import { EventQueueModal } from 'components/Modals/EventQueueModal'; | import { EventQueueModal } from 'components/Modals/EventQueueModal'; | ||||||
| import FactoryResetModal from 'components/Modals/FactoryResetModal'; | import FactoryResetModal from 'components/Modals/FactoryResetModal'; | ||||||
| import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal'; | import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal'; | ||||||
| import { RebootModal } from 'components/Modals/RebootModal'; |  | ||||||
| import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; | import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; | ||||||
| import { TelemetryModal } from 'components/Modals/TelemetryModal'; | import { TelemetryModal } from 'components/Modals/TelemetryModal'; | ||||||
| import { TraceModal } from 'components/Modals/TraceModal'; | import { TraceModal } from 'components/Modals/TraceModal'; | ||||||
| @@ -57,11 +56,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|   const upgradeModalProps = useDisclosure(); |   const upgradeModalProps = useDisclosure(); | ||||||
|   const telemetryModalProps = useDisclosure(); |   const telemetryModalProps = useDisclosure(); | ||||||
|   const traceModalProps = useDisclosure(); |   const traceModalProps = useDisclosure(); | ||||||
|   const rebootModalProps = useDisclosure(); |  | ||||||
|   const scriptModal = useScriptModal(); |   const scriptModal = useScriptModal(); | ||||||
|   // Sticky-top styles |  | ||||||
|   const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; |  | ||||||
|   const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); |  | ||||||
|   const connectedTag = React.useMemo(() => { |   const connectedTag = React.useMemo(() => { | ||||||
|     if (!getStatus.data) return null; |     if (!getStatus.data) return null; | ||||||
|  |  | ||||||
| @@ -105,28 +100,9 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|     ); |     ); | ||||||
|   }, [getStatus.data, getHealth.data]); |   }, [getStatus.data, getHealth.data]); | ||||||
|  |  | ||||||
|   const restrictedTag = React.useMemo(() => { |   // Sticky-top styles | ||||||
|     if (!getDevice.data || !getDevice.data.restrictedDevice) return null; |   const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; | ||||||
|  |   const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); | ||||||
|     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 = () => { |   const refresh = () => { | ||||||
|     getDevice.refetch(); |     getDevice.refetch(); | ||||||
| @@ -143,7 +119,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|               <Heading size="md">{serialNumber}</Heading> |               <Heading size="md">{serialNumber}</Heading> | ||||||
|               {connectedTag} |               {connectedTag} | ||||||
|               {healthTag} |               {healthTag} | ||||||
|               {restrictedTag} |               {getDevice.data?.restrictedDevice && ( | ||||||
|  |                 <Tag size="lg" colorScheme="gray"> | ||||||
|  |                   <TagLeftIcon boxSize="18px" as={LockSimple} /> | ||||||
|  |                   <TagLabel>{t('devices.restricted')}</TagLabel> | ||||||
|  |                 </Tag> | ||||||
|  |               )} | ||||||
|             </HStack> |             </HStack> | ||||||
|             <Spacer /> |             <Spacer /> | ||||||
|             <HStack spacing={2}> |             <HStack spacing={2}> | ||||||
| @@ -161,7 +142,6 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|                   onOpenConfigureModal={configureModalProps.onOpen} |                   onOpenConfigureModal={configureModalProps.onOpen} | ||||||
|                   onOpenTelemetryModal={telemetryModalProps.onOpen} |                   onOpenTelemetryModal={telemetryModalProps.onOpen} | ||||||
|                   onOpenScriptModal={scriptModal.openModal} |                   onOpenScriptModal={scriptModal.openModal} | ||||||
|                   onOpenRebootModal={rebootModalProps.onOpen} |  | ||||||
|                   size="md" |                   size="md" | ||||||
|                   isCompact |                   isCompact | ||||||
|                 /> |                 /> | ||||||
| @@ -192,7 +172,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|                 <Heading size="md">{serialNumber}</Heading> |                 <Heading size="md">{serialNumber}</Heading> | ||||||
|                 {connectedTag} |                 {connectedTag} | ||||||
|                 {healthTag} |                 {healthTag} | ||||||
|                 {restrictedTag} |                 {getDevice.data?.restrictedDevice && ( | ||||||
|  |                   <Tag size="lg" colorScheme="gray"> | ||||||
|  |                     <TagLeftIcon boxSize="18px" as={LockSimple} /> | ||||||
|  |                     <TagLabel>{t('devices.restricted')}</TagLabel> | ||||||
|  |                   </Tag> | ||||||
|  |                 )} | ||||||
|               </HStack> |               </HStack> | ||||||
|               <Spacer /> |               <Spacer /> | ||||||
|               <HStack spacing={2}> |               <HStack spacing={2}> | ||||||
| @@ -209,7 +194,6 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|                     onOpenEventQueue={eventQueueProps.onOpen} |                     onOpenEventQueue={eventQueueProps.onOpen} | ||||||
|                     onOpenConfigureModal={configureModalProps.onOpen} |                     onOpenConfigureModal={configureModalProps.onOpen} | ||||||
|                     onOpenTelemetryModal={telemetryModalProps.onOpen} |                     onOpenTelemetryModal={telemetryModalProps.onOpen} | ||||||
|                     onOpenRebootModal={rebootModalProps.onOpen} |  | ||||||
|                     onOpenScriptModal={scriptModal.openModal} |                     onOpenScriptModal={scriptModal.openModal} | ||||||
|                     size="md" |                     size="md" | ||||||
|                   /> |                   /> | ||||||
| @@ -233,7 +217,6 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|       <EventQueueModal serialNumber={serialNumber} modalProps={eventQueueProps} /> |       <EventQueueModal serialNumber={serialNumber} modalProps={eventQueueProps} /> | ||||||
|       <ConfigureModal serialNumber={serialNumber} modalProps={configureModalProps} /> |       <ConfigureModal serialNumber={serialNumber} modalProps={configureModalProps} /> | ||||||
|       <TelemetryModal serialNumber={serialNumber} modalProps={telemetryModalProps} /> |       <TelemetryModal serialNumber={serialNumber} modalProps={telemetryModalProps} /> | ||||||
|       <RebootModal serialNumber={serialNumber} modalProps={rebootModalProps} /> |  | ||||||
|       {scriptModal.modal} |       {scriptModal.modal} | ||||||
|       <Box mt={isCompact ? '0px' : '68px'}> |       <Box mt={isCompact ? '0px' : '68px'}> | ||||||
|         <Masonry |         <Masonry | ||||||
|   | |||||||
| @@ -1,16 +1,27 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { FormControl, FormErrorMessage, FormLabel, Input, Textarea, useDisclosure, useToast } from '@chakra-ui/react'; | import { | ||||||
|  |   Alert, | ||||||
|  |   AlertDescription, | ||||||
|  |   AlertIcon, | ||||||
|  |   AlertTitle, | ||||||
|  |   Box, | ||||||
|  |   FormControl, | ||||||
|  |   FormErrorMessage, | ||||||
|  |   FormLabel, | ||||||
|  |   Input, | ||||||
|  |   Textarea, | ||||||
|  |   useDisclosure, | ||||||
|  |   useToast, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { CreateButton } from 'components/Buttons/CreateButton'; | import { CreateButton } from 'components/Buttons/CreateButton'; | ||||||
| import { SaveButton } from 'components/Buttons/SaveButton'; | import { SaveButton } from 'components/Buttons/SaveButton'; | ||||||
| import { Modal } from 'components/Modals/Modal'; | import { Modal } from 'components/Modals/Modal'; | ||||||
| import { useCreateBlacklist } from 'hooks/Network/Blacklist'; | import { useCreateBlacklist } from 'hooks/Network/Blacklist'; | ||||||
| import { AxiosError } from 'models/Axios'; |  | ||||||
|  |  | ||||||
| const CreateBlacklistModal = () => { | const CreateBlacklistModal = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const initialRef = React.useRef<HTMLInputElement>(null); |  | ||||||
|   const modalProps = useDisclosure(); |   const modalProps = useDisclosure(); | ||||||
|   const createDevice = useCreateBlacklist(); |   const createDevice = useCreateBlacklist(); | ||||||
|   const [serialNumber, setSerialNumber] = React.useState<string>(''); |   const [serialNumber, setSerialNumber] = React.useState<string>(''); | ||||||
| @@ -32,50 +43,41 @@ const CreateBlacklistModal = () => { | |||||||
|           }); |           }); | ||||||
|           modalProps.onClose(); |           modalProps.onClose(); | ||||||
|         }, |         }, | ||||||
|         onError: (e) => { |  | ||||||
|           toast({ |  | ||||||
|             id: 'add-blacklist-error', |  | ||||||
|             title: t('common.error'), |  | ||||||
|             description: (e as AxiosError)?.response?.data?.ErrorDescription, |  | ||||||
|             status: 'error', |  | ||||||
|             duration: 5000, |  | ||||||
|             isClosable: true, |  | ||||||
|             position: 'top-right', |  | ||||||
|           }); |  | ||||||
|         }, |  | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const isSerialValid = serialNumber.length === 12 && serialNumber.match('^[a-fA-F0-9]+$') !== null; |   const isSerialValid = serialNumber.length === 12 && serialNumber.match('^[a-fA-F0-9]+$') !== null; | ||||||
|  |  | ||||||
|   const onOpen = () => { |   React.useEffect(() => { | ||||||
|     setSerialNumber(''); |     setSerialNumber(''); | ||||||
|     setReason(''); |     setReason(''); | ||||||
|     modalProps.onOpen(); |   }, [modalProps.isOpen]); | ||||||
|     setTimeout(() => { |  | ||||||
|       initialRef.current?.focus(); |  | ||||||
|     }, 200); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <CreateButton onClick={onOpen} isCompact ml={2} /> |       <CreateButton onClick={modalProps.onOpen} isCompact ml={2} /> | ||||||
|       <Modal |       <Modal | ||||||
|         {...modalProps} |         {...modalProps} | ||||||
|         title={t('controller.devices.add_blacklist')} |         title={t('controller.devices.add_blacklist')} | ||||||
|         topRightButtons={<SaveButton onClick={onSave} isLoading={createDevice.isLoading} isCompact />} |         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}> |           <FormControl isInvalid={!isSerialValid} mb={2}> | ||||||
|             <FormLabel>{t('inventory.serial_number')}</FormLabel> |             <FormLabel>{t('inventory.serial_number')}</FormLabel> | ||||||
|             <Input |             <Input type="text" onChange={(e) => setSerialNumber(e.target.value)} value={serialNumber} w="140px" /> | ||||||
|               type="text" |  | ||||||
|               onChange={(e) => setSerialNumber(e.target.value)} |  | ||||||
|               value={serialNumber} |  | ||||||
|               w="140px" |  | ||||||
|               ref={initialRef} |  | ||||||
|             /> |  | ||||||
|             <FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage> |             <FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage> | ||||||
|           </FormControl> |           </FormControl> | ||||||
|           <FormControl> |           <FormControl> | ||||||
|   | |||||||
| @@ -34,7 +34,6 @@ interface Props { | |||||||
|   onOpenConfigureModal: (serialNumber: string) => void; |   onOpenConfigureModal: (serialNumber: string) => void; | ||||||
|   onOpenTelemetryModal: (serialNumber: string) => void; |   onOpenTelemetryModal: (serialNumber: string) => void; | ||||||
|   onOpenScriptModal: (device: GatewayDevice) => void; |   onOpenScriptModal: (device: GatewayDevice) => void; | ||||||
|   onOpenRebootModal: (serialNumber: string) => void; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const Actions: React.FC<Props> = ({ | const Actions: React.FC<Props> = ({ | ||||||
| @@ -48,7 +47,6 @@ const Actions: React.FC<Props> = ({ | |||||||
|   onOpenConfigureModal, |   onOpenConfigureModal, | ||||||
|   onOpenTelemetryModal, |   onOpenTelemetryModal, | ||||||
|   onOpenScriptModal, |   onOpenScriptModal, | ||||||
|   onOpenRebootModal, |  | ||||||
| }) => { | }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); |   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||||
| @@ -104,7 +102,6 @@ const Actions: React.FC<Props> = ({ | |||||||
|         onOpenConfigureModal={onOpenConfigureModal} |         onOpenConfigureModal={onOpenConfigureModal} | ||||||
|         onOpenTelemetryModal={onOpenTelemetryModal} |         onOpenTelemetryModal={onOpenTelemetryModal} | ||||||
|         onOpenScriptModal={onOpenScriptModal} |         onOpenScriptModal={onOpenScriptModal} | ||||||
|         onOpenRebootModal={onOpenRebootModal} |  | ||||||
|       /> |       /> | ||||||
|       <Tooltip hasArrow label={t('common.view_details')} placement="top"> |       <Tooltip hasArrow label={t('common.view_details')} placement="top"> | ||||||
|         <Link href={`#/devices/${device.serialNumber}`}> |         <Link href={`#/devices/${device.serialNumber}`}> | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react'; | import { Box, Button, Heading, Image, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react'; | ||||||
| import { LockSimple } from 'phosphor-react'; | import { LockSimple } from 'phosphor-react'; | ||||||
| import ReactCountryFlag from 'react-country-flag'; | import ReactCountryFlag from 'react-country-flag'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
| import Actions from './Actions'; | import Actions from './Actions'; | ||||||
| import DeviceListFirmwareButton from './FirmwareButton'; | import DeviceListFirmwareButton from './FirmwareButton'; | ||||||
| import AP from './icons/AP.png'; | import AP from './icons/AP.png'; | ||||||
| @@ -20,7 +21,6 @@ import { ConfigureModal } from 'components/Modals/ConfigureModal'; | |||||||
| import { EventQueueModal } from 'components/Modals/EventQueueModal'; | import { EventQueueModal } from 'components/Modals/EventQueueModal'; | ||||||
| import FactoryResetModal from 'components/Modals/FactoryResetModal'; | import FactoryResetModal from 'components/Modals/FactoryResetModal'; | ||||||
| import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal'; | import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal'; | ||||||
| import { RebootModal } from 'components/Modals/RebootModal'; |  | ||||||
| import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; | import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; | ||||||
| import { TelemetryModal } from 'components/Modals/TelemetryModal'; | import { TelemetryModal } from 'components/Modals/TelemetryModal'; | ||||||
| import { TraceModal } from 'components/Modals/TraceModal'; | import { TraceModal } from 'components/Modals/TraceModal'; | ||||||
| @@ -49,6 +49,7 @@ const BADGE_COLORS: Record<string, string> = { | |||||||
|  |  | ||||||
| const DeviceListCard = () => { | const DeviceListCard = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|   const [serialNumber, setSerialNumber] = React.useState<string>(''); |   const [serialNumber, setSerialNumber] = React.useState<string>(''); | ||||||
|   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); |   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); | ||||||
|   const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined); |   const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined); | ||||||
| @@ -59,7 +60,6 @@ const DeviceListCard = () => { | |||||||
|   const eventQueueProps = useDisclosure(); |   const eventQueueProps = useDisclosure(); | ||||||
|   const telemetryModalProps = useDisclosure(); |   const telemetryModalProps = useDisclosure(); | ||||||
|   const configureModalProps = useDisclosure(); |   const configureModalProps = useDisclosure(); | ||||||
|   const rebootModalProps = useDisclosure(); |  | ||||||
|   const scriptModal = useScriptModal(); |   const scriptModal = useScriptModal(); | ||||||
|   const getCount = useGetDeviceCount({ enabled: true }); |   const getCount = useGetDeviceCount({ enabled: true }); | ||||||
|   const getDevices = useGetDevices({ |   const getDevices = useGetDevices({ | ||||||
| @@ -98,9 +98,9 @@ const DeviceListCard = () => { | |||||||
|     setSerialNumber(serial); |     setSerialNumber(serial); | ||||||
|     configureModalProps.onOpen(); |     configureModalProps.onOpen(); | ||||||
|   }; |   }; | ||||||
|   const onOpenReboot = (serial: string) => { |  | ||||||
|     setSerialNumber(serial); |   const goToSerial = (serial: string) => () => { | ||||||
|     rebootModalProps.onOpen(); |     navigate(`/devices/${serial}`); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const badgeCell = React.useCallback( |   const badgeCell = React.useCallback( | ||||||
| @@ -160,9 +160,9 @@ const DeviceListCard = () => { | |||||||
|  |  | ||||||
|   const serialCell = React.useCallback( |   const serialCell = React.useCallback( | ||||||
|     (device: DeviceWithStatus) => ( |     (device: DeviceWithStatus) => ( | ||||||
|       <Link href={`#/devices/${device.serialNumber}`} fontSize="sm" my="auto" pt={1}> |       <Button variant="link" onClick={goToSerial(device.serialNumber)} fontSize="sm"> | ||||||
|         <pre>{device.serialNumber}</pre> |         <pre>{device.serialNumber}</pre> | ||||||
|       </Link> |       </Button> | ||||||
|     ), |     ), | ||||||
|     [], |     [], | ||||||
|   ); |   ); | ||||||
| @@ -216,7 +216,6 @@ const DeviceListCard = () => { | |||||||
|         onOpenConfigureModal={onOpenConfigure} |         onOpenConfigureModal={onOpenConfigure} | ||||||
|         onOpenTelemetryModal={onOpenTelemetry} |         onOpenTelemetryModal={onOpenTelemetry} | ||||||
|         onOpenScriptModal={scriptModal.openModal} |         onOpenScriptModal={scriptModal.openModal} | ||||||
|         onOpenRebootModal={onOpenReboot} |  | ||||||
|       /> |       /> | ||||||
|     ), |     ), | ||||||
|     [], |     [], | ||||||
| @@ -407,6 +406,7 @@ const DeviceListCard = () => { | |||||||
|             // @ts-ignore |             // @ts-ignore | ||||||
|             setPageInfo={setPageInfo} |             setPageInfo={setPageInfo} | ||||||
|             saveSettingsId="gateway.devices.table" |             saveSettingsId="gateway.devices.table" | ||||||
|  |             minHeight="600px" | ||||||
|           /> |           /> | ||||||
|         </Box> |         </Box> | ||||||
|       </CardBody> |       </CardBody> | ||||||
| @@ -417,7 +417,6 @@ const DeviceListCard = () => { | |||||||
|       <EventQueueModal modalProps={eventQueueProps} serialNumber={serialNumber} /> |       <EventQueueModal modalProps={eventQueueProps} serialNumber={serialNumber} /> | ||||||
|       <ConfigureModal modalProps={configureModalProps} serialNumber={serialNumber} /> |       <ConfigureModal modalProps={configureModalProps} serialNumber={serialNumber} /> | ||||||
|       <TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} /> |       <TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} /> | ||||||
|       <RebootModal modalProps={rebootModalProps} serialNumber={serialNumber} /> |  | ||||||
|       {scriptModal.modal} |       {scriptModal.modal} | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -191,7 +191,6 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => { | |||||||
|               ml={2} |               ml={2} | ||||||
|             /> |             /> | ||||||
|             {isEditingDescription && ( |             {isEditingDescription && ( | ||||||
|               // @ts-ignore |  | ||||||
|               <SaveButton onClick={onSaveDescription} ml={2} isCompact size="sm" isLoading={updateFirmware.isLoading} /> |               <SaveButton onClick={onSaveDescription} ml={2} isCompact size="sm" isLoading={updateFirmware.isLoading} /> | ||||||
|             )} |             )} | ||||||
|           </FormLabel> |           </FormLabel> | ||||||
| @@ -203,15 +202,18 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => { | |||||||
|             isDisabled={!isEditingDescription} |             isDisabled={!isEditingDescription} | ||||||
|           /> |           /> | ||||||
|         </FormControl> |         </FormControl> | ||||||
|       </SimpleGrid> |         <FormControl> | ||||||
|       <FormControl mt={2}> |  | ||||||
|           <FormLabel> |           <FormLabel> | ||||||
|             {t('common.notes')}{' '} |             {t('common.notes')}{' '} | ||||||
|             <Popover trigger="click" placement="auto"> |             <Popover trigger="click" placement="auto"> | ||||||
|               {({ onClose }) => ( |               {({ onClose }) => ( | ||||||
|                 <> |                 <> | ||||||
|                   <PopoverTrigger> |                   <PopoverTrigger> | ||||||
|                   <IconButton aria-label={`${t('crud.add')} ${t('common.note')}`} size="sm" icon={<Plus size={20} />} /> |                     <IconButton | ||||||
|  |                       aria-label={`${t('crud.add')} ${t('common.note')}`} | ||||||
|  |                       size="sm" | ||||||
|  |                       icon={<Plus size={20} />} | ||||||
|  |                     /> | ||||||
|                   </PopoverTrigger> |                   </PopoverTrigger> | ||||||
|                   <PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}> |                   <PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}> | ||||||
|                     <PopoverArrow /> |                     <PopoverArrow /> | ||||||
| @@ -237,17 +239,11 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => { | |||||||
|               )} |               )} | ||||||
|             </Popover> |             </Popover> | ||||||
|           </FormLabel> |           </FormLabel> | ||||||
|       </FormControl> |           <Box overflowX="auto" overflowY="auto" maxH="400px"> | ||||||
|       <Box overflowX="auto" overflowY="auto" maxH="400px" mb={4}> |             <DataTable columns={columns as Column<object>[]} data={notes} obj={t('common.notes')} minHeight="200px" /> | ||||||
|         <DataTable |  | ||||||
|           columns={columns as Column<object>[]} |  | ||||||
|           data={notes} |  | ||||||
|           obj={t('common.notes')} |  | ||||||
|           minHeight="200px" |  | ||||||
|           showAllRows |  | ||||||
|           hideControls |  | ||||||
|         /> |  | ||||||
|           </Box> |           </Box> | ||||||
|  |         </FormControl> | ||||||
|  |       </SimpleGrid> | ||||||
|     </Modal> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,101 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { |  | ||||||
|   Alert, |  | ||||||
|   AlertDescription, |  | ||||||
|   AlertIcon, |  | ||||||
|   AlertTitle, |  | ||||||
|   Box, |  | ||||||
|   Button, |  | ||||||
|   Center, |  | ||||||
|   Tag, |  | ||||||
|   TagLabel, |  | ||||||
|   Text, |  | ||||||
|   useDisclosure, |  | ||||||
|   useToast, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import axios from 'axios'; |  | ||||||
| import { Database } from 'phosphor-react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import FormattedDate from 'components/InformationDisplays/FormattedDate'; |  | ||||||
| import { Modal } from 'components/Modals/Modal'; |  | ||||||
| import { useGetFirmwareDbUpdate, useUpdateFirmwareDb } from 'hooks/Network/Firmware'; |  | ||||||
|  |  | ||||||
| const UpdateDbButton = () => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const toast = useToast(); |  | ||||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); |  | ||||||
|   const updateDb = useUpdateFirmwareDb(); |  | ||||||
|   const getLastUpdate = useGetFirmwareDbUpdate(); |  | ||||||
|  |  | ||||||
|   const onUpdateClick = async () => { |  | ||||||
|     updateDb.mutate(undefined, { |  | ||||||
|       onSuccess: () => { |  | ||||||
|         toast({ |  | ||||||
|           id: `firmware-db-update-success`, |  | ||||||
|           title: t('common.success'), |  | ||||||
|           description: t('firmware.started_db_update'), |  | ||||||
|           status: 'success', |  | ||||||
|           duration: 5000, |  | ||||||
|           isClosable: true, |  | ||||||
|           position: 'top-right', |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|       onError: (e) => { |  | ||||||
|         if (axios.isAxiosError(e)) { |  | ||||||
|           toast({ |  | ||||||
|             id: `firmware-db-update-error`, |  | ||||||
|             title: t('common.error'), |  | ||||||
|             description: e?.response?.data?.ErrorDescription, |  | ||||||
|             status: 'error', |  | ||||||
|             duration: 5000, |  | ||||||
|             isClosable: true, |  | ||||||
|             position: 'top-right', |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <Button colorScheme="teal" leftIcon={<Database size={20} />} onClick={onOpen}> |  | ||||||
|         {t('firmware.last_db_update_title')} |  | ||||||
|       </Button> |  | ||||||
|       <Modal |  | ||||||
|         isOpen={isOpen} |  | ||||||
|         onClose={onClose} |  | ||||||
|         title={t('firmware.last_db_update_modal')} |  | ||||||
|         tags={ |  | ||||||
|           <Tag colorScheme="blue" size="lg"> |  | ||||||
|             <TagLabel display="flex"> |  | ||||||
|               <Text mr={1}>Last Update:</Text> |  | ||||||
|               <FormattedDate date={getLastUpdate.data?.lastUpdateTime} /> |  | ||||||
|             </TagLabel> |  | ||||||
|           </Tag> |  | ||||||
|         } |  | ||||||
|       > |  | ||||||
|         <Box> |  | ||||||
|           <Alert status="warning"> |  | ||||||
|             <AlertIcon /> |  | ||||||
|             <Box> |  | ||||||
|               <AlertTitle>{t('common.warning')}</AlertTitle> |  | ||||||
|               <AlertDescription>{t('firmware.db_update_warning')}</AlertDescription> |  | ||||||
|             </Box> |  | ||||||
|           </Alert> |  | ||||||
|           <Center my={4}> |  | ||||||
|             <Button |  | ||||||
|               colorScheme="red" |  | ||||||
|               leftIcon={<Database size={20} />} |  | ||||||
|               onClick={onUpdateClick} |  | ||||||
|               isLoading={updateDb.isLoading} |  | ||||||
|             > |  | ||||||
|               {t('firmware.start_db_update')} |  | ||||||
|             </Button> |  | ||||||
|           </Center> |  | ||||||
|         </Box> |  | ||||||
|       </Modal> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default UpdateDbButton; |  | ||||||
| @@ -16,7 +16,6 @@ import { MagnifyingGlass } from 'phosphor-react'; | |||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| import FirmwareDetailsModal from './Modal'; | import FirmwareDetailsModal from './Modal'; | ||||||
| import UpdateDbButton from './UpdateDbButton'; |  | ||||||
| import UriCell from './UriCell'; | import UriCell from './UriCell'; | ||||||
| import { RefreshButton } from 'components/Buttons/RefreshButton'; | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
| import { CardBody } from 'components/Containers/Card/CardBody'; | import { CardBody } from 'components/Containers/Card/CardBody'; | ||||||
| @@ -144,7 +143,6 @@ const FirmwareListTable = () => { | |||||||
|           </Box> |           </Box> | ||||||
|           <Text>{t('controller.firmware.show_dev_releases')}</Text> |           <Text>{t('controller.firmware.show_dev_releases')}</Text> | ||||||
|           <Switch isChecked={showDevFirmware} onChange={toggle} size="lg" /> |           <Switch isChecked={showDevFirmware} onChange={toggle} size="lg" /> | ||||||
|           <UpdateDbButton /> |  | ||||||
|           <RefreshButton |           <RefreshButton | ||||||
|             onClick={() => { |             onClick={() => { | ||||||
|               getDeviceTypes.refetch(); |               getDeviceTypes.refetch(); | ||||||
|   | |||||||
| @@ -48,10 +48,7 @@ const _LoginForm: React.FC<_LoginFormProps> = ({ setActiveForm }) => { | |||||||
|   const displayError = useMemo(() => { |   const displayError = useMemo(() => { | ||||||
|     const loginError: AxiosError = error as AxiosError; |     const loginError: AxiosError = error as AxiosError; | ||||||
|  |  | ||||||
|     if (loginError?.response?.data?.ErrorCode === 5) return t('login.waiting_for_email_verification'); |     if (loginError?.response?.data?.ErrorCode === 4) return t('login.waiting_for_email_verification'); | ||||||
|     if (loginError?.response?.data?.ErrorCode === 15) { |  | ||||||
|       return t('login.suspended_error'); |  | ||||||
|     } |  | ||||||
|     return t('login.invalid_credentials'); |     return t('login.invalid_credentials'); | ||||||
|   }, [t, error]); |   }, [t, error]); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,139 +0,0 @@ | |||||||
| import React from 'react'; |  | ||||||
| import { CopyIcon } from '@chakra-ui/icons'; |  | ||||||
| import { |  | ||||||
|   IconButton, |  | ||||||
|   Tooltip, |  | ||||||
|   Popover, |  | ||||||
|   PopoverArrow, |  | ||||||
|   PopoverBody, |  | ||||||
|   PopoverCloseButton, |  | ||||||
|   PopoverContent, |  | ||||||
|   PopoverFooter, |  | ||||||
|   PopoverHeader, |  | ||||||
|   PopoverTrigger, |  | ||||||
|   Center, |  | ||||||
|   Box, |  | ||||||
|   Button, |  | ||||||
|   useDisclosure, |  | ||||||
|   HStack, |  | ||||||
|   Text, |  | ||||||
|   useClipboard, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import { Eye, Trash } from 'phosphor-react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import EditSecretButton from './EditButton'; |  | ||||||
| import { Secret, useDeleteSystemSecret } from 'hooks/Network/Secrets'; |  | ||||||
|  |  | ||||||
| interface Props { |  | ||||||
|   secret: Secret; |  | ||||||
|   isDisabled?: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const SystemSecretActions = ({ secret, isDisabled }: Props) => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); |  | ||||||
|   const deleteSecret = useDeleteSystemSecret(); |  | ||||||
|   const { hasCopied, onCopy } = useClipboard(secret.value); |  | ||||||
|  |  | ||||||
|   const handleDeleteClick = React.useCallback(() => { |  | ||||||
|     deleteSecret.mutate(secret.key, { |  | ||||||
|       onSuccess: () => { |  | ||||||
|         onClose(); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <HStack mx="auto"> |  | ||||||
|       <Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}> |  | ||||||
|         <Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}> |  | ||||||
|           <Box> |  | ||||||
|             <PopoverTrigger> |  | ||||||
|               <IconButton |  | ||||||
|                 aria-label="delete-device" |  | ||||||
|                 colorScheme="red" |  | ||||||
|                 icon={<Trash size={20} />} |  | ||||||
|                 size="sm" |  | ||||||
|                 isDisabled={isDisabled} |  | ||||||
|               /> |  | ||||||
|             </PopoverTrigger> |  | ||||||
|           </Box> |  | ||||||
|         </Tooltip> |  | ||||||
|         <PopoverContent w="340px"> |  | ||||||
|           <PopoverArrow /> |  | ||||||
|           <PopoverCloseButton /> |  | ||||||
|           <PopoverHeader> |  | ||||||
|             {t('crud.delete')} {secret.key} |  | ||||||
|           </PopoverHeader> |  | ||||||
|           <PopoverBody> |  | ||||||
|             <Text whiteSpace="break-spaces">{t('crud.delete_confirm', { obj: t('system.secrets_one') })}</Text> |  | ||||||
|           </PopoverBody> |  | ||||||
|           <PopoverFooter> |  | ||||||
|             <Center> |  | ||||||
|               <Button colorScheme="gray" mr="1" onClick={onClose}> |  | ||||||
|                 {t('common.cancel')} |  | ||||||
|               </Button> |  | ||||||
|               <Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteSecret.isLoading}> |  | ||||||
|                 {t('common.yes')} |  | ||||||
|               </Button> |  | ||||||
|             </Center> |  | ||||||
|           </PopoverFooter> |  | ||||||
|         </PopoverContent> |  | ||||||
|       </Popover> |  | ||||||
|       <Tooltip |  | ||||||
|         label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`} |  | ||||||
|         hasArrow |  | ||||||
|         closeOnClick={false} |  | ||||||
|       > |  | ||||||
|         <IconButton |  | ||||||
|           aria-label={t('common.copy')} |  | ||||||
|           icon={<CopyIcon h={5} w={5} />} |  | ||||||
|           onClick={onCopy} |  | ||||||
|           size="sm" |  | ||||||
|           colorScheme="teal" |  | ||||||
|           mr={2} |  | ||||||
|         /> |  | ||||||
|       </Tooltip> |  | ||||||
|       <EditSecretButton secret={secret} /> |  | ||||||
|       <Popover> |  | ||||||
|         <Tooltip label={`${t('common.view')} ${t('system.secrets_one')}`} hasArrow closeOnClick={false}> |  | ||||||
|           <Box> |  | ||||||
|             <PopoverTrigger> |  | ||||||
|               <IconButton aria-label={t('common.view')} icon={<Eye size={20} />} size="sm" colorScheme="purple" /> |  | ||||||
|             </PopoverTrigger> |  | ||||||
|           </Box> |  | ||||||
|         </Tooltip> |  | ||||||
|         <PopoverContent w="560px"> |  | ||||||
|           <PopoverArrow /> |  | ||||||
|           <PopoverCloseButton /> |  | ||||||
|           <PopoverHeader> |  | ||||||
|             {t('common.view')} {secret.key} |  | ||||||
|             <Tooltip |  | ||||||
|               label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`} |  | ||||||
|               hasArrow |  | ||||||
|               closeOnClick={false} |  | ||||||
|             > |  | ||||||
|               <IconButton |  | ||||||
|                 aria-label={t('common.copy')} |  | ||||||
|                 icon={<CopyIcon h={4} w={4} />} |  | ||||||
|                 onClick={onCopy} |  | ||||||
|                 size="xs" |  | ||||||
|                 colorScheme="teal" |  | ||||||
|                 ml={2} |  | ||||||
|               /> |  | ||||||
|             </Tooltip> |  | ||||||
|           </PopoverHeader> |  | ||||||
|           <PopoverBody> |  | ||||||
|             <Text whiteSpace="break-spaces"> |  | ||||||
|               <Center> |  | ||||||
|                 <pre style={{ fontFamily: 'monospace' }}>{secret.value}</pre> |  | ||||||
|               </Center> |  | ||||||
|             </Text> |  | ||||||
|           </PopoverBody> |  | ||||||
|         </PopoverContent> |  | ||||||
|       </Popover> |  | ||||||
|     </HStack> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default SystemSecretActions; |  | ||||||
| @@ -1,118 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { |  | ||||||
|   Box, |  | ||||||
|   FormControl, |  | ||||||
|   FormErrorMessage, |  | ||||||
|   FormLabel, |  | ||||||
|   Input, |  | ||||||
|   Textarea, |  | ||||||
|   useDisclosure, |  | ||||||
|   useToast, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { CreateButton } from '../../../components/Buttons/CreateButton'; |  | ||||||
| import { SaveButton } from '../../../components/Buttons/SaveButton'; |  | ||||||
| import { Modal } from '../../../components/Modals/Modal'; |  | ||||||
| import { useCreateSystemSecret } from 'hooks/Network/Secrets'; |  | ||||||
| import { AxiosError } from 'models/Axios'; |  | ||||||
|  |  | ||||||
| type FormValues = { |  | ||||||
|   key: string; |  | ||||||
|   value: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const DEFAULT_FORM_VALUES: FormValues = { |  | ||||||
|   key: '', |  | ||||||
|   value: '', |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const SystemSecretCreateButton = () => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const toast = useToast(); |  | ||||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); |  | ||||||
|   const [form, setForm] = React.useState<FormValues>(DEFAULT_FORM_VALUES); |  | ||||||
|   const [isNameChanged, setIsNameChanged] = React.useState(false); |  | ||||||
|   const [isValueChanged, setIsValueChanged] = React.useState(false); |  | ||||||
|   const create = useCreateSystemSecret(); |  | ||||||
|  |  | ||||||
|   const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { |  | ||||||
|     setForm({ ...form, key: e.target.value }); |  | ||||||
|     if (!isNameChanged) setIsNameChanged(true); |  | ||||||
|   }; |  | ||||||
|   const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { |  | ||||||
|     setForm({ ...form, value: e.target.value }); |  | ||||||
|     if (!isValueChanged) setIsValueChanged(true); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const isNameError = form.key.length === 0; |  | ||||||
|   const isValueError = form.value.length === 0; |  | ||||||
|  |  | ||||||
|   const onSubmit = () => { |  | ||||||
|     create.mutate(form, { |  | ||||||
|       onSuccess: () => { |  | ||||||
|         toast({ |  | ||||||
|           id: 'create-system-secret-success', |  | ||||||
|           title: t('common.success'), |  | ||||||
|           description: t('crud.success_update_obj', { |  | ||||||
|             obj: t('system.secrets_one'), |  | ||||||
|           }), |  | ||||||
|           status: 'success', |  | ||||||
|           duration: 5000, |  | ||||||
|           isClosable: true, |  | ||||||
|           position: 'top-right', |  | ||||||
|         }); |  | ||||||
|         onClose(); |  | ||||||
|       }, |  | ||||||
|       onError: (e) => { |  | ||||||
|         toast({ |  | ||||||
|           id: 'create-system-secret-error', |  | ||||||
|           title: t('common.error'), |  | ||||||
|           description: (e as AxiosError)?.response?.data?.ErrorDescription, |  | ||||||
|           status: 'error', |  | ||||||
|           duration: 5000, |  | ||||||
|           isClosable: true, |  | ||||||
|           position: 'top-right', |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const handleOpenClick = () => { |  | ||||||
|     setIsNameChanged(false); |  | ||||||
|     setIsValueChanged(false); |  | ||||||
|     setForm(DEFAULT_FORM_VALUES); |  | ||||||
|     onOpen(); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <CreateButton onClick={handleOpenClick} isCompact /> |  | ||||||
|       <Modal |  | ||||||
|         isOpen={isOpen} |  | ||||||
|         onClose={onClose} |  | ||||||
|         title={t('system.secrets_create')} |  | ||||||
|         topRightButtons={ |  | ||||||
|           <SaveButton onClick={onSubmit} isDisabled={isNameError || isValueError} isLoading={create.isLoading} /> |  | ||||||
|         } |  | ||||||
|         options={{ |  | ||||||
|           modalSize: 'sm', |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         <Box> |  | ||||||
|           <FormControl mb={2} isInvalid={isNameError && isNameChanged}> |  | ||||||
|             <FormLabel>{t('common.name')}</FormLabel> |  | ||||||
|             <Input value={form.key} onChange={onKeyChange} /> |  | ||||||
|             <FormErrorMessage>{t('form.required')}</FormErrorMessage> |  | ||||||
|           </FormControl> |  | ||||||
|           <FormControl mb={2} isInvalid={isValueError && isValueChanged}> |  | ||||||
|             <FormLabel>{t('common.value')}</FormLabel> |  | ||||||
|             <Textarea value={form.value} onChange={onValueChange} rows={2} /> |  | ||||||
|             <FormErrorMessage>{t('form.required')}</FormErrorMessage> |  | ||||||
|           </FormControl> |  | ||||||
|         </Box> |  | ||||||
|       </Modal> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default SystemSecretCreateButton; |  | ||||||
| @@ -1,146 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { |  | ||||||
|   Box, |  | ||||||
|   Button, |  | ||||||
|   Center, |  | ||||||
|   FormControl, |  | ||||||
|   FormErrorMessage, |  | ||||||
|   FormLabel, |  | ||||||
|   IconButton, |  | ||||||
|   Input, |  | ||||||
|   Popover, |  | ||||||
|   PopoverArrow, |  | ||||||
|   PopoverBody, |  | ||||||
|   PopoverCloseButton, |  | ||||||
|   PopoverContent, |  | ||||||
|   PopoverFooter, |  | ||||||
|   PopoverHeader, |  | ||||||
|   PopoverTrigger, |  | ||||||
|   Text, |  | ||||||
|   Textarea, |  | ||||||
|   Tooltip, |  | ||||||
|   useDisclosure, |  | ||||||
|   useToast, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import { Pencil } from 'phosphor-react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { Secret, useUpdateSystemSecret } from 'hooks/Network/Secrets'; |  | ||||||
| import { AxiosError } from 'models/Axios'; |  | ||||||
|  |  | ||||||
| type FormValues = { |  | ||||||
|   key: string; |  | ||||||
|   value: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| type Props = { |  | ||||||
|   secret: Secret; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const EditSecretButton = ({ secret }: Props) => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const toast = useToast(); |  | ||||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); |  | ||||||
|   const [form, setForm] = React.useState<FormValues>({ |  | ||||||
|     key: secret.key, |  | ||||||
|     value: secret.value, |  | ||||||
|   }); |  | ||||||
|   const [isNameChanged, setIsNameChanged] = React.useState(false); |  | ||||||
|   const [isValueChanged, setIsValueChanged] = React.useState(false); |  | ||||||
|   const update = useUpdateSystemSecret(); |  | ||||||
|  |  | ||||||
|   const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { |  | ||||||
|     setForm({ ...form, key: e.target.value }); |  | ||||||
|     if (!isNameChanged) setIsNameChanged(true); |  | ||||||
|   }; |  | ||||||
|   const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { |  | ||||||
|     setForm({ ...form, value: e.target.value }); |  | ||||||
|     if (!isValueChanged) setIsValueChanged(true); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const isNameError = form.key.length === 0; |  | ||||||
|   const isValueError = form.value.length === 0; |  | ||||||
|  |  | ||||||
|   const onSubmit = () => { |  | ||||||
|     update.mutate(form, { |  | ||||||
|       onSuccess: () => { |  | ||||||
|         toast({ |  | ||||||
|           id: 'create-system-secret-success', |  | ||||||
|           title: t('common.success'), |  | ||||||
|           status: 'success', |  | ||||||
|           duration: 5000, |  | ||||||
|           isClosable: true, |  | ||||||
|           position: 'top-right', |  | ||||||
|         }); |  | ||||||
|         onClose(); |  | ||||||
|       }, |  | ||||||
|       onError: (e) => { |  | ||||||
|         toast({ |  | ||||||
|           id: 'create-system-secret-error', |  | ||||||
|           title: t('common.error'), |  | ||||||
|           description: (e as AxiosError)?.response?.data?.ErrorDescription, |  | ||||||
|           status: 'error', |  | ||||||
|           duration: 5000, |  | ||||||
|           isClosable: true, |  | ||||||
|           position: 'top-right', |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const handleOpenClick = () => { |  | ||||||
|     setIsNameChanged(false); |  | ||||||
|     setIsValueChanged(false); |  | ||||||
|     setForm({ |  | ||||||
|       key: secret.key, |  | ||||||
|       value: secret.value, |  | ||||||
|     }); |  | ||||||
|     onOpen(); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Popover isOpen={isOpen} onOpen={handleOpenClick} onClose={onClose}> |  | ||||||
|       <Tooltip hasArrow label={t('crud.edit')} placement="top" isDisabled={isOpen}> |  | ||||||
|         <Box> |  | ||||||
|           <PopoverTrigger> |  | ||||||
|             <IconButton aria-label="delete-device" colorScheme="blue" icon={<Pencil size={20} />} size="sm" /> |  | ||||||
|           </PopoverTrigger> |  | ||||||
|         </Box> |  | ||||||
|       </Tooltip> |  | ||||||
|       <PopoverContent w="340px"> |  | ||||||
|         <PopoverArrow /> |  | ||||||
|         <PopoverCloseButton /> |  | ||||||
|         <PopoverHeader> |  | ||||||
|           {t('crud.edit')} {secret.key} |  | ||||||
|         </PopoverHeader> |  | ||||||
|         <PopoverBody> |  | ||||||
|           <Text whiteSpace="break-spaces"> |  | ||||||
|             <Box> |  | ||||||
|               <FormControl mb={2} isInvalid={isNameError && isNameChanged}> |  | ||||||
|                 <FormLabel>{t('common.name')}</FormLabel> |  | ||||||
|                 <Input value={form.key} onChange={onKeyChange} /> |  | ||||||
|                 <FormErrorMessage>{t('form.required')}</FormErrorMessage> |  | ||||||
|               </FormControl> |  | ||||||
|               <FormControl mb={2} isInvalid={isValueError && isValueChanged}> |  | ||||||
|                 <FormLabel>{t('common.value')}</FormLabel> |  | ||||||
|                 <Textarea value={form.value} onChange={onValueChange} rows={2} /> |  | ||||||
|                 <FormErrorMessage>{t('form.required')}</FormErrorMessage> |  | ||||||
|               </FormControl> |  | ||||||
|             </Box> |  | ||||||
|           </Text> |  | ||||||
|         </PopoverBody> |  | ||||||
|         <PopoverFooter> |  | ||||||
|           <Center> |  | ||||||
|             <Button colorScheme="gray" mr="1" onClick={onClose}> |  | ||||||
|               {t('common.cancel')} |  | ||||||
|             </Button> |  | ||||||
|             <Button colorScheme="blue" ml="1" onClick={onSubmit} isLoading={update.isLoading}> |  | ||||||
|               {t('common.save')} |  | ||||||
|             </Button> |  | ||||||
|           </Center> |  | ||||||
|         </PopoverFooter> |  | ||||||
|       </PopoverContent> |  | ||||||
|     </Popover> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default EditSecretButton; |  | ||||||
| @@ -1,70 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { Box } from '@chakra-ui/react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import { DataTable } from '../../../components/DataTables/DataTable'; |  | ||||||
| import SystemSecretActions from './Actions'; |  | ||||||
| import { Secret, useGetAllSystemSecrets, useGetSystemSecretsDictionary } from 'hooks/Network/Secrets'; |  | ||||||
| import { Column } from 'models/Table'; |  | ||||||
|  |  | ||||||
| const SystemSecretsTable = () => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const getSecrets = useGetAllSystemSecrets(); |  | ||||||
|   const getDictionary = useGetSystemSecretsDictionary(); |  | ||||||
|  |  | ||||||
|   const descriptionCell = React.useCallback( |  | ||||||
|     (secret: Secret) => { |  | ||||||
|       if (!getDictionary.data) return '-'; |  | ||||||
|  |  | ||||||
|       return getDictionary.data.find((d) => d.key === secret.key)?.description ?? '-'; |  | ||||||
|     }, |  | ||||||
|     [getDictionary.data], |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   const actionCell = React.useCallback((secret: Secret) => <SystemSecretActions secret={secret} />, []); |  | ||||||
|  |  | ||||||
|   const columns = React.useMemo( |  | ||||||
|     (): Column<Secret>[] => [ |  | ||||||
|       { |  | ||||||
|         id: 'key', |  | ||||||
|         Header: t('common.name'), |  | ||||||
|         Footer: '', |  | ||||||
|         accessor: 'key', |  | ||||||
|         alwaysShow: true, |  | ||||||
|         customWidth: '120px', |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         id: 'description', |  | ||||||
|         Header: t('common.description'), |  | ||||||
|         Footer: '', |  | ||||||
|         Cell: ({ cell }) => descriptionCell(cell.row.original), |  | ||||||
|         accessor: 'description', |  | ||||||
|         hasPopover: true, |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         id: 'actions', |  | ||||||
|         Header: t('common.actions'), |  | ||||||
|         Footer: '', |  | ||||||
|         Cell: (v) => actionCell(v.cell.row.original), |  | ||||||
|         disableSortBy: true, |  | ||||||
|         customWidth: '120px', |  | ||||||
|         alwaysShow: true, |  | ||||||
|       }, |  | ||||||
|     ], |  | ||||||
|     [t, descriptionCell], |  | ||||||
|   ); |  | ||||||
|   return ( |  | ||||||
|     <Box w="100%"> |  | ||||||
|       <DataTable |  | ||||||
|         columns={columns as Column<object>[]} |  | ||||||
|         saveSettingsId="system.secrets.table" |  | ||||||
|         data={getSecrets.data ?? []} |  | ||||||
|         obj={t('keys.other')} |  | ||||||
|         sortBy={[{ id: 'key', desc: false }]} |  | ||||||
|         showAllRows |  | ||||||
|         hideControls |  | ||||||
|       /> |  | ||||||
|     </Box> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default SystemSecretsTable; |  | ||||||
| @@ -1,53 +0,0 @@ | |||||||
| import * as React from 'react'; |  | ||||||
| import { |  | ||||||
|   BackgroundProps, |  | ||||||
|   Box, |  | ||||||
|   EffectProps, |  | ||||||
|   Heading, |  | ||||||
|   InteractivityProps, |  | ||||||
|   LayoutProps, |  | ||||||
|   PositionProps, |  | ||||||
|   SpaceProps, |  | ||||||
|   Spacer, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; |  | ||||||
| import SystemSecretCreateButton from './CreateButton'; |  | ||||||
| import SystemSecretsTable from './Table'; |  | ||||||
| import { Card } from 'components/Containers/Card'; |  | ||||||
| import { CardBody } from 'components/Containers/Card/CardBody'; |  | ||||||
| import { CardHeader } from 'components/Containers/Card/CardHeader'; |  | ||||||
| import { useAuth } from 'contexts/AuthProvider'; |  | ||||||
|  |  | ||||||
| export interface SystemSecretsCardProps |  | ||||||
|   extends LayoutProps, |  | ||||||
|     SpaceProps, |  | ||||||
|     BackgroundProps, |  | ||||||
|     InteractivityProps, |  | ||||||
|     PositionProps, |  | ||||||
|     EffectProps {} |  | ||||||
|  |  | ||||||
| export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const { user } = useAuth(); |  | ||||||
|  |  | ||||||
|   if (!user || user.userRole !== 'root') { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Box px={4} py={4}> |  | ||||||
|       <Card variant="widget" {...props}> |  | ||||||
|         <CardHeader> |  | ||||||
|           <Heading size="md" my="auto"> |  | ||||||
|             {t('system.secrets')} |  | ||||||
|           </Heading> |  | ||||||
|           <Spacer /> |  | ||||||
|           <SystemSecretCreateButton /> |  | ||||||
|         </CardHeader> |  | ||||||
|         <CardBody p={4}> |  | ||||||
|           <SystemSecretsTable /> |  | ||||||
|         </CardBody> |  | ||||||
|       </Card> |  | ||||||
|     </Box> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -21,8 +21,8 @@ import axios from 'axios'; | |||||||
| import { FloppyDisk } from 'phosphor-react'; | import { FloppyDisk } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| import { Modal } from '../../../../components/Modals/Modal'; |  | ||||||
| import { LoadingOverlay } from 'components/LoadingOverlay'; | import { LoadingOverlay } from 'components/LoadingOverlay'; | ||||||
|  | import { Modal } from 'components/Modals/Modal'; | ||||||
| import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System'; | import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System'; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import React, { useCallback } from 'react'; | import React, { useCallback } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { DataTable } from '../../../components/DataTables/DataTable'; | import { DataTable } from 'components/DataTables/DataTable'; | ||||||
| import { compactDate } from 'helpers/dateFormatting'; | import { compactDate } from 'helpers/dateFormatting'; | ||||||
| import { Column } from 'models/Table'; | import { Column } from 'models/Table'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,11 +19,11 @@ import { | |||||||
| import { MultiValue, Select } from 'chakra-react-select'; | import { MultiValue, Select } from 'chakra-react-select'; | ||||||
| import { ArrowsClockwise } from 'phosphor-react'; | import { ArrowsClockwise } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import FormattedDate from '../../../components/InformationDisplays/FormattedDate'; |  | ||||||
| import SystemLoggingButton from './LoggingButton'; | import SystemLoggingButton from './LoggingButton'; | ||||||
| import SystemCertificatesTable from './SystemCertificatesTable'; | import SystemCertificatesTable from './SystemCertificatesTable'; | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| import { CardBody } from 'components/Containers/Card/CardBody'; | import { CardBody } from 'components/Containers/Card/CardBody'; | ||||||
|  | import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||||
| import { compactSecondsToDetailed } from 'helpers/dateFormatting'; | import { compactSecondsToDetailed } from 'helpers/dateFormatting'; | ||||||
| import { EndpointApiResponse } from 'hooks/Network/Endpoints'; | import { EndpointApiResponse } from 'hooks/Network/Endpoints'; | ||||||
| import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System'; | import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System'; | ||||||
| @@ -65,7 +65,7 @@ const SystemTile = ({ endpoint, token }: Props) => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Card variant="widget"> |       <Card> | ||||||
|         <Box display="flex" mb={2}> |         <Box display="flex" mb={2}> | ||||||
|           <Heading pt={0}>{endpoint.type}</Heading> |           <Heading pt={0}>{endpoint.type}</Heading> | ||||||
|           <Spacer /> |           <Spacer /> | ||||||
| @@ -73,7 +73,7 @@ const SystemTile = ({ endpoint, token }: Props) => { | |||||||
|           <Button |           <Button | ||||||
|             mt={1} |             mt={1} | ||||||
|             minWidth="112px" |             minWidth="112px" | ||||||
|             colorScheme="blue" |             colorScheme="gray" | ||||||
|             rightIcon={<ArrowsClockwise />} |             rightIcon={<ArrowsClockwise />} | ||||||
|             onClick={refresh} |             onClick={refresh} | ||||||
|             isLoading={isFetchingSystem || isFetchingSubsystems} |             isLoading={isFetchingSystem || isFetchingSubsystems} | ||||||
| @@ -179,7 +179,7 @@ const SystemTile = ({ endpoint, token }: Props) => { | |||||||
|                   ml={2} |                   ml={2} | ||||||
|                   onClick={handleReloadClick} |                   onClick={handleReloadClick} | ||||||
|                   icon={<ArrowsClockwise size={20} />} |                   icon={<ArrowsClockwise size={20} />} | ||||||
|                   colorScheme="blue" |                   colorScheme="gray" | ||||||
|                   isLoading={isReloading} |                   isLoading={isReloading} | ||||||
|                   isDisabled={subs.length === 0} |                   isDisabled={subs.length === 0} | ||||||
|                 /> |                 /> | ||||||
|   | |||||||
| @@ -1,47 +1,27 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; | import { Heading, SimpleGrid, Spacer } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| import { RefreshButton } from '../../components/Buttons/RefreshButton'; |  | ||||||
| import { SystemSecretsCard } from './SystemSecrets'; |  | ||||||
| import SystemTile from './SystemTile'; | import SystemTile from './SystemTile'; | ||||||
|  | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| import { CardHeader } from 'components/Containers/Card/CardHeader'; | import { CardHeader } from 'components/Containers/Card/CardHeader'; | ||||||
| import { axiosSec } from 'constants/axiosInstances'; | import { axiosSec } from 'constants/axiosInstances'; | ||||||
| import { useAuth } from 'contexts/AuthProvider'; | import { useAuth } from 'contexts/AuthProvider'; | ||||||
| import { useGetEndpoints } from 'hooks/Network/Endpoints'; | import { useGetEndpoints } from 'hooks/Network/Endpoints'; | ||||||
|  |  | ||||||
| const getDefaultTabIndex = () => { | const SystemPage = () => { | ||||||
|   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 { t } = useTranslation(); | ||||||
|   const { token, user } = useAuth(); |   const { token } = useAuth(); | ||||||
|   const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} }); |   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(() => { |   const endpointsList = React.useMemo(() => { | ||||||
|     if (!token || (!isOnlySec && !endpoints)) return null; |     if (!endpoints || !token) return null; | ||||||
|  |  | ||||||
|     const endpointList = endpoints ? [...endpoints] : []; |     const endpointList = [...endpoints]; | ||||||
|     endpointList.push({ |     endpointList.push({ | ||||||
|       uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '', |       uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '', | ||||||
|       type: isOnlySec ? '' : 'owsec', |       type: 'owsec', | ||||||
|       id: 0, |       id: 0, | ||||||
|       vendor: 'owsec', |       vendor: 'owsec', | ||||||
|       authenticationType: '', |       authenticationType: '', | ||||||
| @@ -57,51 +37,20 @@ const SystemPage = ({ isOnlySec }: Props) => { | |||||||
|   }, [endpoints, token]); |   }, [endpoints, token]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card p={0}> |     <> | ||||||
|       <Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy> |       <Card mb={4} py={2} px={4}> | ||||||
|         <TabList> |  | ||||||
|         <CardHeader> |         <CardHeader> | ||||||
|             <Tab>{t('system.services')}</Tab> |           <Heading size="md" my="auto"> | ||||||
|             <Tab hidden={!isRoot}>{t('system.configuration')}</Tab> |             {t('controller.firmware.endpoints')} | ||||||
|           </CardHeader> |           </Heading> | ||||||
|         </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 /> |           <Spacer /> | ||||||
|           <RefreshButton onClick={refetch} isFetching={isFetching} /> |           <RefreshButton onClick={refetch} isFetching={isFetching} /> | ||||||
|         </CardHeader> |         </CardHeader> | ||||||
|               )} |       </Card> | ||||||
|               <SimpleGrid minChildWidth="500px" spacing="20px" p={4}> |       <SimpleGrid minChildWidth="500px" spacing="20px" mb={3}> | ||||||
|         {endpointsList} |         {endpointsList} | ||||||
|       </SimpleGrid> |       </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; | export default SystemPage; | ||||||
|   | |||||||
| @@ -12,11 +12,9 @@ interface Props { | |||||||
|   isSuspended: boolean; |   isSuspended: boolean; | ||||||
|   isWaitingForCheck: boolean; |   isWaitingForCheck: boolean; | ||||||
|   refresh: () => void; |   refresh: () => void; | ||||||
|   isDisabled?: boolean; |  | ||||||
|   size?: 'sm' | 'md' | 'lg'; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', isDisabled }: Props) => { | const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refresh }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh }); |   const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh }); | ||||||
| @@ -78,16 +76,9 @@ const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', | |||||||
|   return ( |   return ( | ||||||
|     <Menu> |     <Menu> | ||||||
|       <Tooltip label={t('commands.other')}> |       <Tooltip label={t('commands.other')}> | ||||||
|         <MenuButton |         <MenuButton as={IconButton} aria-label="Commands" icon={<Wrench size={20} />} size="sm" ml={2} /> | ||||||
|           as={IconButton} |  | ||||||
|           aria-label="Commands" |  | ||||||
|           icon={<Wrench size={20} />} |  | ||||||
|           size={size} |  | ||||||
|           ml={2} |  | ||||||
|           isDisabled={isDisabled} |  | ||||||
|         /> |  | ||||||
|       </Tooltip> |       </Tooltip> | ||||||
|       <MenuList fontSize="md"> |       <MenuList> | ||||||
|         <MenuItem onClick={handleSuspendClick}> |         <MenuItem onClick={handleSuspendClick}> | ||||||
|           {isSuspended ? t('users.reactivate_user') : t('users.suspend')} |           {isSuspended ? t('users.reactivate_user') : t('users.suspend')} | ||||||
|         </MenuItem> |         </MenuItem> | ||||||
|   | |||||||
| @@ -62,7 +62,7 @@ const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Pro | |||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     setFormKey(uuid()); |     setFormKey(uuid()); | ||||||
|   }, [isOpen, editing]); |   }, [isOpen]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Formik |     <Formik | ||||||
|   | |||||||
| @@ -1,14 +1,12 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { useEffect } from 'react'; | import { useEffect } from 'react'; | ||||||
| import { Spinner, Center, useDisclosure, useBoolean, Tag } from '@chakra-ui/react'; | import { Spinner, Center, useDisclosure, useBoolean } from '@chakra-ui/react'; | ||||||
| import { useQueryClient } from '@tanstack/react-query'; |  | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { EditButton } from '../../../../components/Buttons/EditButton'; | ||||||
| import { SaveButton } from '../../../../components/Buttons/SaveButton'; | import { SaveButton } from '../../../../components/Buttons/SaveButton'; | ||||||
| import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert'; | import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert'; | ||||||
| import { Modal } from '../../../../components/Modals/Modal'; | import { Modal } from '../../../../components/Modals/Modal'; | ||||||
| import ActionsDropdown from '../ActionsDropdown'; |  | ||||||
| import UpdateUserForm from './Form'; | import UpdateUserForm from './Form'; | ||||||
| import { ToggleEditButton } from 'components/Buttons/ToggleEditButton'; |  | ||||||
| import { useGetUser, User } from 'hooks/Network/Users'; | import { useGetUser, User } from 'hooks/Network/Users'; | ||||||
| import { useFormRef } from 'hooks/useFormRef'; | import { useFormRef } from 'hooks/useFormRef'; | ||||||
|  |  | ||||||
| @@ -21,11 +19,10 @@ type Props = { | |||||||
| const EditUserModal = ({ isOpen, onClose, userId }: Props) => { | const EditUserModal = ({ isOpen, onClose, userId }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const [editing, setEditing] = useBoolean(); |   const [editing, setEditing] = useBoolean(); | ||||||
|   const queryClient = useQueryClient(); |  | ||||||
|   const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure(); |   const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure(); | ||||||
|   const { form, formRef } = useFormRef<User>(); |   const { form, formRef } = useFormRef<User>(); | ||||||
|   const canFetchUser = userId !== '' && isOpen; |   const canFetchUser = userId !== '' && isOpen; | ||||||
|   const { data: user, isFetching, refetch } = useGetUser({ id: userId ?? '', enabled: canFetchUser }); |   const { data: user, isFetching } = useGetUser({ id: userId ?? '', enabled: canFetchUser }); | ||||||
|  |  | ||||||
|   const closeModal = () => (form.dirty ? openConfirm() : onClose()); |   const closeModal = () => (form.dirty ? openConfirm() : onClose()); | ||||||
|  |  | ||||||
| @@ -34,11 +31,6 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => { | |||||||
|     onClose(); |     onClose(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const refresh = () => { |  | ||||||
|     refetch(); |  | ||||||
|     queryClient.invalidateQueries(['users']); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (isOpen) setEditing.off(); |     if (isOpen) setEditing.off(); | ||||||
|   }, [isOpen]); |   }, [isOpen]); | ||||||
| @@ -48,40 +40,15 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => { | |||||||
|       <Modal |       <Modal | ||||||
|         isOpen={isOpen} |         isOpen={isOpen} | ||||||
|         onClose={closeModal} |         onClose={closeModal} | ||||||
|         title={user?.name ?? t('crud.edit_obj', { obj: t('user.title') })} |         title={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={ |         topRightButtons={ | ||||||
|           <> |           <> | ||||||
|             <SaveButton |             <SaveButton | ||||||
|               onClick={form.submitForm} |               onClick={form.submitForm} | ||||||
|               isLoading={form.isSubmitting} |               isLoading={form.isSubmitting} | ||||||
|               isDisabled={!editing || !form.isValid || !form.dirty} |               isDisabled={!editing || !form.isValid || !form.dirty} | ||||||
|               hidden={!editing} |  | ||||||
|             /> |             /> | ||||||
|             <ToggleEditButton ml={2} isEditing={editing} toggleEdit={setEditing.toggle} isDirty={form.dirty} /> |             <EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact /> | ||||||
|             {user ? ( |  | ||||||
|               <ActionsDropdown |  | ||||||
|                 id={user?.id} |  | ||||||
|                 isSuspended={user?.suspended} |  | ||||||
|                 isWaitingForCheck={user?.waitingForEmailCheck} |  | ||||||
|                 refresh={refresh} |  | ||||||
|                 size="md" |  | ||||||
|                 isDisabled={editing} |  | ||||||
|               /> |  | ||||||
|             ) : null} |  | ||||||
|           </> |           </> | ||||||
|         } |         } | ||||||
|       > |       > | ||||||
|   | |||||||
| @@ -141,7 +141,7 @@ const UserTable = () => { | |||||||
|           <Box overflowX="auto" w="100%"> |           <Box overflowX="auto" w="100%"> | ||||||
|             <DataTable |             <DataTable | ||||||
|               columns={columns as Column<object>[]} |               columns={columns as Column<object>[]} | ||||||
|               data={users ?? []} |               data={users?.filter((curr) => curr.email !== user?.email) ?? []} | ||||||
|               isLoading={isFetching} |               isLoading={isFetching} | ||||||
|               obj={t('users.title')} |               obj={t('users.title')} | ||||||
|               sortBy={[{ id: 'email', desc: false }]} |               sortBy={[{ id: 'email', desc: false }]} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user