mirror of
				https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
				synced 2025-10-31 02:38:01 +00:00 
			
		
		
		
	Compare commits
	
		
			37 Commits
		
	
	
		
			v2.8.0
			...
			v2.9.0-RC2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 69b0d1ee9d | ||
|   | ef52497b04 | ||
|   | 039e641046 | ||
|   | f1f62efe6f | ||
|   | b3053f32b2 | ||
|   | 09184b0402 | ||
|   | 98562fd967 | ||
|   | 65e9e64cb4 | ||
|   | 573ecbd58d | ||
|   | a801fcca49 | ||
|   | e9d16ee172 | ||
|   | db4dfc93e8 | ||
|   | 975b715a7c | ||
|   | cf17f03ae0 | ||
|   | 64f3ee797e | ||
|   | e287705e88 | ||
|   | 9583b2bae0 | ||
|   | 2698993a6d | ||
|   | a14b595e8c | ||
|   | d7957b85ae | ||
|   | 227a51423d | ||
|   | ea0e7340cc | ||
|   | 999680e94b | ||
|   | 566dbbb157 | ||
|   | 75d995d54e | ||
|   | 908faa491b | ||
|   | 7a254e343e | ||
|   | 016ac336b9 | ||
|   | 1cfd3a10ad | ||
|   | 1838029d22 | ||
|   | 7767043a5a | ||
|   | b1cfa6db19 | ||
|   | 623d5a5546 | ||
|   | 8c676eb965 | ||
|   | 1e4ccce36c | ||
|   | 1808206e74 | ||
|   | 0fbc2b92aa | 
| @@ -8,7 +8,7 @@ fullnameOverride: "" | |||||||
| images: | images: | ||||||
|   owgwui: |   owgwui: | ||||||
|     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui |     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui | ||||||
|     tag: main |     tag: v2.9.0-RC2 | ||||||
|     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.8.0(44)", |   "version": "2.9.0(23)", | ||||||
|   "lockfileVersion": 2, |   "lockfileVersion": 2, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "ucentral-client", |       "name": "ucentral-client", | ||||||
|       "version": "2.8.0(44)", |       "version": "2.9.0(23)", | ||||||
|       "license": "ISC", |       "license": "ISC", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@chakra-ui/icons": "^2.0.11", |         "@chakra-ui/icons": "^2.0.11", | ||||||
| @@ -16,6 +16,8 @@ | |||||||
|         "@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", | ||||||
| @@ -24,6 +26,7 @@ | |||||||
|         "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", | ||||||
| @@ -54,6 +57,7 @@ | |||||||
|         "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", | ||||||
| @@ -2848,6 +2852,30 @@ | |||||||
|       "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, | ||||||
| @@ -3501,6 +3529,12 @@ | |||||||
|       "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, | ||||||
| @@ -5529,7 +5563,6 @@ | |||||||
|     }, |     }, | ||||||
|     "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": { | ||||||
| @@ -5537,6 +5570,11 @@ | |||||||
|       "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, | ||||||
| @@ -6642,8 +6680,9 @@ | |||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/json5": { |     "node_modules/json5": { | ||||||
|       "version": "2.2.1", |       "version": "2.2.3", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", | ||||||
|  |       "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", | ||||||
|       "bin": { |       "bin": { | ||||||
|         "json5": "lib/cli.js" |         "json5": "lib/cli.js" | ||||||
|       }, |       }, | ||||||
| @@ -8853,9 +8892,10 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/tsconfig-paths/node_modules/json5": { |     "node_modules/tsconfig-paths/node_modules/json5": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", | ||||||
|  |       "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "minimist": "^1.2.0" |         "minimist": "^1.2.0" | ||||||
|       }, |       }, | ||||||
| @@ -11425,6 +11465,27 @@ | |||||||
|     "@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, | ||||||
| @@ -11786,6 +11847,12 @@ | |||||||
|       "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 | ||||||
| @@ -13025,13 +13092,17 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "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, | ||||||
| @@ -13688,7 +13759,9 @@ | |||||||
|       "dev": true |       "dev": true | ||||||
|     }, |     }, | ||||||
|     "json5": { |     "json5": { | ||||||
|       "version": "2.2.1" |       "version": "2.2.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", | ||||||
|  |       "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" | ||||||
|     }, |     }, | ||||||
|     "jsonfile": { |     "jsonfile": { | ||||||
|       "version": "6.1.0", |       "version": "6.1.0", | ||||||
| @@ -14981,7 +15054,9 @@ | |||||||
|       }, |       }, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "json5": { |         "json5": { | ||||||
|           "version": "1.0.1", |           "version": "1.0.2", | ||||||
|  |           "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", | ||||||
|  |           "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", | ||||||
|           "dev": true, |           "dev": true, | ||||||
|           "requires": { |           "requires": { | ||||||
|             "minimist": "^1.2.0" |             "minimist": "^1.2.0" | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| { | { | ||||||
|   "name": "ucentral-client", |   "name": "ucentral-client", | ||||||
|   "version": "2.8.0(44)", |   "version": "2.9.0(23)", | ||||||
|   "description": "", |   "description": "", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "main": "index.tsx", |   "main": "index.tsx", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "vite", |     "dev": "vite", | ||||||
|     "build": "vite build", |     "build": "vite build", | ||||||
|     "format": "prettier --write \"src/**/*.js\"", |     "format": "prettier --write \"src/**/*x.{ts,tsx,js,jsx}\"", | ||||||
|     "analyze": "source-map-explorer 'build/static/js/*.js'", |     "analyze": "source-map-explorer 'build/static/js/*.js'", | ||||||
|     "lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix", |     "lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix", | ||||||
|     "clean": "rm -rf node_modules && rm -rf build" |     "clean": "rm -rf node_modules && rm -rf build" | ||||||
| @@ -22,12 +22,15 @@ | |||||||
|     "@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", | ||||||
| @@ -60,6 +63,7 @@ | |||||||
|     "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,8 +79,11 @@ | |||||||
| 		"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": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu", | 		"missing_board": "Analytics-Überwachung an diesem Ort ist nicht mehr aktiv. Klicken Sie hier, um die Überwachung neu zu starten", | ||||||
| 		"mode": "Modus", | 		"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", | ||||||
| @@ -91,6 +94,8 @@ | |||||||
| 		"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", | ||||||
| @@ -175,6 +180,7 @@ | |||||||
| 		"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", | ||||||
| @@ -391,6 +397,7 @@ | |||||||
| 		"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": { | ||||||
| @@ -600,6 +607,7 @@ | |||||||
| 		"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", | ||||||
| @@ -613,6 +621,7 @@ | |||||||
| 		"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts", | 		"import_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", | ||||||
| @@ -621,6 +630,8 @@ | |||||||
| 		"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", | ||||||
| @@ -671,7 +682,15 @@ | |||||||
| 		"test_digicert_creds": "Anmeldeinformationen testen", | 		"test_digicert_creds": "Anmeldeinformationen testen", | ||||||
| 		"title": "Entitäten", | 		"title": "Entitäten", | ||||||
| 		"tree": "Entitätsbaum", | 		"tree": "Entitätsbaum", | ||||||
| 		"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden. Bitte erstellen Sie neue Entitäten und erstellen Sie Veranstaltungsorte unter diesen." | 		"update_success": "Entität aktualisiert!", | ||||||
|  | 		"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden" | ||||||
|  | 	}, | ||||||
|  | 	"firmware": { | ||||||
|  | 		"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern", | ||||||
|  | 		"last_db_update_modal": "Firmware-Datenbank", | ||||||
|  | 		"last_db_update_title": "Datenbank", | ||||||
|  | 		"start_db_update": "Datenbankaktualisierung starten", | ||||||
|  | 		"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern" | ||||||
| 	}, | 	}, | ||||||
| 	"footer": { | 	"footer": { | ||||||
| 		"powered_by": "Unterstützt von", | 		"powered_by": "Unterstützt von", | ||||||
| @@ -765,13 +784,17 @@ | |||||||
| 		"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", | ||||||
| @@ -797,6 +820,7 @@ | |||||||
| 		"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!", | ||||||
| @@ -902,7 +926,7 @@ | |||||||
| 		"dfs": "DFS-Überschreibung", | 		"dfs": "DFS-Überschreibung", | ||||||
| 		"gw_commands": "Gateway-Befehle", | 		"gw_commands": "Gateway-Befehle", | ||||||
| 		"identifier": "Identifikator", | 		"identifier": "Identifikator", | ||||||
| 		"key_verification": "Überprüfung des Signaturschlüssels", | 		"key_verification": "Signieren von Schlüsselinformationen", | ||||||
| 		"restricted": "Beschränkt", | 		"restricted": "Beschränkt", | ||||||
| 		"signed_upgrade": "Nur signiertes Upgrade", | 		"signed_upgrade": "Nur signiertes Upgrade", | ||||||
| 		"title": "Beschränkungen", | 		"title": "Beschränkungen", | ||||||
| @@ -1022,6 +1046,7 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"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", | ||||||
| @@ -1032,6 +1057,10 @@ | |||||||
| 		"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!", | ||||||
| @@ -1053,9 +1082,11 @@ | |||||||
| 		"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": { | ||||||
| @@ -1100,9 +1131,11 @@ | |||||||
| 		"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", | ||||||
| 		"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware", | 		"update_success": "Veranstaltungsort aktualisiert!", | ||||||
|  | 		"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte", | ||||||
| 		"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}", | 		"upgrade_all_devices_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,8 +79,11 @@ | |||||||
| 		"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, please restart monitoring using the top menu", | 		"missing_board": "Analytics monitoring on this venue is no longer active. Click here to restart monitoring", | ||||||
| 		"mode": "Mode", | 		"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", | ||||||
| @@ -91,6 +94,8 @@ | |||||||
| 		"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", | ||||||
| @@ -175,6 +180,7 @@ | |||||||
| 		"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", | ||||||
| @@ -391,6 +397,7 @@ | |||||||
| 		"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": { | ||||||
| @@ -600,6 +607,7 @@ | |||||||
| 		"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", | ||||||
| @@ -613,6 +621,7 @@ | |||||||
| 		"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired", | 		"import_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", | ||||||
| @@ -621,6 +630,8 @@ | |||||||
| 		"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", | ||||||
| @@ -671,7 +682,15 @@ | |||||||
| 		"test_digicert_creds": "Test Credentials", | 		"test_digicert_creds": "Test Credentials", | ||||||
| 		"title": "Entities", | 		"title": "Entities", | ||||||
| 		"tree": "Entity Tree", | 		"tree": "Entity Tree", | ||||||
| 		"venues_under_root": "Venues cannot be created directly under the root entity. Please create new entities and create venues under these." | 		"update_success": "Entity updated!", | ||||||
|  | 		"venues_under_root": "Venues cannot be created directly under the root entity" | ||||||
|  | 	}, | ||||||
|  | 	"firmware": { | ||||||
|  | 		"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes", | ||||||
|  | 		"last_db_update_modal": "Firmware Database", | ||||||
|  | 		"last_db_update_title": "Database", | ||||||
|  | 		"start_db_update": "Start Database Update", | ||||||
|  | 		"started_db_update": "Started database update, this operation should take up to 25 minutes to complete" | ||||||
| 	}, | 	}, | ||||||
| 	"footer": { | 	"footer": { | ||||||
| 		"powered_by": "Powered By", | 		"powered_by": "Powered By", | ||||||
| @@ -765,13 +784,17 @@ | |||||||
| 		"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", | ||||||
| @@ -797,6 +820,7 @@ | |||||||
| 		"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!", | ||||||
| @@ -902,7 +926,7 @@ | |||||||
| 		"dfs": "DFS Override", | 		"dfs": "DFS Override", | ||||||
| 		"gw_commands": "Gateway Commands", | 		"gw_commands": "Gateway Commands", | ||||||
| 		"identifier": "Identifier", | 		"identifier": "Identifier", | ||||||
| 		"key_verification": "Signing Key Verification", | 		"key_verification": "Signing Key Information", | ||||||
| 		"restricted": "Restricted", | 		"restricted": "Restricted", | ||||||
| 		"signed_upgrade": "Signed Upgrade Only", | 		"signed_upgrade": "Signed Upgrade Only", | ||||||
| 		"title": "Restrictions", | 		"title": "Restrictions", | ||||||
| @@ -1022,6 +1046,7 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"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", | ||||||
| @@ -1032,6 +1057,10 @@ | |||||||
| 		"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!", | ||||||
| @@ -1053,9 +1082,11 @@ | |||||||
| 		"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": { | ||||||
| @@ -1100,9 +1131,11 @@ | |||||||
| 		"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", | ||||||
| 		"upgrade_all_devices": "Upgrade All Devices to Latest Firmware", | 		"update_success": "Venue updated!", | ||||||
|  | 		"upgrade_all_devices": "Upgrade All Devices Firmware", | ||||||
| 		"upgrade_all_devices_error": "Error upgrading devices: {{e}}", | 		"upgrade_all_devices_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,8 +79,11 @@ | |||||||
| 		"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, reinicie el monitoreo usando el menú superior", | 		"missing_board": "El monitoreo analítico en este lugar ya no está activo. Haga clic aquí para reiniciar el monitoreo", | ||||||
| 		"mode": "Modo", | 		"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", | ||||||
| @@ -91,6 +94,8 @@ | |||||||
| 		"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", | ||||||
| @@ -175,6 +180,7 @@ | |||||||
| 		"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", | ||||||
| @@ -391,6 +397,7 @@ | |||||||
| 		"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": { | ||||||
| @@ -600,6 +607,7 @@ | |||||||
| 		"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", | ||||||
| @@ -613,6 +621,7 @@ | |||||||
| 		"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado", | 		"import_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", | ||||||
| @@ -621,6 +630,8 @@ | |||||||
| 		"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", | ||||||
| @@ -671,7 +682,15 @@ | |||||||
| 		"test_digicert_creds": "Credenciales de prueba", | 		"test_digicert_creds": "Credenciales de prueba", | ||||||
| 		"title": "entidades", | 		"title": "entidades", | ||||||
| 		"tree": "Árbol de entidades", | 		"tree": "Árbol de entidades", | ||||||
| 		"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz. Cree nuevas entidades y cree lugares bajo estas." | 		"update_success": "¡Entidad actualizada!", | ||||||
|  | 		"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz" | ||||||
|  | 	}, | ||||||
|  | 	"firmware": { | ||||||
|  | 		"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos", | ||||||
|  | 		"last_db_update_modal": "Base de datos de firmware", | ||||||
|  | 		"last_db_update_title": "Base de datos", | ||||||
|  | 		"start_db_update": "Iniciar actualización de la base de datos", | ||||||
|  | 		"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse" | ||||||
| 	}, | 	}, | ||||||
| 	"footer": { | 	"footer": { | ||||||
| 		"powered_by": "energizado por", | 		"powered_by": "energizado por", | ||||||
| @@ -765,13 +784,17 @@ | |||||||
| 		"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", | ||||||
| @@ -797,6 +820,7 @@ | |||||||
| 		"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!", | ||||||
| @@ -902,7 +926,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": "Verificación de clave de firma", | 		"key_verification": "Información 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", | ||||||
| @@ -1022,6 +1046,7 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"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", | ||||||
| @@ -1032,6 +1057,10 @@ | |||||||
| 		"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!", | ||||||
| @@ -1053,9 +1082,11 @@ | |||||||
| 		"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": { | ||||||
| @@ -1100,9 +1131,11 @@ | |||||||
| 		"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", | ||||||
| 		"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente", | 		"update_success": "Lugar actualizado!", | ||||||
|  | 		"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos", | ||||||
| 		"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}", | 		"upgrade_all_devices_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,8 +79,11 @@ | |||||||
| 		"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 lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut", | 		"missing_board": "La surveillance analytique sur ce site n'est plus active. Cliquez ici pour redémarrer la surveillance", | ||||||
| 		"mode": "Mode", | 		"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", | ||||||
| @@ -91,6 +94,8 @@ | |||||||
| 		"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", | ||||||
| @@ -175,6 +180,7 @@ | |||||||
| 		"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", | ||||||
| @@ -391,6 +397,7 @@ | |||||||
| 		"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": { | ||||||
| @@ -600,6 +607,7 @@ | |||||||
| 		"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", | ||||||
| @@ -613,6 +621,7 @@ | |||||||
| 		"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée", | 		"import_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é", | ||||||
| @@ -621,6 +630,8 @@ | |||||||
| 		"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", | ||||||
| @@ -671,7 +682,15 @@ | |||||||
| 		"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", | ||||||
| 		"venues_under_root": "Les sites ne peuvent pas être créés directement sous l'entité racine. Veuillez créer de nouvelles entités et créer des lieux sous celles-ci." | 		"update_success": "Entité mise à jour !", | ||||||
|  | 		"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine" | ||||||
|  | 	}, | ||||||
|  | 	"firmware": { | ||||||
|  | 		"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes", | ||||||
|  | 		"last_db_update_modal": "Base de données du micrologiciel", | ||||||
|  | 		"last_db_update_title": "Base de données", | ||||||
|  | 		"start_db_update": "Démarrer la mise à jour de la base de données", | ||||||
|  | 		"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes" | ||||||
| 	}, | 	}, | ||||||
| 	"footer": { | 	"footer": { | ||||||
| 		"powered_by": "Alimenté par", | 		"powered_by": "Alimenté par", | ||||||
| @@ -765,13 +784,17 @@ | |||||||
| 		"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", | ||||||
| @@ -797,6 +820,7 @@ | |||||||
| 		"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!", | ||||||
| @@ -902,7 +926,7 @@ | |||||||
| 		"dfs": "Remplacement DFS", | 		"dfs": "Remplacement DFS", | ||||||
| 		"gw_commands": "Commandes de passerelle", | 		"gw_commands": "Commandes de passerelle", | ||||||
| 		"identifier": "Identifiant", | 		"identifier": "Identifiant", | ||||||
| 		"key_verification": "Vérification de la clé de signature", | 		"key_verification": "Signature des informations clés", | ||||||
| 		"restricted": "Limité", | 		"restricted": "Limité", | ||||||
| 		"signed_upgrade": "Mise à niveau signée uniquement", | 		"signed_upgrade": "Mise à niveau signée uniquement", | ||||||
| 		"title": "Restrictions", | 		"title": "Restrictions", | ||||||
| @@ -1022,6 +1046,7 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"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", | ||||||
| @@ -1032,6 +1057,10 @@ | |||||||
| 		"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 !", | ||||||
| @@ -1053,9 +1082,11 @@ | |||||||
| 		"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": { | ||||||
| @@ -1100,9 +1131,11 @@ | |||||||
| 		"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", | ||||||
| 		"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel", | 		"update_success": "Lieu mis à jour !", | ||||||
|  | 		"upgrade_all_devices": "Mettre à niveau le micrologiciel de tous les appareils", | ||||||
| 		"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}", | 		"upgrade_all_devices_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,8 +79,11 @@ | |||||||
| 		"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, reinicie o monitoramento usando o menu superior", | 		"missing_board": "O monitoramento analítico neste local não está mais ativo. Clique aqui para reiniciar o monitoramento", | ||||||
| 		"mode": "Modo", | 		"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", | ||||||
| @@ -91,6 +94,8 @@ | |||||||
| 		"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", | ||||||
| @@ -175,6 +180,7 @@ | |||||||
| 		"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", | ||||||
| @@ -391,6 +397,7 @@ | |||||||
| 		"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": { | ||||||
| @@ -600,6 +607,7 @@ | |||||||
| 		"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", | ||||||
| @@ -613,6 +621,7 @@ | |||||||
| 		"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado", | 		"import_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", | ||||||
| @@ -621,6 +630,8 @@ | |||||||
| 		"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", | ||||||
| @@ -671,7 +682,15 @@ | |||||||
| 		"test_digicert_creds": "Credenciais de teste", | 		"test_digicert_creds": "Credenciais de teste", | ||||||
| 		"title": "Entidades", | 		"title": "Entidades", | ||||||
| 		"tree": "Árvore de entidades", | 		"tree": "Árvore de entidades", | ||||||
| 		"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz. Por favor, crie novas entidades e crie locais sob elas." | 		"update_success": "Entidade atualizada!", | ||||||
|  | 		"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz" | ||||||
|  | 	}, | ||||||
|  | 	"firmware": { | ||||||
|  | 		"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos", | ||||||
|  | 		"last_db_update_modal": "banco de dados de firmware", | ||||||
|  | 		"last_db_update_title": "base de dados", | ||||||
|  | 		"start_db_update": "Iniciar atualização do banco de dados", | ||||||
|  | 		"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída" | ||||||
| 	}, | 	}, | ||||||
| 	"footer": { | 	"footer": { | ||||||
| 		"powered_by": "Distribuído por", | 		"powered_by": "Distribuído por", | ||||||
| @@ -765,13 +784,17 @@ | |||||||
| 		"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", | ||||||
| @@ -797,6 +820,7 @@ | |||||||
| 		"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!", | ||||||
| @@ -902,7 +926,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": "Verificação da chave de assinatura", | 		"key_verification": "Informações 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", | ||||||
| @@ -1022,6 +1046,7 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"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", | ||||||
| @@ -1032,6 +1057,10 @@ | |||||||
| 		"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!", | ||||||
| @@ -1053,9 +1082,11 @@ | |||||||
| 		"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": { | ||||||
| @@ -1100,9 +1131,11 @@ | |||||||
| 		"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", | ||||||
| 		"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente", | 		"update_success": "Local atualizado!", | ||||||
|  | 		"upgrade_all_devices": "Atualize o firmware de todos os dispositivos", | ||||||
| 		"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}", | 		"upgrade_all_devices_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" | ||||||
|   | |||||||
| @@ -12,7 +12,14 @@ export interface AlertButtonProps extends ThemeProps { | |||||||
|   label?: string; |   label?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| const _AlertButton: React.FC<AlertButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => { | const _AlertButton: React.FC<AlertButtonProps> = ({ | ||||||
|  |   onClick, | ||||||
|  |   isDisabled, | ||||||
|  |   isLoading, | ||||||
|  |   isCompact = true, | ||||||
|  |   label, | ||||||
|  |   ...props | ||||||
|  | }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,14 @@ export interface CreateButtonProps extends SpaceProps { | |||||||
|   label?: string; |   label?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| const _CreateButton: React.FC<CreateButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => { | const _CreateButton: React.FC<CreateButtonProps> = ({ | ||||||
|  |   onClick, | ||||||
|  |   isDisabled, | ||||||
|  |   isLoading, | ||||||
|  |   isCompact = true, | ||||||
|  |   label, | ||||||
|  |   ...props | ||||||
|  | }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ const _DeleteButton: React.FC<DeleteButtonProps> = ({ | |||||||
|   onClick, |   onClick, | ||||||
|   isDisabled, |   isDisabled, | ||||||
|   isLoading, |   isLoading, | ||||||
|   isCompact, |   isCompact = true, | ||||||
|   label, |   label, | ||||||
|   ml, |   ml, | ||||||
|   ...props |   ...props | ||||||
|   | |||||||
| @@ -1,9 +1,19 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip, useToast } from '@chakra-ui/react'; | import { | ||||||
|  |   Button, | ||||||
|  |   IconButton, | ||||||
|  |   Menu, | ||||||
|  |   MenuButton, | ||||||
|  |   MenuItem, | ||||||
|  |   MenuList, | ||||||
|  |   Portal, | ||||||
|  |   Spinner, | ||||||
|  |   Tooltip, | ||||||
|  |   useToast, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import axios from 'axios'; | import 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'; | ||||||
| @@ -22,6 +32,7 @@ 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; | ||||||
| } | } | ||||||
| @@ -38,8 +49,9 @@ const DeviceActionDropdown = ({ | |||||||
|   onOpenTelemetryModal, |   onOpenTelemetryModal, | ||||||
|   onOpenConfigureModal, |   onOpenConfigureModal, | ||||||
|   onOpenScriptModal, |   onOpenScriptModal, | ||||||
|  |   onOpenRebootModal, | ||||||
|   size, |   size, | ||||||
|   isCompact, |   isCompact = true, | ||||||
| }: Props) => { | }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
| @@ -145,11 +157,13 @@ const DeviceActionDropdown = ({ | |||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleConnectClick = () => getRtty(); |   const handleConnectClick = () => getRtty(); | ||||||
|  |   const handleRebootClick = () => onOpenRebootModal(device.serialNumber); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Menu> |     <Menu> | ||||||
|       <Tooltip label={t('commands.other')}> |       <Tooltip label={t('common.actions')}> | ||||||
|         {size === undefined || isCompact ? ( |         {size === undefined || isCompact ? ( | ||||||
|           <MenuButton |           <MenuButton | ||||||
|             as={IconButton} |             as={IconButton} | ||||||
| @@ -168,10 +182,11 @@ const DeviceActionDropdown = ({ | |||||||
|             isDisabled={isDisabled} |             isDisabled={isDisabled} | ||||||
|             ml={2} |             ml={2} | ||||||
|           > |           > | ||||||
|             {t('commands.other')} |             {t('common.actions')} | ||||||
|           </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> | ||||||
| @@ -179,7 +194,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> | ||||||
|         <RebootMenuItem device={device} refresh={refresh} /> |           <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem> | ||||||
|           <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> | ||||||
| @@ -188,6 +203,7 @@ const DeviceActionDropdown = ({ | |||||||
|           </MenuItem> |           </MenuItem> | ||||||
|           <MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem> |           <MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem> | ||||||
|         </MenuList> |         </MenuList> | ||||||
|  |       </Portal> | ||||||
|     </Menu> |     </Menu> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react'; | import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react'; | ||||||
| import { Pen } from 'phosphor-react'; | import { Pen } from 'phosphor-react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  |  | ||||||
| export interface EditButtonProps { | export interface EditButtonProps { | ||||||
|   onClick: () => void; |   onClick: () => void; | ||||||
| @@ -11,7 +12,15 @@ export interface EditButtonProps { | |||||||
|   ml?: string | number; |   ml?: string | number; | ||||||
| } | } | ||||||
|  |  | ||||||
| const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, isLoading, isCompact, ...props }) => { | const _EditButton: React.FC<EditButtonProps> = ({ | ||||||
|  |   onClick, | ||||||
|  |   label, | ||||||
|  |   isDisabled, | ||||||
|  |   isLoading, | ||||||
|  |   isCompact = true, | ||||||
|  |   ...props | ||||||
|  | }) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|  |  | ||||||
|   if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') { |   if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') { | ||||||
| @@ -24,12 +33,12 @@ const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, is | |||||||
|         isDisabled={isDisabled} |         isDisabled={isDisabled} | ||||||
|         {...props} |         {...props} | ||||||
|       > |       > | ||||||
|         {label} |         {label ?? t('common.edit')} | ||||||
|       </Button> |       </Button> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   return ( |   return ( | ||||||
|     <Tooltip label={label}> |     <Tooltip label={label ?? t('common.edit')} hasArrow> | ||||||
|       <IconButton |       <IconButton | ||||||
|         aria-label="edit" |         aria-label="edit" | ||||||
|         colorScheme="gray" |         colorScheme="gray" | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ const _RefreshButton: React.FC<RefreshButtonProps> = ({ | |||||||
|   onClick, |   onClick, | ||||||
|   isDisabled, |   isDisabled, | ||||||
|   isFetching, |   isFetching, | ||||||
|   isCompact, |   isCompact = true, | ||||||
|   ml, |   ml, | ||||||
|   size, |   size, | ||||||
|   ...props |   ...props | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ const _ResponsiveButton: React.FC<ResponsiveButtonProps> = ({ | |||||||
|   onClick, |   onClick, | ||||||
|   isDisabled, |   isDisabled, | ||||||
|   isLoading, |   isLoading, | ||||||
|   isCompact, |   isCompact = true, | ||||||
|   color, |   color, | ||||||
|   label, |   label, | ||||||
|   icon, |   icon, | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ const _SaveButton: React.FC<SaveButtonProps> = ({ | |||||||
|   onClick, |   onClick, | ||||||
|   isDisabled, |   isDisabled, | ||||||
|   isLoading, |   isLoading, | ||||||
|   isCompact, |   isCompact = true, | ||||||
|   isDirty, |   isDirty, | ||||||
|   dirtyCheck, |   dirtyCheck, | ||||||
|   ...props |   ...props | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({ | |||||||
|   isDirty, |   isDirty, | ||||||
|   isDisabled, |   isDisabled, | ||||||
|   isLoading, |   isLoading, | ||||||
|   isCompact, |   isCompact = true, | ||||||
|   ml, |   ml, | ||||||
|   ...props |   ...props | ||||||
| }) => { | }) => { | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ const _WarningButton: React.FC<WarningButtonProps> = ({ | |||||||
|   onClick, |   onClick, | ||||||
|   isDisabled, |   isDisabled, | ||||||
|   isLoading, |   isLoading, | ||||||
|   isCompact, |   isCompact = true, | ||||||
|   label, |   label, | ||||||
|   ...props |   ...props | ||||||
| }) => { | }) => { | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ export const ColumnPicker = ({ | |||||||
|   hiddenColumns, |   hiddenColumns, | ||||||
|   setHiddenColumns, |   setHiddenColumns, | ||||||
|   size, |   size, | ||||||
|   isCompact, |   isCompact = true, | ||||||
| }: ColumnPickerProps) => { | }: ColumnPickerProps) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { getPref, setPref } = useAuth(); |   const { getPref, setPref } = useAuth(); | ||||||
|   | |||||||
| @@ -44,15 +44,18 @@ const defaultProps = { | |||||||
|   sortBy: [], |   sortBy: [], | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type DataTableProps = { | export type DataTableProps<TValue> = { | ||||||
|   columns: readonly Column<object>[]; |   columns: Column<TValue>[]; | ||||||
|   data: object[]; |   data: TValue[]; | ||||||
|   count?: number; |   count?: number; | ||||||
|   setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>; |   setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>; | ||||||
|   isLoading?: boolean; |   isLoading?: boolean; | ||||||
|  |   onRowClick?: (row: TValue) => void; | ||||||
|  |   isRowClickable?: (row: TValue) => boolean; | ||||||
|   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; | ||||||
| @@ -67,7 +70,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> & | |||||||
|     state: UsePaginationState<T>; |     state: UsePaginationState<T>; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| const _DataTable = ({ | const _DataTable = <TValue extends object>({ | ||||||
|   columns, |   columns, | ||||||
|   data, |   data, | ||||||
|   isLoading, |   isLoading, | ||||||
| @@ -77,15 +80,19 @@ const _DataTable = ({ | |||||||
|   sortBy, |   sortBy, | ||||||
|   hiddenColumns, |   hiddenColumns, | ||||||
|   hideControls, |   hideControls, | ||||||
|  |   hideEmptyListText, | ||||||
|   count, |   count, | ||||||
|   setPageInfo, |   setPageInfo, | ||||||
|   isManual, |   isManual, | ||||||
|   saveSettingsId, |   saveSettingsId, | ||||||
|   showAllRows, |   showAllRows, | ||||||
| }: DataTableProps) => { |   onRowClick, | ||||||
|  |   isRowClickable, | ||||||
|  | }: DataTableProps<TValue>) => { | ||||||
|   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; | ||||||
| @@ -140,8 +147,12 @@ const _DataTable = ({ | |||||||
|     }, |     }, | ||||||
|     useSortBy, |     useSortBy, | ||||||
|     usePagination, |     usePagination, | ||||||
|   ) as TableInstanceWithHooks<object>; |   ) as TableInstanceWithHooks<TValue>; | ||||||
|  |  | ||||||
|  |   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)); | ||||||
| @@ -253,10 +264,19 @@ const _DataTable = ({ | |||||||
|             </Thead> |             </Thead> | ||||||
|             {data.length > 0 && ( |             {data.length > 0 && ( | ||||||
|               <Tbody {...getTableBodyProps()}> |               <Tbody {...getTableBodyProps()}> | ||||||
|                 {page.map((row: Row) => { |                 {page.map((row: Row<TValue>) => { | ||||||
|                   prepareRow(row); |                   prepareRow(row); | ||||||
|  |                   const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true; | ||||||
|  |                   const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined; | ||||||
|                   return ( |                   return ( | ||||||
|                     <Tr {...row.getRowProps()} key={uuid()}> |                     <Tr | ||||||
|  |                       {...row.getRowProps()} | ||||||
|  |                       key={uuid()} | ||||||
|  |                       _hover={{ | ||||||
|  |                         backgroundColor: hoveredRowBg, | ||||||
|  |                       }} | ||||||
|  |                       onClick={onClick} | ||||||
|  |                     > | ||||||
|                       { |                       { | ||||||
|                         // @ts-ignore |                         // @ts-ignore | ||||||
|                         row.cells.map((cell) => ( |                         row.cells.map((cell) => ( | ||||||
| @@ -275,8 +295,26 @@ const _DataTable = ({ | |||||||
|                             fontSize="14px" |                             fontSize="14px" | ||||||
|                             // @ts-ignore |                             // @ts-ignore | ||||||
|                             textAlign={cell.column.isCentered ? 'center' : undefined} |                             textAlign={cell.column.isCentered ? 'center' : undefined} | ||||||
|  |                             fontFamily={ | ||||||
|                               // @ts-ignore |                               // @ts-ignore | ||||||
|                             fontFamily={cell.column.isMonospace ? 'monospace' : undefined} |                               cell.column.isMonospace | ||||||
|  |                                 ? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace' | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|  |                             onClick={ | ||||||
|  |                               // @ts-ignore | ||||||
|  |                               cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick) | ||||||
|  |                                 ? (e) => { | ||||||
|  |                                     e.stopPropagation(); | ||||||
|  |                                   } | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|  |                             cursor={ | ||||||
|  |                               // @ts-ignore | ||||||
|  |                               !cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick | ||||||
|  |                                 ? 'pointer' | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|                           > |                           > | ||||||
|                             {cell.render('Cell')} |                             {cell.render('Cell')} | ||||||
|                           </Td> |                           </Td> | ||||||
| @@ -288,7 +326,7 @@ const _DataTable = ({ | |||||||
|               </Tbody> |               </Tbody> | ||||||
|             )} |             )} | ||||||
|           </Table> |           </Table> | ||||||
|           {!isLoading && data.length === 0 && ( |           {!isLoading && data.length === 0 && !hideEmptyListText && ( | ||||||
|             <Center> |             <Center> | ||||||
|               {obj ? ( |               {obj ? ( | ||||||
|                 <Heading size="md" pt={12}> |                 <Heading size="md" pt={12}> | ||||||
| @@ -309,7 +347,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={() => gotoPage(0)} |                 onClick={() => handleGoToPage(0)} | ||||||
|                 isDisabled={!canPreviousPage} |                 isDisabled={!canPreviousPage} | ||||||
|                 icon={<ArrowLeftIcon h={3} w={3} />} |                 icon={<ArrowLeftIcon h={3} w={3} />} | ||||||
|                 mr={4} |                 mr={4} | ||||||
| @@ -347,7 +385,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; | ||||||
|                     gotoPage(newPage); |                     handleGoToPage(newPage); | ||||||
|                   }} |                   }} | ||||||
|                   defaultValue={pageIndex + 1} |                   defaultValue={pageIndex + 1} | ||||||
|                 > |                 > | ||||||
| @@ -386,7 +424,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={() => gotoPage(pageCount - 1)} |                 onClick={() => handleGoToPage(pageCount - 1)} | ||||||
|                 isDisabled={!canNextPage} |                 isDisabled={!canNextPage} | ||||||
|                 icon={<ArrowRightIcon h={3} w={3} />} |                 icon={<ArrowRightIcon h={3} w={3} />} | ||||||
|                 ml={4} |                 ml={4} | ||||||
| @@ -401,4 +439,4 @@ const _DataTable = ({ | |||||||
|  |  | ||||||
| _DataTable.defaultProps = defaultProps; | _DataTable.defaultProps = defaultProps; | ||||||
|  |  | ||||||
| export const DataTable = React.memo(_DataTable); | export const DataTable = React.memo(_DataTable) as unknown as typeof _DataTable; | ||||||
|   | |||||||
| @@ -100,6 +100,7 @@ const SortableDataTable: React.FC<Props> = ({ | |||||||
| }) => { | }) => { | ||||||
|   const { t } = useTranslation(); |   const { 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; | ||||||
| @@ -223,7 +224,13 @@ const SortableDataTable: React.FC<Props> = ({ | |||||||
|                 {page.map((row: Row) => { |                 {page.map((row: Row) => { | ||||||
|                   prepareRow(row); |                   prepareRow(row); | ||||||
|                   return ( |                   return ( | ||||||
|                     <Tr {...row.getRowProps()} key={uuid()}> |                     <Tr | ||||||
|  |                       {...row.getRowProps()} | ||||||
|  |                       key={uuid()} | ||||||
|  |                       _hover={{ | ||||||
|  |                         backgroundColor: hoveredRowBg, | ||||||
|  |                       }} | ||||||
|  |                     > | ||||||
|                       { |                       { | ||||||
|                         // @ts-ignore |                         // @ts-ignore | ||||||
|                         row.cells.map((cell) => ( |                         row.cells.map((cell) => ( | ||||||
| @@ -242,8 +249,12 @@ const SortableDataTable: React.FC<Props> = ({ | |||||||
|                             fontSize="14px" |                             fontSize="14px" | ||||||
|                             // @ts-ignore |                             // @ts-ignore | ||||||
|                             textAlign={cell.column.isCentered ? 'center' : undefined} |                             textAlign={cell.column.isCentered ? 'center' : undefined} | ||||||
|  |                             fontFamily={ | ||||||
|                               // @ts-ignore |                               // @ts-ignore | ||||||
|                             fontFamily={cell.column.isMonospace ? 'monospace' : undefined} |                               cell.column.isMonospace | ||||||
|  |                                 ? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace' | ||||||
|  |                                 : undefined | ||||||
|  |                             } | ||||||
|                           > |                           > | ||||||
|                             {cell.render('Cell')} |                             {cell.render('Cell')} | ||||||
|                           </Td> |                           </Td> | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								src/components/Maps/GoogleMap/Marker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/components/Maps/GoogleMap/Marker.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  |  | ||||||
|  | const _GoogleMapMarker = (options: google.maps.MarkerOptions) => { | ||||||
|  |   const [marker, setMarker] = React.useState<google.maps.Marker>(); | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (!marker) { | ||||||
|  |       setMarker(new google.maps.Marker()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return () => { | ||||||
|  |       if (marker) { | ||||||
|  |         marker.setMap(null); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }, [marker]); | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (marker) { | ||||||
|  |       marker.setOptions(options); | ||||||
|  |     } | ||||||
|  |   }, [marker, options]); | ||||||
|  |  | ||||||
|  |   return null; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const GoogleMapMarker = React.memo(_GoogleMapMarker); | ||||||
							
								
								
									
										89
									
								
								src/components/Maps/GoogleMap/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/components/Maps/GoogleMap/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { isLatLngLiteral } from '@googlemaps/typescript-guards'; | ||||||
|  | import { createCustomEqual } from 'fast-equals'; | ||||||
|  |  | ||||||
|  | const deepCompareEqualsForMaps = createCustomEqual((deepEqual) => | ||||||
|  |   // @ts-ignore | ||||||
|  |   (a: number | google.maps.LatLng | google.maps.LatLngLiteral, b: number | google.maps.LatLng | google.maps.LatLngLiteral) => { | ||||||
|  |     if ( | ||||||
|  |       isLatLngLiteral(a) || | ||||||
|  |       a instanceof google.maps.LatLng || | ||||||
|  |       isLatLngLiteral(b) || | ||||||
|  |       b instanceof google.maps.LatLng | ||||||
|  |     ) { | ||||||
|  |       return new google.maps.LatLng(a).equals(new google.maps.LatLng(b)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // @ts-ignore | ||||||
|  |     return deepEqual(a, b); | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const useDeepCompareMemoize = (value: unknown) => { | ||||||
|  |   const ref = React.useRef<unknown>(); | ||||||
|  |  | ||||||
|  |   if (!deepCompareEqualsForMaps(value, ref.current)) { | ||||||
|  |     ref.current = value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ref.current; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const useDeepCompareEffectForMaps = (callback: React.EffectCallback, dependencies: unknown[]) => { | ||||||
|  |   React.useEffect(callback, dependencies.map(useDeepCompareMemoize)); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export interface GoogleMapProps extends google.maps.MapOptions { | ||||||
|  |   style: { [key: string]: string }; | ||||||
|  |   onClick?: (e: google.maps.MapMouseEvent) => void; | ||||||
|  |   onIdle?: (map: google.maps.Map) => void; | ||||||
|  |   children?: React.ReactNode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const _GoogleMap = ({ style, onClick, onIdle, children, ...options }: GoogleMapProps) => { | ||||||
|  |   const ref = React.useRef<HTMLDivElement>(null); | ||||||
|  |   const [map, setMap] = React.useState<google.maps.Map>(); | ||||||
|  |  | ||||||
|  |   // because React does not do deep comparisons, a custom hook is used | ||||||
|  |   useDeepCompareEffectForMaps(() => { | ||||||
|  |     if (map) { | ||||||
|  |       map.setOptions(options); | ||||||
|  |     } | ||||||
|  |   }, [map, options]); | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (ref.current && !map) { | ||||||
|  |       setMap(new window.google.maps.Map(ref.current, {})); | ||||||
|  |     } | ||||||
|  |   }, [ref, map]); | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (map) { | ||||||
|  |       ['click', 'idle'].forEach((eventName) => google.maps.event.clearListeners(map, eventName)); | ||||||
|  |  | ||||||
|  |       if (onClick) { | ||||||
|  |         map.addListener('click', onClick); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (onIdle) { | ||||||
|  |         map.addListener('idle', () => onIdle(map)); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, [map, onClick, onIdle]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <div ref={ref} style={style} /> | ||||||
|  |       {React.Children.map(children, (child) => { | ||||||
|  |         if (React.isValidElement(child)) { | ||||||
|  |           // set the map prop on the child component | ||||||
|  |           // @ts-ignore | ||||||
|  |           return React.cloneElement(child, { map }); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |       })} | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const GoogleMap = React.memo(_GoogleMap); | ||||||
| @@ -5,17 +5,22 @@ import { | |||||||
|   AlertIcon, |   AlertIcon, | ||||||
|   AlertTitle, |   AlertTitle, | ||||||
|   Box, |   Box, | ||||||
|  |   Button, | ||||||
|  |   Flex, | ||||||
|   FormControl, |   FormControl, | ||||||
|   FormErrorMessage, |   FormErrorMessage, | ||||||
|   FormLabel, |   FormLabel, | ||||||
|   Textarea, |   Textarea, | ||||||
|   useToast, |   useToast, | ||||||
| } from '@chakra-ui/react'; | } from '@chakra-ui/react'; | ||||||
|  | import { ClipboardText } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { SaveButton } from '../../Buttons/SaveButton'; | import { SaveButton } from '../../Buttons/SaveButton'; | ||||||
| import { Modal } from '../Modal'; | import { Modal } from '../Modal'; | ||||||
| import { FileInputButton } from 'components/Buttons/FileInputButton'; | import { FileInputButton } from 'components/Buttons/FileInputButton'; | ||||||
| import { useConfigureDevice } from 'hooks/Network/Commands'; | import { useConfigureDevice } from 'hooks/Network/Commands'; | ||||||
|  | import { useGetDevice } from 'hooks/Network/Devices'; | ||||||
|  | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
| export type ConfigureModalProps = { | export type ConfigureModalProps = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| @@ -29,11 +34,17 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const configure = useConfigureDevice({ serialNumber }); |   const configure = useConfigureDevice({ serialNumber }); | ||||||
|  |   const getDevice = useGetDevice({ serialNumber }); | ||||||
|  |  | ||||||
|   const [newConfig, setNewConfig] = React.useState(''); |   const [newConfig, setNewConfig] = React.useState(''); | ||||||
|  |  | ||||||
|   const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => { |   const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||
|     setNewConfig(e.target.value); |     setNewConfig(e.target.value); | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|  |   const onImportConfiguration = () => { | ||||||
|  |     setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : ''); | ||||||
|  |   }; | ||||||
|   const isValid = React.useMemo(() => { |   const isValid = React.useMemo(() => { | ||||||
|     try { |     try { | ||||||
|       JSON.parse(newConfig); |       JSON.parse(newConfig); | ||||||
| @@ -60,9 +71,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|           modalProps.onClose(); |           modalProps.onClose(); | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     } catch (e) { |     } catch (e) {} | ||||||
|       // console.log(e); |  | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -79,10 +88,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|             <AlertIcon /> |             <AlertIcon /> | ||||||
|             <Box> |             <Box> | ||||||
|               <AlertTitle>{t('common.error')}</AlertTitle> |               <AlertTitle>{t('common.error')}</AlertTitle> | ||||||
|               { |               <AlertDescription>{(configure.error as AxiosError)?.response?.data?.ErrorDescription}</AlertDescription> | ||||||
|                 // @ts-ignore |  | ||||||
|                 <AlertDescription>{configure.error?.response?.data?.ErrorDescription}</AlertDescription> |  | ||||||
|               } |  | ||||||
|             </Box> |             </Box> | ||||||
|           </Alert> |           </Alert> | ||||||
|         )} |         )} | ||||||
| @@ -92,7 +98,8 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|         </Alert> |         </Alert> | ||||||
|         <FormControl isInvalid={!isValid && newConfig.length > 0}> |         <FormControl isInvalid={!isValid && newConfig.length > 0}> | ||||||
|           <FormLabel>{t('configurations.one')}</FormLabel> |           <FormLabel>{t('configurations.one')}</FormLabel> | ||||||
|           <Box mb={2} w="240px"> |           <Flex mb={2}> | ||||||
|  |             <Box w="240px"> | ||||||
|               <FileInputButton |               <FileInputButton | ||||||
|                 value={newConfig} |                 value={newConfig} | ||||||
|                 setValue={(v) => setNewConfig(v)} |                 setValue={(v) => setNewConfig(v)} | ||||||
| @@ -101,6 +108,15 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|                 isStringFile |                 isStringFile | ||||||
|               /> |               /> | ||||||
|             </Box> |             </Box> | ||||||
|  |             <Button | ||||||
|  |               rightIcon={<ClipboardText size={20} />} | ||||||
|  |               onClick={onImportConfiguration} | ||||||
|  |               hidden={!getDevice.data} | ||||||
|  |               ml={2} | ||||||
|  |             > | ||||||
|  |               Current Configuration | ||||||
|  |             </Button> | ||||||
|  |           </Flex> | ||||||
|           <Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} /> |           <Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} /> | ||||||
|           <FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage> |           <FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage> | ||||||
|         </FormControl> |         </FormControl> | ||||||
|   | |||||||
| @@ -57,7 +57,8 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu | |||||||
|     upgrade({ |     upgrade({ | ||||||
|       keepRedirector: isRedirector, |       keepRedirector: isRedirector, | ||||||
|       uri, |       uri, | ||||||
|       signature: device?.restrictedDevice ? ref.current?.values?.signature : undefined, |       signature: | ||||||
|  |         device?.restrictedDevice && !device?.restrictionDetails?.developer ? ref.current?.values?.signature : undefined, | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -89,7 +90,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu | |||||||
|                 </FormLabel> |                 </FormLabel> | ||||||
|                 <Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" /> |                 <Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" /> | ||||||
|               </FormControl> |               </FormControl> | ||||||
|               {device?.restrictedDevice && ( |               {device?.restrictedDevice && !device?.restrictionDetails?.developer && ( | ||||||
|                 <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,15 +1,19 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { Flex, ModalHeader as Header, Spacer } from '@chakra-ui/react'; | import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react'; | ||||||
|  |  | ||||||
| export interface ModalHeaderProps { | export interface ModalHeaderProps { | ||||||
|   title: string; |   title: string; | ||||||
|  |   left?: React.ReactNode; | ||||||
|   right: React.ReactNode; |   right: React.ReactNode; | ||||||
| } | } | ||||||
|  |  | ||||||
| const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, right }) => ( | const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, 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,6 +8,7 @@ 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']; | ||||||
| @@ -15,7 +16,7 @@ export type ModalProps = { | |||||||
|   children: React.ReactElement; |   children: React.ReactElement; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => { | const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, children }: ModalProps) => { | ||||||
|   const maxWidth = React.useMemo(() => { |   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; | ||||||
| @@ -32,6 +33,7 @@ const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: | |||||||
|       <ModalContent maxWidth={maxWidth}> |       <ModalContent maxWidth={maxWidth}> | ||||||
|         <ModalHeader |         <ModalHeader | ||||||
|           title={title} |           title={title} | ||||||
|  |           left={tags} | ||||||
|           right={ |           right={ | ||||||
|             <HStack spacing={2}> |             <HStack spacing={2}> | ||||||
|               {topRightButtons} |               {topRightButtons} | ||||||
|   | |||||||
| @@ -1,40 +1,42 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { MenuItem, useToast } from '@chakra-ui/react'; | import { Alert, AlertIcon, Box, Button, Center, useToast } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { 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'; |  | ||||||
| 
 | 
 | ||||||
| type Props = { | export type RebootModalProps = { | ||||||
|   device: GatewayDevice; |   serialNumber: string; | ||||||
|   refresh: () => void; |   modalProps: { | ||||||
|  |     isOpen: boolean; | ||||||
|  |     onClose: () => void; | ||||||
|  |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const RebootMenuItem = ({ device, refresh }: Props) => { | export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => { | ||||||
|   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 } = useRebootDevice({ serialNumber: device.serialNumber }); |   const { mutateAsync: reboot, isLoading } = useRebootDevice({ 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-${device.serialNumber}`, |           id: `device-connection-${serialNumber}`, | ||||||
|           type: 'DEVICE_CONNECTION', |           type: 'DEVICE_CONNECTION', | ||||||
|           serialNumber: device.serialNumber, |           serialNumber, | ||||||
|           callback: () => { |           callback: () => { | ||||||
|             const id = `device-connection-notification-${device.serialNumber}`; |             const id = `device-connection-notification-${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: device.serialNumber }), |                 description: t('controller.devices.finished_reboot', { serialNumber }), | ||||||
|                 status: 'success', |                 status: 'success', | ||||||
|                 duration: 5000, |                 duration: 5000, | ||||||
|                 isClosable: true, |                 isClosable: true, | ||||||
| @@ -44,17 +46,17 @@ const RebootMenuItem = ({ device, refresh }: Props) => { | |||||||
|           }, |           }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           id: `device-disconnected-${device.serialNumber}`, |           id: `device-disconnected-${serialNumber}`, | ||||||
|           type: 'DEVICE_DISCONNECTION', |           type: 'DEVICE_DISCONNECTION', | ||||||
|           serialNumber: device.serialNumber, |           serialNumber, | ||||||
|           callback: () => { |           callback: () => { | ||||||
|             const id = `device-disconnection-notification-${device.serialNumber}`; |             const id = `device-disconnection-notification-${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: device.serialNumber }), |                 description: t('controller.devices.started_reboot', { serialNumber }), | ||||||
|                 status: 'success', |                 status: 'success', | ||||||
|                 duration: 5000, |                 duration: 5000, | ||||||
|                 isClosable: true, |                 isClosable: true, | ||||||
| @@ -66,17 +68,39 @@ const RebootMenuItem = ({ device, refresh }: Props) => { | |||||||
|       ]); |       ]); | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|   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 <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>; |   return ( | ||||||
|  |     <Modal | ||||||
|  |       {...modalProps} | ||||||
|  |       title={t('commands.reboot')} | ||||||
|  |       topRightButtons={ | ||||||
|  |         <Button colorScheme="blue" onClick={handleRebootClick} isLoading={isLoading}> | ||||||
|  |           {t('commands.reboot')} | ||||||
|  |         </Button> | ||||||
|  |       } | ||||||
|  |       options={{ | ||||||
|  |         modalSize: 'sm', | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <Box> | ||||||
|  |         <Center mb={2}> | ||||||
|  |           <Alert status="info" w="unset"> | ||||||
|  |             <AlertIcon /> | ||||||
|  |             {t('commands.reboot_description')} | ||||||
|  |           </Alert> | ||||||
|  |         </Center> | ||||||
|  |       </Box> | ||||||
|  |     </Modal> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| export default RebootMenuItem; |  | ||||||
| @@ -183,7 +183,9 @@ const CustomScriptForm = ({ | |||||||
|             <> |             <> | ||||||
|               <Flex> |               <Flex> | ||||||
|                 <Box> |                 <Box> | ||||||
|                   {device?.restrictedDevice && <SignatureField name="signature" isDisabled={areFieldsDisabled} />} |                   {device?.restrictedDevice && !device?.restrictionDetails?.developer && ( | ||||||
|  |                     <SignatureField name="signature" isDisabled={areFieldsDisabled} /> | ||||||
|  |                   )} | ||||||
|                 </Box> |                 </Box> | ||||||
|               </Flex> |               </Flex> | ||||||
|               <SelectField |               <SelectField | ||||||
|   | |||||||
| @@ -60,8 +60,14 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => { | |||||||
|     let requestData: { |     let requestData: { | ||||||
|       [k: string]: unknown; |       [k: string]: unknown; | ||||||
|       serialNumber: string; |       serialNumber: string; | ||||||
|  |       script?: string; | ||||||
|       timeout?: number | undefined; |       timeout?: number | undefined; | ||||||
|     } = data; |     } = data; | ||||||
|  |  | ||||||
|  |     if (requestData.script) { | ||||||
|  |       requestData.script = btoa(requestData.script); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (selectedScript === 'diagnostics') { |     if (selectedScript === 'diagnostics') { | ||||||
|       requestData = { |       requestData = { | ||||||
|         serialNumber: device?.serialNumber ?? '', |         serialNumber: device?.serialNumber ?? '', | ||||||
| @@ -76,7 +82,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 ? data.signature : undefined, |         signature: device?.restrictedDevice && !device?.restrictionDetails?.developer ? 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, | ||||||
| @@ -88,6 +94,19 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => { | |||||||
|         setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2)); |         setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2)); | ||||||
|         queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']); |         queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']); | ||||||
|       }, |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         if (axios.isAxiosError(e) && e.response?.data?.ErrorDescription) { | ||||||
|  |           toast({ | ||||||
|  |             id: 'script-update-error', | ||||||
|  |             title: t('common.error'), | ||||||
|  |             description: e.response?.data?.ErrorDescription, | ||||||
|  |             status: 'error', | ||||||
|  |             duration: 5000, | ||||||
|  |             isClosable: true, | ||||||
|  |             position: 'top-right', | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|     }); |     }); | ||||||
|     if (!waitForResponse) { |     if (!waitForResponse) { | ||||||
|       toast({ |       toast({ | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }: | |||||||
|     if (isOpen) resetData(); |     if (isOpen) resetData(); | ||||||
|   }, [isOpen]); |   }, [isOpen]); | ||||||
|   return ( |   return ( | ||||||
|     (<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside"> |     <Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside"> | ||||||
|       <ModalOverlay /> |       <ModalOverlay /> | ||||||
|       <ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}> |       <ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}> | ||||||
|         <ModalHeader |         <ModalHeader | ||||||
| @@ -66,7 +66,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }: | |||||||
|             <> |             <> | ||||||
|               {csvData ? ( |               {csvData ? ( | ||||||
|                 // @ts-ignore |                 // @ts-ignore | ||||||
|                 (<CSVLink |                 <CSVLink | ||||||
|                   filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`} |                   filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`} | ||||||
|                   data={csvData as object[]} |                   data={csvData as object[]} | ||||||
|                 > |                 > | ||||||
| @@ -77,7 +77,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }: | |||||||
|                     label={t('common.download')} |                     label={t('common.download')} | ||||||
|                     onClick={() => {}} |                     onClick={() => {}} | ||||||
|                   /> |                   /> | ||||||
|                 </CSVLink>) |                 </CSVLink> | ||||||
|               ) : ( |               ) : ( | ||||||
|                 <ResponsiveButton |                 <ResponsiveButton | ||||||
|                   color="gray" |                   color="gray" | ||||||
| @@ -118,6 +118,6 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }: | |||||||
|         confirm={closeCancelAndForm} |         confirm={closeCancelAndForm} | ||||||
|         cancel={closeConfirm} |         cancel={closeConfirm} | ||||||
|       /> |       /> | ||||||
|     </Modal>) |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -149,6 +149,8 @@ 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,6 +52,8 @@ type ConnectionStatisticsMessage = { | |||||||
|       numberOfDevices: number; |       numberOfDevices: number; | ||||||
|       numberOfConnectingDevices: number; |       numberOfConnectingDevices: number; | ||||||
|       averageConnectedTime: number; |       averageConnectedTime: number; | ||||||
|  |       tx: number; | ||||||
|  |       rx: number; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   serialNumbers?: undefined; |   serialNumbers?: undefined; | ||||||
| @@ -85,6 +87,8 @@ 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; | ||||||
|   | |||||||
| @@ -110,6 +110,18 @@ export const useDeleteCommand = () => { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const useGetSingleCommandHistory = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => | ||||||
|  |   useQuery( | ||||||
|  |     ['commands', serialNumber, commandId], | ||||||
|  |     () => | ||||||
|  |       axiosGw | ||||||
|  |         .get(`command/${commandId}?serialNumber=${serialNumber}`) | ||||||
|  |         .then((response) => response.data as DeviceCommandHistory), | ||||||
|  |     { | ||||||
|  |       enabled: serialNumber !== undefined && serialNumber !== '' && commandId !== undefined && commandId !== '', | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  |  | ||||||
| export type EventQueueResponse = { | export type EventQueueResponse = { | ||||||
|   UUID: string; |   UUID: string; | ||||||
|   attachFile: number; |   attachFile: number; | ||||||
| @@ -245,6 +257,7 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => { | |||||||
|       queryClient.invalidateQueries(['commands', serialNumber]); |       queryClient.invalidateQueries(['commands', serialNumber]); | ||||||
|     }, |     }, | ||||||
|     onError: (e) => { |     onError: (e) => { | ||||||
|  |       queryClient.invalidateQueries(['commands', serialNumber]); | ||||||
|       if (axios.isAxiosError(e)) { |       if (axios.isAxiosError(e)) { | ||||||
|         toast({ |         toast({ | ||||||
|           id: 'script-error', |           id: 'script-error', | ||||||
| @@ -263,14 +276,44 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => { | |||||||
| const downloadScript = (serialNumber: string, commandId: string) => | const downloadScript = (serialNumber: string, commandId: string) => | ||||||
|   axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' }); |   axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' }); | ||||||
|  |  | ||||||
| export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => | export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => { | ||||||
|   useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), { |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|  |  | ||||||
|  |   return useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), { | ||||||
|     enabled: false, |     enabled: false, | ||||||
|     onSuccess: (response) => { |     onSuccess: (response) => { | ||||||
|       const blob = new Blob([response.data], { type: 'application/octet-stream' }); |       const blob = new Blob([response.data], { type: 'application/octet-stream' }); | ||||||
|       const link = document.createElement('a'); |       const link = document.createElement('a'); | ||||||
|       link.href = window.URL.createObjectURL(blob); |       link.href = window.URL.createObjectURL(blob); | ||||||
|       link.download = `Script_${commandId}.tar.gz`; |       const headerLine = | ||||||
|  |         (response.headers['content-disposition'] as string | undefined) ?? | ||||||
|  |         (response.headers['content-disposition'] as string | undefined); | ||||||
|  |       const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Script_${commandId}.tar.gz`; | ||||||
|  |       link.download = filename; | ||||||
|       link.click(); |       link.click(); | ||||||
|     }, |     }, | ||||||
|  |     onError: (e) => { | ||||||
|  |       if (axios.isAxiosError(e)) { | ||||||
|  |         const bufferResponse = e.response?.data; | ||||||
|  |         let errorMessage = ''; | ||||||
|  |         // If the response is a buffer, parse to JSON object | ||||||
|  |         if (bufferResponse instanceof ArrayBuffer) { | ||||||
|  |           const decoder = new TextDecoder('utf-8'); | ||||||
|  |           const json = JSON.parse(decoder.decode(bufferResponse)); | ||||||
|  |           errorMessage = json.ErrorDescription; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         toast({ | ||||||
|  |           id: `script-download-error-${serialNumber}`, | ||||||
|  |           title: t('common.error'), | ||||||
|  |           description: errorMessage, | ||||||
|  |           status: 'error', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|         }); |         }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -11,8 +11,10 @@ export type DeviceLog = { | |||||||
|   severity: number; |   severity: number; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getDeviceLogs = (limit: number, serialNumber?: string) => async () => | const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1) => async () => | ||||||
|   axiosGw.get(`device/${serialNumber}/logs?newest=true&limit=${limit}`).then((response) => response.data) as Promise<{ |   axiosGw | ||||||
|  |     .get(`device/${serialNumber}/logs?newest=true&limit=${limit}&logType=${logType}`) | ||||||
|  |     .then((response) => response.data) as Promise<{ | ||||||
|     values: DeviceLog[]; |     values: DeviceLog[]; | ||||||
|     serialNumber: string; |     serialNumber: string; | ||||||
|   }>; |   }>; | ||||||
| @@ -21,20 +23,29 @@ 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 }], getDeviceLogs(limit, serialNumber), { |   useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), { | ||||||
|     keepPreviousData: true, |     keepPreviousData: true, | ||||||
|     enabled: serialNumber !== undefined && serialNumber !== '', |     enabled: serialNumber !== undefined && serialNumber !== '', | ||||||
|     staleTime: 30000, |     staleTime: 30000, | ||||||
|     onError, |     onError, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| const deleteLogs = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) => | const deleteLogs = async ({ | ||||||
|   axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}`); |   serialNumber, | ||||||
|  |   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(); | ||||||
|  |  | ||||||
| @@ -45,15 +56,25 @@ export const useDeleteLogs = () => { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getLogsBatch = (serialNumber?: string, start?: number, end?: number, limit?: number, offset?: number) => | const getLogsBatch = ( | ||||||
|  |   serialNumber?: string, | ||||||
|  |   start?: number, | ||||||
|  |   end?: number, | ||||||
|  |   limit?: number, | ||||||
|  |   offset?: number, | ||||||
|  |   logType?: 0 | 1, | ||||||
|  | ) => | ||||||
|   axiosGw |   axiosGw | ||||||
|     .get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}`) |     .get( | ||||||
|  |       `device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}&logType=${logType}`, | ||||||
|  |     ) | ||||||
|     .then((response) => response.data) as Promise<{ |     .then((response) => response.data) as Promise<{ | ||||||
|     values: DeviceLog[]; |     values: DeviceLog[]; | ||||||
|     serialNumber: string; |     serialNumber: string; | ||||||
|   }>; |   }>; | ||||||
|  |  | ||||||
| const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () => { | const getDeviceLogsWithTimestamps = | ||||||
|  |   (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[] = []; | ||||||
| @@ -63,7 +84,7 @@ const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end? | |||||||
|     }; |     }; | ||||||
|     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); |       latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset, logType); | ||||||
|       logs = logs.concat(latestResponse.values); |       logs = logs.concat(latestResponse.values); | ||||||
|       offset += limit; |       offset += limit; | ||||||
|     } while (latestResponse.values.length === limit); |     } while (latestResponse.values.length === limit); | ||||||
| @@ -77,14 +98,20 @@ export const useGetDeviceLogsWithTimestamps = ({ | |||||||
|   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(['devicelogs', serialNumber, { start, end }], getDeviceLogsWithTimestamps(serialNumber, start, end), { |   useQuery( | ||||||
|  |     ['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,7 +165,10 @@ 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,17 +7,32 @@ 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( |   return useQuery(['get-device-profile'], () => getAllAvailableFirmware(deviceType), { | ||||||
|     ['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')) | ||||||
| @@ -34,8 +49,7 @@ 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 }) => | ||||||
| @@ -56,7 +70,13 @@ 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`, { serialNumber, when: 0, keepRedirector, uri, signature }), |       axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, { | ||||||
|  |         serialNumber, | ||||||
|  |         when: 0, | ||||||
|  |         keepRedirector, | ||||||
|  |         uri, | ||||||
|  |         signature, | ||||||
|  |       }), | ||||||
|     { |     { | ||||||
|       onSuccess: () => { |       onSuccess: () => { | ||||||
|         toast({ |         toast({ | ||||||
| @@ -222,3 +242,23 @@ 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,4 +1,4 @@ | |||||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||||
| import { axiosGw } from 'constants/axiosInstances'; | import { axiosGw } from 'constants/axiosInstances'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
| @@ -89,3 +89,18 @@ export const useDeleteHealthChecks = () => { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const getDevicesWithHealthBetween = ( | ||||||
|  |   context: QueryFunctionContext<[string, string, { lowerLimit: number; upperLimit: number }]>, | ||||||
|  | ) => | ||||||
|  |   axiosGw | ||||||
|  |     .get(`devices?health=true&lowLimit=${context.queryKey[2].lowerLimit}&highLimit=${context.queryKey[2].upperLimit}`) | ||||||
|  |     .then((res) => res.data.serialNumbers as string[]); | ||||||
|  |  | ||||||
|  | export const useGetDevicesWithHealthBetween = ({ | ||||||
|  |   lowerLimit, | ||||||
|  |   upperLimit, | ||||||
|  | }: { | ||||||
|  |   lowerLimit: number; | ||||||
|  |   upperLimit: number; | ||||||
|  | }) => useQuery(['devices', 'health', { lowerLimit, upperLimit }], getDevicesWithHealthBetween); | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								src/hooks/Network/Secrets.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/hooks/Network/Secrets.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||||
|  | import { axiosSec } from 'constants/axiosInstances'; | ||||||
|  |  | ||||||
|  | export type SecretName = 'google.maps.apikey' | string; | ||||||
|  |  | ||||||
|  | export type Secret = { | ||||||
|  |   key: SecretName; | ||||||
|  |   value: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type SecretDictionaryValue = { | ||||||
|  |   key: SecretName; | ||||||
|  |   description: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getSecret = async (context: QueryFunctionContext<string[], unknown>) => | ||||||
|  |   axiosSec.get(`/systemSecret/${context.queryKey[1]}`).then(({ data }: { data: Secret }) => data); | ||||||
|  |  | ||||||
|  | export const useGetSystemSecret = ({ secret }: { secret: SecretName }) => | ||||||
|  |   useQuery(['secrets', secret], getSecret, { | ||||||
|  |     staleTime: 1000 * 60 * 10, | ||||||
|  |     refetchInterval: 1000 * 60 * 10, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | const getAllSecrets = async () => | ||||||
|  |   axiosSec.get('/systemSecret/0?all=true').then(({ data }: { data: { secrets: Secret[] } }) => data.secrets); | ||||||
|  |  | ||||||
|  | export const useGetAllSystemSecrets = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useQuery(['secrets'], getAllSecrets, { | ||||||
|  |     staleTime: 1000 * 60 * 10, | ||||||
|  |     refetchInterval: 1000 * 60 * 10, | ||||||
|  |     onSuccess: (data) => { | ||||||
|  |       for (const secret of data) { | ||||||
|  |         queryClient.setQueryData(['secrets', secret.key], secret); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getSecretsDictionary = async () => | ||||||
|  |   axiosSec | ||||||
|  |     .get('/systemSecret/0?dictionary=true') | ||||||
|  |     .then(({ data }: { data: { knownKeys: SecretDictionaryValue[] } }) => data.knownKeys); | ||||||
|  |  | ||||||
|  | export const useGetSystemSecretsDictionary = () => | ||||||
|  |   useQuery(['secrets', 'dictionary'], getSecretsDictionary, { | ||||||
|  |     staleTime: 1000 * 60 * 10, | ||||||
|  |     refetchInterval: 1000 * 60 * 10, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | const updateSecret = async ({ key, value }: { key: string; value: string }) => | ||||||
|  |   axiosSec.put(`/systemSecret/${key}?value=${value}`, { key, value }); | ||||||
|  |  | ||||||
|  | export const useUpdateSystemSecret = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |   return useMutation(updateSecret, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['secrets']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const useCreateSystemSecret = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |   return useMutation(updateSecret, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['secrets']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteSecret = async (key: string) => axiosSec.delete(`/systemSecret/${key}`); | ||||||
|  |  | ||||||
|  | export const useDeleteSystemSecret = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |   return useMutation(deleteSecret, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['secrets']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | import { useQuery } from '@tanstack/react-query'; | ||||||
| import { axiosGw } from 'constants/axiosInstances'; | import { axiosGw } from 'constants/axiosInstances'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
| @@ -21,6 +21,18 @@ 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; | ||||||
| @@ -148,6 +160,11 @@ export type DeviceStatistics = { | |||||||
|       }; |       }; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|  |   gps?: { | ||||||
|  |     elevation: string; | ||||||
|  |     latitude: string; | ||||||
|  |     longitude: string; | ||||||
|  |   }; | ||||||
|   version?: number; |   version?: number; | ||||||
| }; | }; | ||||||
| const getLastStats = (serialNumber?: string) => | const getLastStats = (serialNumber?: string) => | ||||||
| @@ -163,7 +180,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 !== '' && false, |     enabled: serialNumber !== undefined && serialNumber !== '', | ||||||
|     staleTime: 1000 * 60, |     staleTime: 1000 * 60, | ||||||
|     onError, |     onError, | ||||||
|   }); |   }); | ||||||
| @@ -183,24 +200,12 @@ export const useGetDeviceNewestStats = ({ | |||||||
|   serialNumber?: string; |   serialNumber?: string; | ||||||
|   limit: number; |   limit: number; | ||||||
|   onError?: (e: AxiosError) => void; |   onError?: (e: AxiosError) => void; | ||||||
| }) => { | }) => | ||||||
|   const queryClient = useQueryClient(); |   useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), { | ||||||
|  |  | ||||||
|   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,6 @@ | |||||||
| import { useToast } from '@chakra-ui/react'; | import { useToast } from '@chakra-ui/react'; | ||||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||||
|  | import axios from 'axios'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { axiosGw } from 'constants/axiosInstances'; | import { axiosGw } from 'constants/axiosInstances'; | ||||||
|  |  | ||||||
| @@ -85,14 +86,44 @@ export const useTrace = ({ serialNumber, alertOnCompletion }: { serialNumber: st | |||||||
| export const downloadTrace = (serialNumber: string, commandId: string) => | export const downloadTrace = (serialNumber: string, commandId: string) => | ||||||
|   axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' }); |   axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' }); | ||||||
|  |  | ||||||
| export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => | export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => { | ||||||
|   useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), { |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|  |  | ||||||
|  |   return useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), { | ||||||
|     enabled: false, |     enabled: false, | ||||||
|     onSuccess: (response) => { |     onSuccess: (response) => { | ||||||
|       const blob = new Blob([response.data], { type: 'application/octet-stream' }); |       const blob = new Blob([response.data], { type: 'application/octet-stream' }); | ||||||
|       const link = document.createElement('a'); |       const link = document.createElement('a'); | ||||||
|       link.href = window.URL.createObjectURL(blob); |       link.href = window.URL.createObjectURL(blob); | ||||||
|       link.download = `Trace_${commandId}.pcap`; |       const headerLine = | ||||||
|  |         (response.headers['content-disposition'] as string | undefined) ?? | ||||||
|  |         (response.headers['content-disposition'] as string | undefined); | ||||||
|  |       const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Trace_${commandId}.pcap`; | ||||||
|  |       link.download = filename; | ||||||
|       link.click(); |       link.click(); | ||||||
|     }, |     }, | ||||||
|  |     onError: (e) => { | ||||||
|  |       if (axios.isAxiosError(e)) { | ||||||
|  |         const bufferResponse = e.response?.data; | ||||||
|  |         let errorMessage = ''; | ||||||
|  |         // If the response is a buffer, parse to JSON object | ||||||
|  |         if (bufferResponse instanceof ArrayBuffer) { | ||||||
|  |           const decoder = new TextDecoder('utf-8'); | ||||||
|  |           const json = JSON.parse(decoder.decode(bufferResponse)); | ||||||
|  |           errorMessage = json.ErrorDescription; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         toast({ | ||||||
|  |           id: `trace-download-error-${serialNumber}`, | ||||||
|  |           title: t('common.error'), | ||||||
|  |           description: errorMessage, | ||||||
|  |           status: 'error', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|         }); |         }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { useToast } from '@chakra-ui/react'; | import { useToast } from '@chakra-ui/react'; | ||||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { 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,11 +58,23 @@ export type User = { | |||||||
|   waitingForEmailCheck: boolean; |   waitingForEmailCheck: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getAvatarPromises = (userList: User[]) => { | const getAvatarPromises = (userList: User[], queryClient: QueryClient) => { | ||||||
|   const promises = userList.map(async (user) => { |   const promises = userList.map(async (user) => { | ||||||
|     if (user.avatar !== '' && user.avatar !== '0') { |     if (user.avatar !== '' && user.avatar !== '0') { | ||||||
|       return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, { |       // If the avatar is already in the cache, return it | ||||||
|  |       const cachedAvatar = queryClient.getQueryData(['avatar', user.id, user.avatar]); | ||||||
|  |       if (cachedAvatar) return cachedAvatar; | ||||||
|  |  | ||||||
|  |       return axiosSec | ||||||
|  |         .get(`avatar/${user.id}?cache=${user.avatar}`, { | ||||||
|           responseType: 'arraybuffer', |           responseType: 'arraybuffer', | ||||||
|  |         }) | ||||||
|  |         .then((response) => { | ||||||
|  |           queryClient.setQueryData(['avatar', user.id, user.avatar], response); | ||||||
|  |           return response; | ||||||
|  |         }) | ||||||
|  |         .catch((e) => { | ||||||
|  |           throw e; | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     return Promise.resolve(''); |     return Promise.resolve(''); | ||||||
| @@ -71,10 +83,35 @@ const getAvatarPromises = (userList: User[]) => { | |||||||
|   return promises; |   return promises; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getUsers = async () => { | const getBatchUsers = async (offset: number, limit: number) => { | ||||||
|   const users = await axiosSec.get('users').then(({ data }) => data.users as User[]); |   const users = await axiosSec | ||||||
|  |     .get(`users?offset=${offset}&limit=${limit}&withExtendedInfo=true`) | ||||||
|  |     .then(({ data }) => data.users as User[]); | ||||||
|  |  | ||||||
|   const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) => |   return users; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getAllUsers = async () => { | ||||||
|  |   let users: User[] = []; | ||||||
|  |   let offset = 0; | ||||||
|  |   const limit = 500; | ||||||
|  |   let lastResponseLength = 0; | ||||||
|  |  | ||||||
|  |   do { | ||||||
|  |     // eslint-disable-next-line no-await-in-loop | ||||||
|  |     const response = await getBatchUsers(offset, limit); | ||||||
|  |     users = [...users, ...response]; | ||||||
|  |     offset += limit; | ||||||
|  |     lastResponseLength = response.length; | ||||||
|  |   } while (lastResponseLength === limit); | ||||||
|  |  | ||||||
|  |   return users; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getUsers = async (queryClient: QueryClient) => { | ||||||
|  |   const users = await getAllUsers(); | ||||||
|  |  | ||||||
|  |   const avatars = await Promise.allSettled(getAvatarPromises(users, queryClient)).then((results) => | ||||||
|     results.map((response) => { |     results.map((response) => { | ||||||
|       if (response.status === 'fulfilled' && response?.value !== '') { |       if (response.status === 'fulfilled' && response?.value !== '') { | ||||||
|         const base64 = btoa( |         const base64 = btoa( | ||||||
| @@ -93,8 +130,10 @@ const getUsers = async () => { | |||||||
| 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, { |   return useQuery(['users'], () => getUsers(queryClient), { | ||||||
|  |     staleTime: 30 * 1000, | ||||||
|     onError: (e: AxiosError) => { |     onError: (e: AxiosError) => { | ||||||
|       if (!toast.isActive('users-fetching-error')) |       if (!toast.isActive('users-fetching-error')) | ||||||
|         toast({ |         toast({ | ||||||
| @@ -118,7 +157,7 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) => | |||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |  | ||||||
|   return useQuery( |   return useQuery( | ||||||
|     ['get-user', id], |     ['users', 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, | ||||||
| @@ -173,16 +212,41 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| export const useSuspendUser = ({ id }: { id: string }) => | export const useSuspendUser = ({ id }: { id: string }) => { | ||||||
|   useMutation((isSuspended: boolean) => |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   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 useResetPassword = ({ id }: { id: string }) => | export const useResetMfa = ({ id }: { id: string }) => { | ||||||
|   useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {})); |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}), { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['users']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const useResetPassword = ({ id }: { id: string }) => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}), { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['users']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
| const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`); | const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`); | ||||||
| export const useDeleteUser = () => { | export const useDeleteUser = () => { | ||||||
|   | |||||||
| @@ -1,7 +1,20 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react'; | import { | ||||||
|  |   Box, | ||||||
|  |   CircularProgress, | ||||||
|  |   CircularProgressLabel, | ||||||
|  |   Flex, | ||||||
|  |   Heading, | ||||||
|  |   Icon, | ||||||
|  |   Text, | ||||||
|  |   Tooltip, | ||||||
|  |   VStack, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import { ArrowSquareDown, ArrowSquareUp, Clock } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { Card } from 'components/Containers/Card'; | ||||||
| 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 = () => { | ||||||
| @@ -10,18 +23,19 @@ const SidebarDevices = () => { | |||||||
|   const [lastTime, setLastTime] = React.useState<Date | undefined>(); |   const [lastTime, setLastTime] = React.useState<Date | undefined>(); | ||||||
|   const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>(); |   const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>(); | ||||||
|  |  | ||||||
|   const getTime = () => { |   const time = React.useMemo(() => { | ||||||
|     if (lastTime === undefined || lastUpdate === undefined) return null; |     if (lastTime === undefined || lastUpdate === undefined) return null; | ||||||
|  |  | ||||||
|     const seconds = lastTime.getTime() - lastUpdate.getTime(); |     const seconds = lastTime.getTime() - lastUpdate.getTime(); | ||||||
|  |  | ||||||
|     return Math.max(0, Math.floor(seconds / 1000)); |     return Math.max(0, Math.floor(seconds / 1000)); | ||||||
|   }; |   }, [lastTime, lastUpdate]); | ||||||
|  |  | ||||||
|   const refresh = () => { |   const circleColor = () => { | ||||||
|     if (document.visibilityState !== 'hidden') { |     if (time === null) return 'gray.300'; | ||||||
|       getStats.refetch(); |     if (time < 10) return 'green.300'; | ||||||
|     } |     if (time < 30) return 'yellow.300'; | ||||||
|  |     return 'red.300'; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
| @@ -37,37 +51,60 @@ const SidebarDevices = () => { | |||||||
|     }; |     }; | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     document.addEventListener('visibilitychange', refresh); |  | ||||||
|  |  | ||||||
|     return () => { |  | ||||||
|       document.removeEventListener('visibilitychange', refresh); |  | ||||||
|     }; |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   if (!getStats.data) return null; |   if (!getStats.data) return null; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <VStack spacing={4}> |     <Card borderWidth="2px"> | ||||||
|  |       <Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}> | ||||||
|  |         <CircularProgress | ||||||
|  |           isIndeterminate | ||||||
|  |           color={circleColor()} | ||||||
|  |           position="absolute" | ||||||
|  |           right="6px" | ||||||
|  |           top="6px" | ||||||
|  |           w="unset" | ||||||
|  |           size={6} | ||||||
|  |           thickness="14px" | ||||||
|  |         > | ||||||
|  |           <CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel> | ||||||
|  |         </CircularProgress> | ||||||
|  |       </Tooltip> | ||||||
|  |       <Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}> | ||||||
|  |         <Box position="absolute" right="8px" top="8px" w="unset" hidden> | ||||||
|  |           <Clock size={16} /> | ||||||
|  |         </Box> | ||||||
|  |       </Tooltip> | ||||||
|  |       <VStack mb={-1}> | ||||||
|         <Flex flexDir="column" textAlign="center"> |         <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" display="flex" justifyContent="center"> | ||||||
|           {t('common.connected')} {t('devices.title')} |             <Text> | ||||||
|         </Heading> |               {t('common.connected')} {t('devices.title')}{' '} | ||||||
|         <Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400"> |             </Text>{' '} | ||||||
|           ({getStats.data.connectingDevices} {t('controller.devices.connecting')}) |  | ||||||
|           </Heading> |           </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={1}> | ||||||
|               {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} |               {minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)} | ||||||
|             </Heading> |             </Heading> | ||||||
|           </Tooltip> |           </Tooltip> | ||||||
|           <Heading size="xs">{t('controller.devices.average_uptime')}</Heading> |           <Heading size="xs">{t('controller.devices.average_uptime')}</Heading> | ||||||
|         <Heading size="xs" mt={2} fontStyle="italic" fontWeight="normal" color="gray.400"> |           <Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}> | ||||||
|           {t('controller.stats.seconds_ago', { s: getTime() })} |             <Tooltip hasArrow label="Rx"> | ||||||
|         </Heading> |               <Flex alignItems="center" mr={1}> | ||||||
|  |                 <Icon as={ArrowSquareUp} weight="bold" boxSize={5} mt="1px" color="blue.400" />{' '} | ||||||
|  |                 {getStats.data.rx !== undefined ? bytesString(getStats.data.rx, 0) : '-'} | ||||||
|  |               </Flex> | ||||||
|  |             </Tooltip> | ||||||
|  |             <Tooltip hasArrow label="Tx"> | ||||||
|  |               <Flex alignItems="center"> | ||||||
|  |                 <Icon as={ArrowSquareDown} weight="bold" boxSize={5} mt="1px" color="purple.400" />{' '} | ||||||
|  |                 {getStats.data.tx !== undefined ? bytesString(getStats.data.tx, 0) : '-'} | ||||||
|  |               </Flex> | ||||||
|  |             </Tooltip> | ||||||
|  |           </Flex> | ||||||
|         </Flex> |         </Flex> | ||||||
|       </VStack> |       </VStack> | ||||||
|  |     </Card> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ export interface Column<T> { | |||||||
|   alwaysShow?: boolean; |   alwaysShow?: boolean; | ||||||
|   Footer?: string; |   Footer?: string; | ||||||
|   accessor?: string; |   accessor?: string; | ||||||
|  |   stopPropagation?: boolean; | ||||||
|   disableSortBy?: boolean; |   disableSortBy?: boolean; | ||||||
|   hasPopover?: boolean; |   hasPopover?: boolean; | ||||||
|   customMaxWidth?: string; |   customMaxWidth?: string; | ||||||
|   | |||||||
| @@ -99,13 +99,14 @@ const DefaultConfigurationsList = () => { | |||||||
|       <CardBody> |       <CardBody> | ||||||
|         <Box overflowX="auto" w="100%"> |         <Box overflowX="auto" w="100%"> | ||||||
|           <LoadingOverlay isLoading={getConfigs.isFetching}> |           <LoadingOverlay isLoading={getConfigs.isFetching}> | ||||||
|             <DataTable |             <DataTable<DefaultConfigurationResponse> | ||||||
|               columns={columns as Column<object>[]} |               columns={columns} | ||||||
|               saveSettingsId="firmware.table" |               saveSettingsId="firmware.table" | ||||||
|               data={getConfigs.data ?? []} |               data={getConfigs.data ?? []} | ||||||
|               obj={t('controller.configurations.title')} |               obj={t('controller.configurations.title')} | ||||||
|               minHeight="200px" |               minHeight="200px" | ||||||
|               sortBy={[{ id: 'name', desc: true }]} |               sortBy={[{ id: 'name', desc: true }]} | ||||||
|  |               onRowClick={onViewDetails} | ||||||
|             /> |             /> | ||||||
|           </LoadingOverlay> |           </LoadingOverlay> | ||||||
|         </Box> |         </Box> | ||||||
|   | |||||||
							
								
								
									
										75
									
								
								src/pages/Device/LocationDisplayButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/pages/Device/LocationDisplayButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Box, Button, Flex, FormControl, FormLabel, useDisclosure } from '@chakra-ui/react'; | ||||||
|  | import { Wrapper } from '@googlemaps/react-wrapper'; | ||||||
|  | import { Globe } from 'phosphor-react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { GoogleMap } from 'components/Maps/GoogleMap'; | ||||||
|  | import { GoogleMapMarker } from 'components/Maps/GoogleMap/Marker'; | ||||||
|  | import { Modal } from 'components/Modals/Modal'; | ||||||
|  | import { useGetSystemSecret } from 'hooks/Network/Secrets'; | ||||||
|  | import { useGetDeviceLastStats } from 'hooks/Network/Statistics'; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   serialNumber: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const LocationDisplayButton = ({ serialNumber }: Props) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||||
|  |   const getGoogleApiKey = useGetSystemSecret({ secret: 'google.maps.apikey' }); | ||||||
|  |   const getLastStats = useGetDeviceLastStats({ serialNumber }); | ||||||
|  |  | ||||||
|  |   const location: google.maps.LatLngLiteral | undefined = React.useMemo(() => { | ||||||
|  |     if (!getLastStats.data?.gps) return undefined; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       return { | ||||||
|  |         lat: Number.parseFloat(getLastStats.data.gps.latitude), | ||||||
|  |         lng: Number.parseFloat(getLastStats.data.gps.longitude), | ||||||
|  |       }; | ||||||
|  |     } catch (e) { | ||||||
|  |       return undefined; | ||||||
|  |     } | ||||||
|  |   }, [getLastStats.data?.gps]); | ||||||
|  |  | ||||||
|  |   if (!location) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue"> | ||||||
|  |         {t('locations.view_gps')} | ||||||
|  |       </Button> | ||||||
|  |       <Modal isOpen={isOpen} onClose={onClose} title={t('locations.one')}> | ||||||
|  |         <Box w="100%" h="100%"> | ||||||
|  |           <Flex mb={4}> | ||||||
|  |             <FormControl w="unset"> | ||||||
|  |               <FormLabel>{t('locations.lat')}</FormLabel> | ||||||
|  |               <pre>{location.lat}</pre> | ||||||
|  |             </FormControl> | ||||||
|  |             <FormControl w="unset" mx={4}> | ||||||
|  |               <FormLabel>{t('locations.longitude')}</FormLabel> | ||||||
|  |               <pre>{location.lng}</pre> | ||||||
|  |             </FormControl> | ||||||
|  |             <FormControl w="unset"> | ||||||
|  |               <FormLabel>{t('locations.elevation')}</FormLabel> | ||||||
|  |               <pre>{getLastStats.data?.gps?.elevation}</pre> | ||||||
|  |             </FormControl> | ||||||
|  |           </Flex> | ||||||
|  |           {getGoogleApiKey.data ? ( | ||||||
|  |             <Box h="500px"> | ||||||
|  |               <Wrapper apiKey={getGoogleApiKey.data.value}> | ||||||
|  |                 <GoogleMap center={location} style={{ flexGrow: '1', height: '100%' }} zoom={10}> | ||||||
|  |                   <GoogleMapMarker position={location} /> | ||||||
|  |                 </GoogleMap> | ||||||
|  |               </Wrapper> | ||||||
|  |             </Box> | ||||||
|  |           ) : null} | ||||||
|  |         </Box> | ||||||
|  |       </Modal> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default LocationDisplayButton; | ||||||
| @@ -28,7 +28,7 @@ import { Modal } from 'components/Modals/Modal'; | |||||||
| import WifiScanResultDisplay from 'components/Modals/WifiScanModal/ResultDisplay'; | import WifiScanResultDisplay from 'components/Modals/WifiScanModal/ResultDisplay'; | ||||||
| import { compactDate } from 'helpers/dateFormatting'; | import { compactDate } from 'helpers/dateFormatting'; | ||||||
| import { uppercaseFirstLetter } from 'helpers/stringHelper'; | import { uppercaseFirstLetter } from 'helpers/stringHelper'; | ||||||
| import { DeviceCommandHistory } from 'hooks/Network/Commands'; | import { DeviceCommandHistory, useGetSingleCommandHistory } from 'hooks/Network/Commands'; | ||||||
| import { WifiScanResult } from 'models/Device'; | import { WifiScanResult } from 'models/Device'; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
| @@ -39,9 +39,13 @@ type Props = { | |||||||
|   command?: DeviceCommandHistory; |   command?: DeviceCommandHistory; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const CommandResultModal = ({ modalProps, command }: Props) => { | const CommandResultModal = ({ modalProps, command: initialCommandInfo }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { colorMode } = useColorMode(); |   const { colorMode } = useColorMode(); | ||||||
|  |   const { data: command } = useGetSingleCommandHistory({ | ||||||
|  |     commandId: initialCommandInfo?.UUID ?? '', | ||||||
|  |     serialNumber: initialCommandInfo?.serialNumber ?? '', | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   if (!command) return null; |   if (!command) return null; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,13 +44,13 @@ const CommandHistory = ({ serialNumber }: Props) => { | |||||||
|       <Box textAlign="right" display="flex"> |       <Box textAlign="right" display="flex"> | ||||||
|         <Spacer /> |         <Spacer /> | ||||||
|         <HStack> |         <HStack> | ||||||
|           <HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} /> |  | ||||||
|           <ColumnPicker |           <ColumnPicker | ||||||
|             columns={columns as Column<unknown>[]} |             columns={columns as Column<unknown>[]} | ||||||
|             hiddenColumns={hiddenColumns} |             hiddenColumns={hiddenColumns} | ||||||
|             setHiddenColumns={setHiddenColumns} |             setHiddenColumns={setHiddenColumns} | ||||||
|             preference="gateway.device.commandshistory.hiddenColumns" |             preference="gateway.device.commandshistory.hiddenColumns" | ||||||
|           /> |           /> | ||||||
|  |           <HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} /> | ||||||
|           <RefreshButton |           <RefreshButton | ||||||
|             isCompact |             isCompact | ||||||
|             isFetching={getCommands.isFetching} |             isFetching={getCommands.isFetching} | ||||||
|   | |||||||
| @@ -99,16 +99,6 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => { | |||||||
|   const actionCell = React.useCallback( |   const actionCell = React.useCallback( | ||||||
|     (command: DeviceCommandHistory) => ( |     (command: DeviceCommandHistory) => ( | ||||||
|       <HStack> |       <HStack> | ||||||
|         <Tooltip label={t('common.view_details')}> |  | ||||||
|           <IconButton |  | ||||||
|             aria-label={t('common.view_details')} |  | ||||||
|             onClick={onOpenDetails(command)} |  | ||||||
|             colorScheme="blue" |  | ||||||
|             icon={<MagnifyingGlass size={20} />} |  | ||||||
|             size="sm" |  | ||||||
|             isLoading={loadingDeleteSerial === command.UUID} |  | ||||||
|           /> |  | ||||||
|         </Tooltip> |  | ||||||
|         <Tooltip label={t('crud.delete')}> |         <Tooltip label={t('crud.delete')}> | ||||||
|           <IconButton |           <IconButton | ||||||
|             aria-label={t('crud.delete')} |             aria-label={t('crud.delete')} | ||||||
| @@ -119,6 +109,16 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => { | |||||||
|             isLoading={loadingDeleteSerial === command.UUID} |             isLoading={loadingDeleteSerial === command.UUID} | ||||||
|           /> |           /> | ||||||
|         </Tooltip> |         </Tooltip> | ||||||
|  |         <Tooltip label={t('common.view_details')}> | ||||||
|  |           <IconButton | ||||||
|  |             aria-label={t('common.view_details')} | ||||||
|  |             onClick={onOpenDetails(command)} | ||||||
|  |             colorScheme="blue" | ||||||
|  |             icon={<MagnifyingGlass size={20} />} | ||||||
|  |             size="sm" | ||||||
|  |             isLoading={loadingDeleteSerial === command.UUID} | ||||||
|  |           /> | ||||||
|  |         </Tooltip> | ||||||
|       </HStack> |       </HStack> | ||||||
|     ), |     ), | ||||||
|     [loadingDeleteSerial], |     [loadingDeleteSerial], | ||||||
|   | |||||||
							
								
								
									
										94
									
								
								src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import HistoryDatePickers from '../DatePickers'; | ||||||
|  | import DeleteLogModal from './DeleteModal'; | ||||||
|  | import useDeviceLogsTable from './useDeviceLogsTable'; | ||||||
|  | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
|  | import { ColumnPicker } from 'components/DataTables/ColumnPicker'; | ||||||
|  | import { DataTable } from 'components/DataTables/DataTable'; | ||||||
|  | import { Column } from 'models/Table'; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   serialNumber: string; | ||||||
|  | }; | ||||||
|  | const CrashLogs = ({ serialNumber }: Props) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const [limit, setLimit] = React.useState(25); | ||||||
|  |   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); | ||||||
|  |   const { time, setTime, getCustomLogs, getLogs, columns, modal } = useDeviceLogsTable({ | ||||||
|  |     serialNumber, | ||||||
|  |     limit, | ||||||
|  |     logType: 1, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const setNewTime = (start: Date, end: Date) => { | ||||||
|  |     setTime({ start, end }); | ||||||
|  |   }; | ||||||
|  |   const onClear = () => { | ||||||
|  |     setTime(undefined); | ||||||
|  |   }; | ||||||
|  |   const raiseLimit = () => { | ||||||
|  |     setLimit(limit + 25); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const noMoreAvailable = getLogs.data !== undefined && getLogs.data.values.length < limit; | ||||||
|  |  | ||||||
|  |   const data = React.useMemo(() => { | ||||||
|  |     if (getCustomLogs.data) return getCustomLogs.data.values.sort((a, b) => b.recorded - a.recorded); | ||||||
|  |     if (getLogs.data) return getLogs.data.values; | ||||||
|  |     return []; | ||||||
|  |   }, [getLogs.data, getCustomLogs.data]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Box> | ||||||
|  |       <Flex> | ||||||
|  |         <Spacer /> | ||||||
|  |         <HStack> | ||||||
|  |           <HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} /> | ||||||
|  |           <ColumnPicker | ||||||
|  |             columns={columns as Column<unknown>[]} | ||||||
|  |             hiddenColumns={hiddenColumns} | ||||||
|  |             setHiddenColumns={setHiddenColumns} | ||||||
|  |             preference="gateway.device.logs.hiddenColumns" | ||||||
|  |           /> | ||||||
|  |           <DeleteLogModal serialNumber={serialNumber} logType={0} /> | ||||||
|  |           <RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" /> | ||||||
|  |         </HStack> | ||||||
|  |       </Flex> | ||||||
|  |       <Box overflowY="auto" h="300px"> | ||||||
|  |         <DataTable | ||||||
|  |           columns={ | ||||||
|  |             columns as { | ||||||
|  |               id: string; | ||||||
|  |               Header: string; | ||||||
|  |               Footer: string; | ||||||
|  |               accessor: string; | ||||||
|  |             }[] | ||||||
|  |           } | ||||||
|  |           data={data} | ||||||
|  |           isLoading={getLogs.isFetching || getCustomLogs.isFetching} | ||||||
|  |           hiddenColumns={hiddenColumns} | ||||||
|  |           obj={t('controller.devices.logs')} | ||||||
|  |           // @ts-ignore | ||||||
|  |           hideControls | ||||||
|  |           showAllRows | ||||||
|  |         /> | ||||||
|  |         {getLogs.data !== undefined && ( | ||||||
|  |           <Center mt={1} hidden={getCustomLogs.data !== undefined}> | ||||||
|  |             {!noMoreAvailable || getLogs.isFetching ? ( | ||||||
|  |               <Button colorScheme="blue" onClick={raiseLimit} isLoading={getLogs.isFetching}> | ||||||
|  |                 {t('controller.devices.show_more')} | ||||||
|  |               </Button> | ||||||
|  |             ) : ( | ||||||
|  |               <Heading size="sm">{t('controller.devices.no_more_available')}!</Heading> | ||||||
|  |             )} | ||||||
|  |           </Center> | ||||||
|  |         )} | ||||||
|  |       </Box> | ||||||
|  |       {modal} | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default CrashLogs; | ||||||
| @@ -16,8 +16,8 @@ const CustomInputButton = React.forwardRef( | |||||||
|   ), |   ), | ||||||
| ); | ); | ||||||
|  |  | ||||||
| type Props = { serialNumber: string }; | type Props = { serialNumber: string; logType: 0 | 1 }; | ||||||
| const DeleteLogModal = ({ serialNumber }: Props) => { | const DeleteLogModal = ({ serialNumber, logType }: 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 }: Props) => { | |||||||
|  |  | ||||||
|   const onDeleteClick = () => { |   const onDeleteClick = () => { | ||||||
|     deleteLogs.mutate( |     deleteLogs.mutate( | ||||||
|       { endDate: Math.floor(date.getTime() / 1000), serialNumber }, |       { endDate: Math.floor(date.getTime() / 1000), serialNumber, logType }, | ||||||
|       { |       { | ||||||
|         onSuccess: () => { |         onSuccess: () => { | ||||||
|           modalProps.onClose(); |           modalProps.onClose(); | ||||||
|   | |||||||
| @@ -0,0 +1,55 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Box, Button, Code, Heading, useClipboard } from '@chakra-ui/react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||||
|  | import { Modal } from 'components/Modals/Modal'; | ||||||
|  | import { DeviceLog } from 'hooks/Network/DeviceLogs'; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   modalProps: { | ||||||
|  |     isOpen: boolean; | ||||||
|  |     onClose: () => void; | ||||||
|  |   }; | ||||||
|  |   log?: DeviceLog; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const DetailedLogViewModal = ({ modalProps, log }: Props) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(log?.log ?? {}, null, 2)); | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     setValue(JSON.stringify(log?.log ?? {}, null, 2)); | ||||||
|  |   }, [log]); | ||||||
|  |  | ||||||
|  |   if (!log) return null; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Modal | ||||||
|  |       isOpen={modalProps.isOpen} | ||||||
|  |       onClose={modalProps.onClose} | ||||||
|  |       title={t('devices.logs_one')} | ||||||
|  |       topRightButtons={ | ||||||
|  |         <Button onClick={onCopy} size="md" colorScheme="teal"> | ||||||
|  |           {hasCopied ? `${t('common.copied')}!` : t('common.copy')} | ||||||
|  |         </Button> | ||||||
|  |       } | ||||||
|  |     > | ||||||
|  |       <Box> | ||||||
|  |         <Heading size="sm"> | ||||||
|  |           <FormattedDate date={log.recorded} /> | ||||||
|  |         </Heading> | ||||||
|  |         <Heading size="sm"> | ||||||
|  |           {t('controller.devices.severity')}: {log.severity} | ||||||
|  |         </Heading> | ||||||
|  |         <Heading size="sm"> | ||||||
|  |           {t('controller.devices.config_id')}: {log.UUID} | ||||||
|  |         </Heading> | ||||||
|  |         <Code whiteSpace="pre-line" mt={2}> | ||||||
|  |           {log.log} | ||||||
|  |         </Code> | ||||||
|  |       </Box> | ||||||
|  |     </Modal> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default DetailedLogViewModal; | ||||||
| @@ -16,7 +16,7 @@ const LogHistory = ({ serialNumber }: Props) => { | |||||||
|   const { t } = useTranslation(); |   const { 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 }); |   const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit, logType: 0 }); | ||||||
|  |  | ||||||
|   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} /> |           <DeleteLogModal serialNumber={serialNumber} logType={0} /> | ||||||
|           <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,6 +1,8 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box } from '@chakra-ui/react'; | import { Box, IconButton, Text, useDisclosure } from '@chakra-ui/react'; | ||||||
|  | import { MagnifyingGlass } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { 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'; | ||||||
| @@ -8,18 +10,49 @@ import { Column } from 'models/Table'; | |||||||
| type Props = { | type Props = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
|   limit: number; |   limit: number; | ||||||
|  |   logType: 0 | 1; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const useDeviceLogsTable = ({ serialNumber, limit }: Props) => { | const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const getLogs = useGetDeviceLogs({ serialNumber, limit }); |   const getLogs = useGetDeviceLogs({ serialNumber, limit, logType }); | ||||||
|  |   const modalProps = useDisclosure(); | ||||||
|  |   const [log, setLog] = React.useState<DeviceLog | undefined>(); | ||||||
|   const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>(); |   const [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> | ||||||
| @@ -65,6 +98,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => { | |||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'log', |         accessor: 'log', | ||||||
|         customWidth: '35px', |         customWidth: '35px', | ||||||
|  |         Cell: (v) => logCell(v.cell.row.original), | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -85,6 +119,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => { | |||||||
|     getCustomLogs, |     getCustomLogs, | ||||||
|     time, |     time, | ||||||
|     setTime, |     setTime, | ||||||
|  |     modal: <DetailedLogViewModal modalProps={modalProps} log={log} />, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ 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'; | ||||||
|  |  | ||||||
| @@ -32,6 +33,9 @@ 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}> | ||||||
| @@ -51,10 +55,12 @@ 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,5 +1,16 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Flex, Heading, ListItem, Text, UnorderedList } from '@chakra-ui/react'; | import { | ||||||
|  |   Box, | ||||||
|  |   Flex, | ||||||
|  |   Heading, | ||||||
|  |   ListItem, | ||||||
|  |   Tag, | ||||||
|  |   TagLabel, | ||||||
|  |   TagLeftIcon, | ||||||
|  |   Text, | ||||||
|  |   Tooltip, | ||||||
|  |   UnorderedList, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import { LockSimple, LockSimpleOpen } from 'phosphor-react'; | import { 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'; | ||||||
| @@ -20,7 +31,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 }; | ||||||
| @@ -38,27 +49,52 @@ 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">{t('restrictions.title')}</Heading> |         <Heading size="md" my="auto" mr={2}> | ||||||
|  |           {t('restrictions.title')} | ||||||
|  |         </Heading> | ||||||
|  |         {getDevice.data?.restrictionDetails?.developer ? ( | ||||||
|  |           <Tooltip label={t('devices.restricted_overriden')} hasArrow> | ||||||
|  |             <Tag size="lg" colorScheme="green"> | ||||||
|  |               <TagLeftIcon boxSize="18px" as={LockSimpleOpen} /> | ||||||
|  |               <TagLabel>{t('devices.restrictions_overriden_title')}</TagLabel> | ||||||
|  |             </Tag> | ||||||
|  |           </Tooltip> | ||||||
|  |         ) : null} | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody p={0} display="block"> |       <CardBody p={0} display="block"> | ||||||
|         <Flex mt={2}> |         <Flex mt={2}> | ||||||
|           <Heading size="sm" mr={2}> |           <Heading size="sm" mr={2} my="auto"> | ||||||
|             {t('restrictions.countries')}: |             {t('restrictions.countries')}: | ||||||
|           </Heading> |           </Heading> | ||||||
|           <Text>{restrictions.country.join(', ')}</Text> |           <Text my="auto"> | ||||||
|  |             {restrictions.country?.length === 0 ? t('common.all') : restrictions.country.join(', ')} | ||||||
|  |           </Text> | ||||||
|         </Flex> |         </Flex> | ||||||
|         <Heading size="sm" mt={2}> |         <Flex mt={2}> | ||||||
|           {t('restrictions.key_verification')} |           <Heading size="sm" mt={2} my="auto"> | ||||||
|  |             {t('restrictions.key_verification')} {isMissingSigningInfo ? ':' : ''} | ||||||
|           </Heading> |           </Heading> | ||||||
|         <UnorderedList> |           {isMissingSigningInfo ? ( | ||||||
|  |             <Text my="auto" ml={2}> | ||||||
|  |               {t('common.none')} | ||||||
|  |             </Text> | ||||||
|  |           ) : null} | ||||||
|  |         </Flex> | ||||||
|  |         <UnorderedList hidden={isMissingSigningInfo}> | ||||||
|           <ListItem> |           <ListItem> | ||||||
|             {t('controller.wifi.vendor')}: {restrictions.key_info?.vendor} |             {t('controller.wifi.vendor')}:{' '} | ||||||
|  |             {restrictions.key_info?.vendor?.length > 0 ? restrictions.key_info?.vendor : '-'} | ||||||
|           </ListItem> |           </ListItem> | ||||||
|           <ListItem> |           <ListItem> | ||||||
|             {t('restrictions.algo')}: {restrictions.key_info?.algo} |             {t('restrictions.algo')}: {restrictions.key_info?.algo?.length > 0 ? restrictions.key_info?.algo : '-'} | ||||||
|           </ListItem> |           </ListItem> | ||||||
|         </UnorderedList> |         </UnorderedList> | ||||||
|         <Flex mt={2}> |         <Flex mt={2}> | ||||||
|   | |||||||
| @@ -9,10 +9,12 @@ import { | |||||||
|   Title, |   Title, | ||||||
|   Tooltip, |   Tooltip, | ||||||
|   Legend, |   Legend, | ||||||
|  |   Filler, | ||||||
|  |   ChartData, | ||||||
| } from 'chart.js'; | } from 'chart.js'; | ||||||
| import { Line } from 'react-chartjs-2'; | import { Line } from 'react-chartjs-2'; | ||||||
|  |  | ||||||
| ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); | ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler); | ||||||
|  |  | ||||||
| const getDivisionFactor = (maxBytes: number) => { | const getDivisionFactor = (maxBytes: number) => { | ||||||
|   if (maxBytes < 1024) { |   if (maxBytes < 1024) { | ||||||
| @@ -42,22 +44,28 @@ const InterfaceChart = ({ data }: Props) => { | |||||||
|  |  | ||||||
|   const { factor, unit } = getDivisionFactor(data.maxTx); |   const { factor, unit } = getDivisionFactor(data.maxTx); | ||||||
|  |  | ||||||
|   const points = { |   const points: ChartData<'line', string[], string> = { | ||||||
|     labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()), |     labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()), | ||||||
|     datasets: [ |     datasets: [ | ||||||
|       { |       { | ||||||
|         // 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), |         data: data.rx.map((tx) => (Math.floor((tx / factor) * 100) / 100).toFixed(2)), | ||||||
|           borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 |         borderColor: colorMode === 'light' ? 'rgba(99, 179, 237, 1)' : 'rgba(190, 227, 248, 1)', // blue-300 - blue-100 | ||||||
|           backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 |         backgroundColor: colorMode === 'light' ? 'rgba(99, 179, 237, 0.3)' : 'rgba(190, 227, 248, 0.3)', // blue-300 - blue-100 | ||||||
|  |         tension: 0.5, | ||||||
|  |         pointRadius: 0, | ||||||
|  |         fill: 'start', | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         // Real 'Rx', but shown as 'Tx' |         // Real 'Rx', but shown as 'Tx' | ||||||
|         label: 'Rx', |         label: 'Rx', | ||||||
|           data: data.tx.map((rx) => Math.floor((rx / factor) * 100) / 100), |         data: data.tx.map((rx) => (Math.floor((rx / factor) * 100) / 100).toFixed(2)), | ||||||
|           borderColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200 |         borderColor: colorMode === 'light' ? 'rgba(72, 187, 120, 1)' : 'rgba(154, 230, 180, 1)', // green-400 - green-200 | ||||||
|           backgroundColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200 |         backgroundColor: colorMode === 'light' ? 'rgba(72, 187, 120, 0.3)' : 'rgba(154, 230, 180, 0.3)', // green-400 - green-200 | ||||||
|  |         tension: 0.5, | ||||||
|  |         pointRadius: 0, | ||||||
|  |         fill: 'start', | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -34,20 +34,29 @@ const DeviceMemoryChart = ({ data }: Props) => { | |||||||
|       { |       { | ||||||
|         label: 'Free', |         label: 'Free', | ||||||
|         data: data.free.map((free) => Math.floor(free / 1024 / 1024)), |         data: data.free.map((free) => Math.floor(free / 1024 / 1024)), | ||||||
|         borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 |         borderColor: colorMode === 'light' ? 'rgb(99, 179, 237, 1)' : 'rgb(190, 227, 248, 1)', // blue-300 - blue-100 | ||||||
|         backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100 |         backgroundColor: colorMode === 'light' ? 'rgb(99, 179, 237, 0.3)' : 'rgb(190, 227, 248, 0.3)', // blue-300 - blue-100 | ||||||
|       }, |         tension: 0.5, | ||||||
|       { |         pointRadius: 0, | ||||||
|         label: 'Buffered', |         fill: '+1', | ||||||
|         data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)), |  | ||||||
|         borderColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200 |  | ||||||
|         backgroundColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200 |  | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         label: 'Cached', |         label: 'Cached', | ||||||
|         data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)), |         data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)), | ||||||
|         borderColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200 |         borderColor: colorMode === 'light' ? 'rgb(237, 100, 166, 1)' : 'rgb(251, 182, 206, 1)', // pink-400 - pink-200 | ||||||
|         backgroundColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200 |         backgroundColor: colorMode === 'light' ? 'rgb(237, 100, 166, 0.3)' : 'rgb(251, 182, 206, 0.3)', // pink-400 - pink-200 | ||||||
|  |         tension: 0.5, | ||||||
|  |         pointRadius: 0, | ||||||
|  |         fill: '+1', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         label: 'Buffered', | ||||||
|  |         data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)), | ||||||
|  |         borderColor: colorMode === 'light' ? 'rgb(255, 240, 31, 1)' : 'rgb(250, 240, 137, 1)', // yellow-400 - yellow-200 | ||||||
|  |         backgroundColor: colorMode === 'light' ? 'rgb(255, 240, 31, 0.3)' : 'rgb(250, 240, 137, 0.3)', // yellow-400 - yellow-200 | ||||||
|  |         tension: 0.5, | ||||||
|  |         pointRadius: 0, | ||||||
|  |         fill: 'origin', | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ const ViewLastStatsModal = ({ serialNumber }: Props) => { | |||||||
|     if (getLastStats.data) { |     if (getLastStats.data) { | ||||||
|       setValue(JSON.stringify(getLastStats.data, null, 2)); |       setValue(JSON.stringify(getLastStats.data, null, 2)); | ||||||
|     } |     } | ||||||
|   }, [getLastStats.data]); |   }, [getLastStats.data, isOpen]); | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Tooltip label={t('statistics.last_stats')}> |       <Tooltip label={t('statistics.last_stats')}> | ||||||
|   | |||||||
| @@ -52,8 +52,6 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|           <Heading size="md">{t('configurations.statistics')}</Heading> |           <Heading size="md">{t('configurations.statistics')}</Heading> | ||||||
|           <Spacer /> |           <Spacer /> | ||||||
|           <HStack> |           <HStack> | ||||||
|             <ViewLastStatsModal serialNumber={serialNumber} /> |  | ||||||
|             <StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} /> |  | ||||||
|             <Select value={selected} onChange={onSelectInterface}> |             <Select value={selected} onChange={onSelectInterface}> | ||||||
|               {parsedData?.interfaces |               {parsedData?.interfaces | ||||||
|                 ? Object.keys(parsedData.interfaces).map((v) => ( |                 ? Object.keys(parsedData.interfaces).map((v) => ( | ||||||
| @@ -64,6 +62,8 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|                 : null} |                 : null} | ||||||
|               <option value="memory">{t('statistics.memory')}</option> |               <option value="memory">{t('statistics.memory')}</option> | ||||||
|             </Select> |             </Select> | ||||||
|  |             <StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} /> | ||||||
|  |             <ViewLastStatsModal serialNumber={serialNumber} /> | ||||||
|             <RefreshButton |             <RefreshButton | ||||||
|               size="sm" |               size="sm" | ||||||
|               onClick={refresh} |               onClick={refresh} | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ type Props = { | |||||||
| export const useStatisticsCard = ({ serialNumber }: Props) => { | export const useStatisticsCard = ({ serialNumber }: Props) => { | ||||||
|   const [selected, setSelected] = React.useState('memory'); |   const [selected, setSelected] = React.useState('memory'); | ||||||
|   const [progress, setProgress] = React.useState(0); |   const [progress, setProgress] = React.useState(0); | ||||||
|  |   const [hasSelectedNew, setHasSelectedNew] = React.useState(false); | ||||||
|   const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>(); |   const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>(); | ||||||
|   const onProgressChange = React.useCallback((newProgress: number) => { |   const onProgressChange = React.useCallback((newProgress: number) => { | ||||||
|     setProgress(newProgress); |     setProgress(newProgress); | ||||||
| @@ -29,13 +30,17 @@ export const useStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => { |   const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => { | ||||||
|  |     setHasSelectedNew(true); | ||||||
|     setSelected(event.target.value); |     setSelected(event.target.value); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const parsedData = React.useMemo(() => { |   const parsedData = React.useMemo(() => { | ||||||
|     if (!getStats.data && !getCustomStats.data) return undefined; |     if (!getStats.data && !getCustomStats.data) return undefined; | ||||||
|  |  | ||||||
|     const data: Record<string, { tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number }> = {}; |     const data: Record< | ||||||
|  |       string, | ||||||
|  |       { tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number; removed?: boolean } | ||||||
|  |     > = {}; | ||||||
|     const memoryData = { |     const memoryData = { | ||||||
|       used: [] as number[], |       used: [] as number[], | ||||||
|       buffered: [] as number[], |       buffered: [] as number[], | ||||||
| @@ -56,7 +61,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|       if (index === 0) { |       if (index === 0) { | ||||||
|         let updated = false; |         let updated = false; | ||||||
|         for (const inter of stat.data.interfaces ?? []) { |         for (const inter of stat.data.interfaces ?? []) { | ||||||
|           if (!updated && selected === 'memory') { |           if (!hasSelectedNew && !updated && selected === 'memory') { | ||||||
|             updated = true; |             updated = true; | ||||||
|             setSelected(inter.name); |             setSelected(inter.name); | ||||||
|           } |           } | ||||||
| @@ -77,7 +82,10 @@ 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 (isInterUpstream) { |           if (inter['counters-aggregate']) { | ||||||
|  |             rx = inter['counters-aggregate'].rx_bytes; | ||||||
|  |             tx = inter['counters-aggregate'].tx_bytes; | ||||||
|  |           } else if (isInterUpstream) { | ||||||
|             for (const ssid of inter.ssids ?? []) { |             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; | ||||||
| @@ -97,6 +105,18 @@ export const useStatisticsCard = ({ serialNumber }: Props) => { | |||||||
|               maxRx: rxDelta, |               maxRx: rxDelta, | ||||||
|             }; |             }; | ||||||
|           else { |           else { | ||||||
|  |             if (data[inter.name] && !data[inter.name]?.removed && data[inter.name]?.recorded.length === 1) { | ||||||
|  |               data[inter.name]?.tx.shift(); | ||||||
|  |               data[inter.name]?.rx.shift(); | ||||||
|  |               data[inter.name]?.recorded.shift(); | ||||||
|  |               // @ts-ignore | ||||||
|  |               data[inter.name].maxRx = rxDelta; | ||||||
|  |               // @ts-ignore | ||||||
|  |               data[inter.name].maxTx = txDelta; | ||||||
|  |               // @ts-ignore | ||||||
|  |               data[inter.name].removed = true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             data[inter.name]?.rx.push(rxDelta); |             data[inter.name]?.rx.push(rxDelta); | ||||||
|             data[inter.name]?.tx.push(txDelta); |             data[inter.name]?.tx.push(txDelta); | ||||||
|             data[inter.name]?.recorded.push(stat.recorded); |             data[inter.name]?.recorded.push(stat.recorded); | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react'; | import { Box, Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react'; | ||||||
| import ReactCountryFlag from 'react-country-flag'; | import 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'; | ||||||
| @@ -90,11 +91,12 @@ 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> | ||||||
|   | |||||||
| @@ -9,12 +9,15 @@ import { | |||||||
|   Button, |   Button, | ||||||
|   Center, |   Center, | ||||||
|   Heading, |   Heading, | ||||||
|  |   IconButton, | ||||||
|   Spinner, |   Spinner, | ||||||
|  |   Tooltip, | ||||||
|   useClipboard, |   useClipboard, | ||||||
|   useColorMode, |   useColorMode, | ||||||
|   useDisclosure, |   useDisclosure, | ||||||
| } from '@chakra-ui/react'; | } from '@chakra-ui/react'; | ||||||
| import { JsonViewer } from '@textea/json-viewer'; | import { JsonViewer } from '@textea/json-viewer'; | ||||||
|  | import { ListDashes } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { RefreshButton } from 'components/Buttons/RefreshButton'; | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
| import FormattedDate from 'components/InformationDisplays/FormattedDate'; | import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||||
| @@ -43,9 +46,15 @@ const ViewCapabilitiesModal = ({ serialNumber }: Props) => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Button onClick={onOpen} colorScheme="pink" mr={2}> |       <Tooltip label={t('controller.devices.capabilities')} hasArrow> | ||||||
|         {t('controller.devices.capabilities')} |         <IconButton | ||||||
|       </Button> |           aria-label={t('controller.devices.capabilities')} | ||||||
|  |           icon={<ListDashes size={20} />} | ||||||
|  |           onClick={onOpen} | ||||||
|  |           colorScheme="pink" | ||||||
|  |           mr={2} | ||||||
|  |         /> | ||||||
|  |       </Tooltip> | ||||||
|       <Modal |       <Modal | ||||||
|         isOpen={isOpen} |         isOpen={isOpen} | ||||||
|         title={t('controller.devices.capabilities')} |         title={t('controller.devices.capabilities')} | ||||||
|   | |||||||
| @@ -7,11 +7,14 @@ import { | |||||||
|   AccordionPanel, |   AccordionPanel, | ||||||
|   Box, |   Box, | ||||||
|   Button, |   Button, | ||||||
|  |   IconButton, | ||||||
|  |   Tooltip, | ||||||
|   useClipboard, |   useClipboard, | ||||||
|   useColorMode, |   useColorMode, | ||||||
|   useDisclosure, |   useDisclosure, | ||||||
| } from '@chakra-ui/react'; | } from '@chakra-ui/react'; | ||||||
| import { JsonViewer } from '@textea/json-viewer'; | import { JsonViewer } from '@textea/json-viewer'; | ||||||
|  | import { Barcode } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { Modal } from 'components/Modals/Modal'; | import { Modal } from 'components/Modals/Modal'; | ||||||
| import { DeviceConfiguration } from 'models/Device'; | import { DeviceConfiguration } from 'models/Device'; | ||||||
| @@ -30,9 +33,14 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Button onClick={onOpen} isDisabled={!configuration} colorScheme="purple"> |       <Tooltip label={t('configurations.one')} hasArrow> | ||||||
|         {t('configurations.one')} |         <IconButton | ||||||
|       </Button> |           aria-label={t('configurations.one')} | ||||||
|  |           icon={<Barcode size={20} />} | ||||||
|  |           onClick={onOpen} | ||||||
|  |           colorScheme="purple" | ||||||
|  |         /> | ||||||
|  |       </Tooltip> | ||||||
|       <Modal |       <Modal | ||||||
|         isOpen={isOpen} |         isOpen={isOpen} | ||||||
|         title={t('configurations.one')} |         title={t('configurations.one')} | ||||||
|   | |||||||
| @@ -1,6 +1,13 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { | ||||||
|  |   AlertDialog, | ||||||
|  |   AlertDialogBody, | ||||||
|  |   AlertDialogContent, | ||||||
|  |   AlertDialogFooter, | ||||||
|  |   AlertDialogHeader, | ||||||
|  |   AlertDialogOverlay, | ||||||
|   Box, |   Box, | ||||||
|  |   Button, | ||||||
|   Heading, |   Heading, | ||||||
|   HStack, |   HStack, | ||||||
|   Portal, |   Portal, | ||||||
| @@ -12,10 +19,13 @@ import { | |||||||
|   useBreakpoint, |   useBreakpoint, | ||||||
|   useColorModeValue, |   useColorModeValue, | ||||||
|   useDisclosure, |   useDisclosure, | ||||||
|  |   useToast, | ||||||
| } from '@chakra-ui/react'; | } from '@chakra-ui/react'; | ||||||
| import { Heart, HeartBreak, LockSimple, WifiHigh, WifiSlash } from 'phosphor-react'; | import axios from 'axios'; | ||||||
|  | import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import Masonry from 'react-masonry-css'; | import Masonry from 'react-masonry-css'; | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
| import DeviceDetails from './Details'; | import DeviceDetails from './Details'; | ||||||
| import DeviceLogsCard from './LogsCard'; | import DeviceLogsCard from './LogsCard'; | ||||||
| import DeviceNotes from './Notes'; | import DeviceNotes from './Notes'; | ||||||
| @@ -23,6 +33,7 @@ import RestrictionsCard from './RestrictionsCard'; | |||||||
| import DeviceStatisticsCard from './StatisticsCard'; | import DeviceStatisticsCard from './StatisticsCard'; | ||||||
| import DeviceSummary from './Summary'; | import DeviceSummary from './Summary'; | ||||||
| import WifiAnalysisCard from './WifiAnalysis'; | import WifiAnalysisCard from './WifiAnalysis'; | ||||||
|  | import { DeleteButton } from 'components/Buttons/DeleteButton'; | ||||||
| import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown'; | import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown'; | ||||||
| import { RefreshButton } from 'components/Buttons/RefreshButton'; | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| @@ -33,11 +44,12 @@ 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'; | ||||||
| import { WifiScanModal } from 'components/Modals/WifiScanModal'; | import { WifiScanModal } from 'components/Modals/WifiScanModal'; | ||||||
| import { useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; | import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| @@ -45,10 +57,17 @@ type Props = { | |||||||
|  |  | ||||||
| const DevicePageWrapper = ({ serialNumber }: Props) => { | const DevicePageWrapper = ({ serialNumber }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|   const breakpoint = useBreakpoint(); |   const breakpoint = useBreakpoint(); | ||||||
|  |   const cancelRef = React.useRef(null); | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |   const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({ | ||||||
|  |     serialNumber, | ||||||
|  |   }); | ||||||
|   const getDevice = useGetDevice({ serialNumber }); |   const getDevice = useGetDevice({ serialNumber }); | ||||||
|   const getStatus = useGetDeviceStatus({ serialNumber }); |   const getStatus = useGetDeviceStatus({ serialNumber }); | ||||||
|   const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 }); |   const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 }); | ||||||
|  |   const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure(); | ||||||
|   const scanModalProps = useDisclosure(); |   const scanModalProps = useDisclosure(); | ||||||
|   const resetModalProps = useDisclosure(); |   const resetModalProps = useDisclosure(); | ||||||
|   const eventQueueProps = useDisclosure(); |   const eventQueueProps = useDisclosure(); | ||||||
| @@ -56,7 +75,40 @@ 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 handleDeleteClick = () => | ||||||
|  |     deleteDevice(serialNumber, { | ||||||
|  |       onSuccess: () => { | ||||||
|  |         toast({ | ||||||
|  |           id: `delete-device-success-${serialNumber}`, | ||||||
|  |           title: t('common.success'), | ||||||
|  |           status: 'success', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|  |         }); | ||||||
|  |         navigate('/devices'); | ||||||
|  |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         if (axios.isAxiosError(e)) { | ||||||
|  |           toast({ | ||||||
|  |             id: `delete-device-error-${serialNumber}`, | ||||||
|  |             title: t('common.error'), | ||||||
|  |             description: e.response?.data?.ErrorDescription, | ||||||
|  |             status: 'error', | ||||||
|  |             duration: 5000, | ||||||
|  |             isClosable: true, | ||||||
|  |             position: 'top-right', | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|   const connectedTag = React.useMemo(() => { |   const connectedTag = React.useMemo(() => { | ||||||
|     if (!getStatus.data) return null; |     if (!getStatus.data) return null; | ||||||
|  |  | ||||||
| @@ -100,9 +152,28 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|     ); |     ); | ||||||
|   }, [getStatus.data, getHealth.data]); |   }, [getStatus.data, getHealth.data]); | ||||||
|  |  | ||||||
|   // Sticky-top styles |   const restrictedTag = React.useMemo(() => { | ||||||
|   const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; |     if (!getDevice.data || !getDevice.data.restrictedDevice) return null; | ||||||
|   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(); | ||||||
| @@ -119,16 +190,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|               <Heading size="md">{serialNumber}</Heading> |               <Heading size="md">{serialNumber}</Heading> | ||||||
|               {connectedTag} |               {connectedTag} | ||||||
|               {healthTag} |               {healthTag} | ||||||
|               {getDevice.data?.restrictedDevice && ( |               {restrictedTag} | ||||||
|                 <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}> | ||||||
|               {breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />} |               {breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />} | ||||||
|  |               <DeleteButton isCompact onClick={onDeleteOpen} /> | ||||||
|               {getDevice?.data && ( |               {getDevice?.data && ( | ||||||
|                 <DeviceActionDropdown |                 <DeviceActionDropdown | ||||||
|                   // @ts-ignore |                   // @ts-ignore | ||||||
| @@ -142,6 +209,7 @@ 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 | ||||||
|                 /> |                 /> | ||||||
| @@ -172,16 +240,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|                 <Heading size="md">{serialNumber}</Heading> |                 <Heading size="md">{serialNumber}</Heading> | ||||||
|                 {connectedTag} |                 {connectedTag} | ||||||
|                 {healthTag} |                 {healthTag} | ||||||
|                 {getDevice.data?.restrictedDevice && ( |                 {restrictedTag} | ||||||
|                   <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}> | ||||||
|                 <DeviceSearchBar /> |                 <DeviceSearchBar /> | ||||||
|  |                 <DeleteButton isCompact onClick={onDeleteOpen} /> | ||||||
|                 {getDevice?.data && ( |                 {getDevice?.data && ( | ||||||
|                   <DeviceActionDropdown |                   <DeviceActionDropdown | ||||||
|                     // @ts-ignore |                     // @ts-ignore | ||||||
| @@ -194,8 +258,10 @@ 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" | ||||||
|  |                     isCompact | ||||||
|                   /> |                   /> | ||||||
|                 )} |                 )} | ||||||
|                 <RefreshButton |                 <RefreshButton | ||||||
| @@ -210,6 +276,24 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|           </Card> |           </Card> | ||||||
|         </Portal> |         </Portal> | ||||||
|       )} |       )} | ||||||
|  |       <AlertDialog isOpen={isDeleteOpen} leastDestructiveRef={cancelRef} onClose={onDeleteClose}> | ||||||
|  |         <AlertDialogOverlay> | ||||||
|  |           <AlertDialogContent> | ||||||
|  |             <AlertDialogHeader fontSize="lg" fontWeight="bold"> | ||||||
|  |               {t('crud.delete')} {serialNumber} | ||||||
|  |             </AlertDialogHeader> | ||||||
|  |             <AlertDialogBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</AlertDialogBody> | ||||||
|  |             <AlertDialogFooter> | ||||||
|  |               <Button colorScheme="gray" mr="1" onClick={onDeleteClose} ref={cancelRef}> | ||||||
|  |                 {t('common.cancel')} | ||||||
|  |               </Button> | ||||||
|  |               <Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}> | ||||||
|  |                 {t('common.yes')} | ||||||
|  |               </Button> | ||||||
|  |             </AlertDialogFooter> | ||||||
|  |           </AlertDialogContent> | ||||||
|  |         </AlertDialogOverlay> | ||||||
|  |       </AlertDialog> | ||||||
|       <WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} /> |       <WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} /> | ||||||
|       <FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} /> |       <FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} /> | ||||||
|       <FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} /> |       <FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} /> | ||||||
| @@ -217,6 +301,7 @@ 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,27 +1,16 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { FormControl, FormErrorMessage, FormLabel, Input, Textarea, useDisclosure, useToast } from '@chakra-ui/react'; | ||||||
|   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>(''); | ||||||
| @@ -43,41 +32,50 @@ 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; | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   const onOpen = () => { | ||||||
|     setSerialNumber(''); |     setSerialNumber(''); | ||||||
|     setReason(''); |     setReason(''); | ||||||
|   }, [modalProps.isOpen]); |     modalProps.onOpen(); | ||||||
|  |     setTimeout(() => { | ||||||
|  |       initialRef.current?.focus(); | ||||||
|  |     }, 200); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <CreateButton onClick={modalProps.onOpen} isCompact ml={2} /> |       <CreateButton onClick={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 type="text" onChange={(e) => setSerialNumber(e.target.value)} value={serialNumber} w="140px" /> |             <Input | ||||||
|  |               type="text" | ||||||
|  |               onChange={(e) => setSerialNumber(e.target.value)} | ||||||
|  |               value={serialNumber} | ||||||
|  |               w="140px" | ||||||
|  |               ref={initialRef} | ||||||
|  |             /> | ||||||
|             <FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage> |             <FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage> | ||||||
|           </FormControl> |           </FormControl> | ||||||
|           <FormControl> |           <FormControl> | ||||||
|   | |||||||
| @@ -1,5 +1,22 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { useColorMode } from '@chakra-ui/react'; | import { CopyIcon } from '@chakra-ui/icons'; | ||||||
|  | import { | ||||||
|  |   useColorMode, | ||||||
|  |   Alert, | ||||||
|  |   AlertDescription, | ||||||
|  |   AlertTitle, | ||||||
|  |   Box, | ||||||
|  |   Center, | ||||||
|  |   Heading, | ||||||
|  |   IconButton, | ||||||
|  |   Link, | ||||||
|  |   ListItem, | ||||||
|  |   Spinner, | ||||||
|  |   UnorderedList, | ||||||
|  |   useClipboard, | ||||||
|  |   Tooltip as ChakraTooltip, | ||||||
|  |   useDisclosure, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import { | import { | ||||||
|   Chart as ChartJS, |   Chart as ChartJS, | ||||||
|   CategoryScale, |   CategoryScale, | ||||||
| @@ -11,20 +28,57 @@ import { | |||||||
|   Legend, |   Legend, | ||||||
|   ChartData, |   ChartData, | ||||||
|   ArcElement, |   ArcElement, | ||||||
|  |   ChartTypeRegistry, | ||||||
|  |   ScatterDataPoint, | ||||||
|  |   BubbleDataPoint, | ||||||
| } from 'chart.js'; | } from 'chart.js'; | ||||||
| import { Pie } from 'react-chartjs-2'; | import { Pie, getElementAtEvent } from 'react-chartjs-2'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import GraphStatDisplay from 'components/Containers/GraphStatDisplay'; | import GraphStatDisplay from 'components/Containers/GraphStatDisplay'; | ||||||
|  | import { Modal } from 'components/Modals/Modal'; | ||||||
| import { ControllerDashboardHealth } from 'hooks/Network/Controller'; | import { ControllerDashboardHealth } from 'hooks/Network/Controller'; | ||||||
|  | import { useGetDevicesWithHealthBetween } from 'hooks/Network/HealthChecks'; | ||||||
|  | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
|  | const LABEL_TO_LIMITS = { | ||||||
|  |   '100%': { lowerLimit: 100, upperLimit: 100, label: 'With 100% Health' }, | ||||||
|  |   '>90%': { lowerLimit: 90, upperLimit: 99, label: 'Between 90% and 99%' }, | ||||||
|  |   '>60%': { lowerLimit: 60, upperLimit: 89, label: 'Between 60% and 89%' }, | ||||||
|  |   '<=60%': { lowerLimit: 0, upperLimit: 59, label: 'Between 0% and 59%' }, | ||||||
|  | } as const; | ||||||
|  |  | ||||||
| ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement); | ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement); | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   data: ControllerDashboardHealth[]; |   data: ControllerDashboardHealth[]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const OverallHealthPieChart = ({ data }: Props) => { | const OverallHealthPieChart = ({ data }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { colorMode } = useColorMode(); |   const { colorMode } = useColorMode(); | ||||||
|  |   const { hasCopied, onCopy, setValue } = useClipboard(''); | ||||||
|  |   const modalProps = useDisclosure(); | ||||||
|  |   const [deviceCategory, setDeviceCategory] = React.useState<{ lowerLimit: number; upperLimit: number; label: string }>( | ||||||
|  |     LABEL_TO_LIMITS['100%'], | ||||||
|  |   ); | ||||||
|  |   const serialNumbersFromCategory = useGetDevicesWithHealthBetween(deviceCategory); | ||||||
|  |   const chartRef = | ||||||
|  |     React.useRef<ChartJS<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown>>( | ||||||
|  |       null, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |   const onClick = (event: React.MouseEvent<HTMLCanvasElement>) => { | ||||||
|  |     if (chartRef.current) { | ||||||
|  |       const element = getElementAtEvent(chartRef.current, event)?.[0]; | ||||||
|  |       if (element && element.index !== undefined) { | ||||||
|  |         const label = chartRef.current?.data?.labels?.[element.index] as keyof typeof LABEL_TO_LIMITS | undefined; | ||||||
|  |         if (label && LABEL_TO_LIMITS[label]) { | ||||||
|  |           setDeviceCategory(LABEL_TO_LIMITS[label]); | ||||||
|  |           modalProps.onOpen(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => { |   const parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => { | ||||||
|     const totalDevices = data.reduce( |     const totalDevices = data.reduce( | ||||||
| @@ -85,7 +139,7 @@ const OverallHealthPieChart = ({ data }: Props) => { | |||||||
|     } |     } | ||||||
|     if (totalDevices['<60%'] > 0) { |     if (totalDevices['<60%'] > 0) { | ||||||
|       newData.push(totalDevices['<60%']); |       newData.push(totalDevices['<60%']); | ||||||
|       labels.push('<60%'); |       labels.push('<=60%'); | ||||||
|       const color = colorMode === 'light' ? '#FC8181' : '#FC8181'; |       const color = colorMode === 'light' ? '#FC8181' : '#FC8181'; | ||||||
|       backgroundColor.push(color); |       backgroundColor.push(color); | ||||||
|       borderColor.push(color); |       borderColor.push(color); | ||||||
| @@ -105,14 +159,30 @@ const OverallHealthPieChart = ({ data }: Props) => { | |||||||
|     }; |     }; | ||||||
|   }, [data, colorMode]); |   }, [data, colorMode]); | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (serialNumbersFromCategory.data) setValue(serialNumbersFromCategory.data.join(',')); | ||||||
|  |   }, [serialNumbersFromCategory.data]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|  |     <> | ||||||
|       <GraphStatDisplay |       <GraphStatDisplay | ||||||
|         title={t('controller.dashboard.overall_health')} |         title={t('controller.dashboard.overall_health')} | ||||||
|         explanation={t('controller.dashboard.overall_health_explanation_pie')} |         explanation={t('controller.dashboard.overall_health_explanation_pie')} | ||||||
|         chart={ |         chart={ | ||||||
|           <Pie |           <Pie | ||||||
|  |             // @ts-ignore | ||||||
|  |             ref={chartRef} | ||||||
|             data={parsedData} |             data={parsedData} | ||||||
|  |             onClick={onClick} | ||||||
|             options={{ |             options={{ | ||||||
|  |               onHover: (e, elements) => { | ||||||
|  |                 const element = e.native?.target as unknown as { style: { cursor: string } }; | ||||||
|  |                 if (element && elements.length > 0) { | ||||||
|  |                   element.style.cursor = 'pointer'; | ||||||
|  |                 } else if (element) { | ||||||
|  |                   element.style.cursor = 'default'; | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|               plugins: { |               plugins: { | ||||||
|                 legend: { |                 legend: { | ||||||
|                   position: 'top' as const, |                   position: 'top' as const, | ||||||
| @@ -137,6 +207,60 @@ const OverallHealthPieChart = ({ data }: Props) => { | |||||||
|           /> |           /> | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|  |       <Modal | ||||||
|  |         title={t('controller.dashboard.overall_health')} | ||||||
|  |         {...modalProps} | ||||||
|  |         options={{ | ||||||
|  |           modalSize: 'sm', | ||||||
|  |         }} | ||||||
|  |         topRightButtons={ | ||||||
|  |           <ChakraTooltip label={hasCopied ? `${t('common.copied')}!` : t('common.copy')} hasArrow closeOnClick={false}> | ||||||
|  |             <IconButton | ||||||
|  |               aria-label={t('common.copy')} | ||||||
|  |               icon={<CopyIcon h={5} w={5} />} | ||||||
|  |               onClick={onCopy} | ||||||
|  |               colorScheme="teal" | ||||||
|  |               hidden={!serialNumbersFromCategory.data || serialNumbersFromCategory.data.length === 0} | ||||||
|  |             /> | ||||||
|  |           </ChakraTooltip> | ||||||
|  |         } | ||||||
|  |       > | ||||||
|  |         {serialNumbersFromCategory.isFetching ? ( | ||||||
|  |           <Center my={8}> | ||||||
|  |             <Spinner size="xl" /> | ||||||
|  |           </Center> | ||||||
|  |         ) : ( | ||||||
|  |           <Box> | ||||||
|  |             {serialNumbersFromCategory.error ? ( | ||||||
|  |               <Alert mb={4} status="error"> | ||||||
|  |                 <AlertTitle>{t('common.error')}</AlertTitle> | ||||||
|  |                 <AlertDescription> | ||||||
|  |                   {(serialNumbersFromCategory.error as AxiosError).response?.data.ErrorDescription} | ||||||
|  |                 </AlertDescription> | ||||||
|  |               </Alert> | ||||||
|  |             ) : null} | ||||||
|  |             {serialNumbersFromCategory.data ? ( | ||||||
|  |               <Box> | ||||||
|  |                 <Heading size="md" mb={4}> | ||||||
|  |                   {serialNumbersFromCategory.data.length} {t('devices.title')} {deviceCategory.label} | ||||||
|  |                 </Heading> | ||||||
|  |                 <Box maxH="70vh" overflowY="auto" overflowX="hidden"> | ||||||
|  |                   <UnorderedList pl={2}> | ||||||
|  |                     {serialNumbersFromCategory.data | ||||||
|  |                       .sort((a, b) => a.localeCompare(b)) | ||||||
|  |                       .map((device) => ( | ||||||
|  |                         <ListItem key={device} fontFamily="mono"> | ||||||
|  |                           <Link href={`#/devices/${device}`}>{device}</Link> | ||||||
|  |                         </ListItem> | ||||||
|  |                       ))} | ||||||
|  |                   </UnorderedList> | ||||||
|  |                 </Box> | ||||||
|  |               </Box> | ||||||
|  |             ) : null} | ||||||
|  |           </Box> | ||||||
|  |         )} | ||||||
|  |       </Modal> | ||||||
|  |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ interface Props { | |||||||
|   onOpenConfigureModal: (serialNumber: string) => void; |   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> = ({ | ||||||
| @@ -47,6 +48,7 @@ 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(); | ||||||
| @@ -102,6 +104,7 @@ 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,5 +1,5 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Button, Heading, Image, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react'; | import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react'; | ||||||
| import { LockSimple } from 'phosphor-react'; | import { 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'; | ||||||
| @@ -21,6 +21,7 @@ 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'; | ||||||
| @@ -60,6 +61,7 @@ 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 +100,9 @@ const DeviceListCard = () => { | |||||||
|     setSerialNumber(serial); |     setSerialNumber(serial); | ||||||
|     configureModalProps.onOpen(); |     configureModalProps.onOpen(); | ||||||
|   }; |   }; | ||||||
|  |   const onOpenReboot = (serial: string) => { | ||||||
|   const goToSerial = (serial: string) => () => { |     setSerialNumber(serial); | ||||||
|     navigate(`/devices/${serial}`); |     rebootModalProps.onOpen(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const badgeCell = React.useCallback( |   const badgeCell = React.useCallback( | ||||||
| @@ -160,9 +162,9 @@ const DeviceListCard = () => { | |||||||
|  |  | ||||||
|   const serialCell = React.useCallback( |   const serialCell = React.useCallback( | ||||||
|     (device: DeviceWithStatus) => ( |     (device: DeviceWithStatus) => ( | ||||||
|       <Button variant="link" onClick={goToSerial(device.serialNumber)} fontSize="sm"> |       <Link href={`#/devices/${device.serialNumber}`} fontSize="sm" my="auto" pt={1}> | ||||||
|         <pre>{device.serialNumber}</pre> |         <pre>{device.serialNumber}</pre> | ||||||
|       </Button> |       </Link> | ||||||
|     ), |     ), | ||||||
|     [], |     [], | ||||||
|   ); |   ); | ||||||
| @@ -216,6 +218,7 @@ const DeviceListCard = () => { | |||||||
|         onOpenConfigureModal={onOpenConfigure} |         onOpenConfigureModal={onOpenConfigure} | ||||||
|         onOpenTelemetryModal={onOpenTelemetry} |         onOpenTelemetryModal={onOpenTelemetry} | ||||||
|         onOpenScriptModal={scriptModal.openModal} |         onOpenScriptModal={scriptModal.openModal} | ||||||
|  |         onOpenRebootModal={onOpenReboot} | ||||||
|       /> |       /> | ||||||
|     ), |     ), | ||||||
|     [], |     [], | ||||||
| @@ -251,6 +254,7 @@ const DeviceListCard = () => { | |||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'firmware', |         accessor: 'firmware', | ||||||
|         Cell: (v) => firmwareCell(v.cell.row.original), |         Cell: (v) => firmwareCell(v.cell.row.original), | ||||||
|  |         stopPropagation: true, | ||||||
|         customWidth: '50px', |         customWidth: '50px', | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
| @@ -388,7 +392,7 @@ const DeviceListCard = () => { | |||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody p={4}> |       <CardBody p={4}> | ||||||
|         <Box overflowX="auto" w="100%"> |         <Box overflowX="auto" w="100%"> | ||||||
|           <DataTable |           <DataTable<DeviceWithStatus> | ||||||
|             columns={ |             columns={ | ||||||
|               columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as { |               columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as { | ||||||
|                 id: string; |                 id: string; | ||||||
| @@ -406,7 +410,8 @@ const DeviceListCard = () => { | |||||||
|             // @ts-ignore |             // @ts-ignore | ||||||
|             setPageInfo={setPageInfo} |             setPageInfo={setPageInfo} | ||||||
|             saveSettingsId="gateway.devices.table" |             saveSettingsId="gateway.devices.table" | ||||||
|             minHeight="600px" |             onRowClick={(device) => navigate(`devices/${device.serialNumber}`)} | ||||||
|  |             isRowClickable={() => true} | ||||||
|           /> |           /> | ||||||
|         </Box> |         </Box> | ||||||
|       </CardBody> |       </CardBody> | ||||||
| @@ -417,6 +422,7 @@ 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,6 +191,7 @@ 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> | ||||||
| @@ -202,18 +203,15 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => { | |||||||
|             isDisabled={!isEditingDescription} |             isDisabled={!isEditingDescription} | ||||||
|           /> |           /> | ||||||
|         </FormControl> |         </FormControl> | ||||||
|         <FormControl> |       </SimpleGrid> | ||||||
|  |       <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 |                   <IconButton aria-label={`${t('crud.add')} ${t('common.note')}`} size="sm" icon={<Plus size={20} />} /> | ||||||
|                       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 /> | ||||||
| @@ -239,11 +237,17 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => { | |||||||
|             )} |             )} | ||||||
|           </Popover> |           </Popover> | ||||||
|         </FormLabel> |         </FormLabel> | ||||||
|           <Box overflowX="auto" overflowY="auto" maxH="400px"> |  | ||||||
|             <DataTable columns={columns as Column<object>[]} data={notes} obj={t('common.notes')} minHeight="200px" /> |  | ||||||
|           </Box> |  | ||||||
|       </FormControl> |       </FormControl> | ||||||
|       </SimpleGrid> |       <Box overflowX="auto" overflowY="auto" maxH="400px" mb={4}> | ||||||
|  |         <DataTable | ||||||
|  |           columns={columns as Column<object>[]} | ||||||
|  |           data={notes} | ||||||
|  |           obj={t('common.notes')} | ||||||
|  |           minHeight="200px" | ||||||
|  |           showAllRows | ||||||
|  |           hideControls | ||||||
|  |         /> | ||||||
|  |       </Box> | ||||||
|     </Modal> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								src/pages/Firmware/List/UpdateDbButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/pages/Firmware/List/UpdateDbButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { | ||||||
|  |   Alert, | ||||||
|  |   AlertDescription, | ||||||
|  |   AlertIcon, | ||||||
|  |   AlertTitle, | ||||||
|  |   Box, | ||||||
|  |   Button, | ||||||
|  |   Center, | ||||||
|  |   IconButton, | ||||||
|  |   Tag, | ||||||
|  |   TagLabel, | ||||||
|  |   Text, | ||||||
|  |   Tooltip, | ||||||
|  |   useDisclosure, | ||||||
|  |   useToast, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import axios from 'axios'; | ||||||
|  | import { Database } from 'phosphor-react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||||
|  | import { Modal } from 'components/Modals/Modal'; | ||||||
|  | import { useGetFirmwareDbUpdate, useUpdateFirmwareDb } from 'hooks/Network/Firmware'; | ||||||
|  |  | ||||||
|  | const UpdateDbButton = () => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|  |   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||||
|  |   const updateDb = useUpdateFirmwareDb(); | ||||||
|  |   const getLastUpdate = useGetFirmwareDbUpdate(); | ||||||
|  |  | ||||||
|  |   const onUpdateClick = async () => { | ||||||
|  |     updateDb.mutate(undefined, { | ||||||
|  |       onSuccess: () => { | ||||||
|  |         toast({ | ||||||
|  |           id: `firmware-db-update-success`, | ||||||
|  |           title: t('common.success'), | ||||||
|  |           description: t('firmware.started_db_update'), | ||||||
|  |           status: 'success', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         if (axios.isAxiosError(e)) { | ||||||
|  |           toast({ | ||||||
|  |             id: `firmware-db-update-error`, | ||||||
|  |             title: t('common.error'), | ||||||
|  |             description: e?.response?.data?.ErrorDescription, | ||||||
|  |             status: 'error', | ||||||
|  |             duration: 5000, | ||||||
|  |             isClosable: true, | ||||||
|  |             position: 'top-right', | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Tooltip label={t('firmware.last_db_update_title')}> | ||||||
|  |         <IconButton | ||||||
|  |           aria-label={t('firmware.last_db_update_title')} | ||||||
|  |           colorScheme="teal" | ||||||
|  |           icon={<Database size={20} />} | ||||||
|  |           onClick={onOpen} | ||||||
|  |         /> | ||||||
|  |       </Tooltip> | ||||||
|  |       <Modal | ||||||
|  |         isOpen={isOpen} | ||||||
|  |         onClose={onClose} | ||||||
|  |         title={t('firmware.last_db_update_modal')} | ||||||
|  |         tags={ | ||||||
|  |           <Tag colorScheme="blue" size="lg"> | ||||||
|  |             <TagLabel display="flex"> | ||||||
|  |               <Text mr={1}>Last Update:</Text> | ||||||
|  |               <FormattedDate date={getLastUpdate.data?.lastUpdateTime} /> | ||||||
|  |             </TagLabel> | ||||||
|  |           </Tag> | ||||||
|  |         } | ||||||
|  |       > | ||||||
|  |         <Box> | ||||||
|  |           <Alert status="warning"> | ||||||
|  |             <AlertIcon /> | ||||||
|  |             <Box> | ||||||
|  |               <AlertTitle>{t('common.warning')}</AlertTitle> | ||||||
|  |               <AlertDescription>{t('firmware.db_update_warning')}</AlertDescription> | ||||||
|  |             </Box> | ||||||
|  |           </Alert> | ||||||
|  |           <Center my={4}> | ||||||
|  |             <Button | ||||||
|  |               colorScheme="red" | ||||||
|  |               leftIcon={<Database size={20} />} | ||||||
|  |               onClick={onUpdateClick} | ||||||
|  |               isLoading={updateDb.isLoading} | ||||||
|  |             > | ||||||
|  |               {t('firmware.start_db_update')} | ||||||
|  |             </Button> | ||||||
|  |           </Center> | ||||||
|  |         </Box> | ||||||
|  |       </Modal> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default UpdateDbButton; | ||||||
| @@ -11,7 +11,15 @@ const UriCell = ({ uri }: Props) => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box display="flex"> |     <Box display="flex"> | ||||||
|       <Button onClick={copy.onCopy} size="xs" colorScheme="teal" mr={2}> |       <Button | ||||||
|  |         onClick={(e) => { | ||||||
|  |           copy.onCopy(); | ||||||
|  |           e.stopPropagation(); | ||||||
|  |         }} | ||||||
|  |         size="xs" | ||||||
|  |         colorScheme="teal" | ||||||
|  |         mr={2} | ||||||
|  |       > | ||||||
|         {copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')} |         {copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')} | ||||||
|       </Button> |       </Button> | ||||||
|       <Text my="auto">{uri}</Text> |       <Text my="auto">{uri}</Text> | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ 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'; | ||||||
| @@ -143,6 +144,7 @@ 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(); | ||||||
| @@ -156,13 +158,14 @@ const FirmwareListTable = () => { | |||||||
|       <CardBody p={4}> |       <CardBody p={4}> | ||||||
|         <Box overflowX="auto" w="100%"> |         <Box overflowX="auto" w="100%"> | ||||||
|           <LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}> |           <LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}> | ||||||
|             <DataTable |             <DataTable<Firmware> | ||||||
|               columns={columns as Column<object>[]} |               columns={columns} | ||||||
|               saveSettingsId="firmware.table" |               saveSettingsId="firmware.table" | ||||||
|               data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []} |               data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []} | ||||||
|               obj={t('analytics.firmware')} |               obj={t('analytics.firmware')} | ||||||
|               minHeight="200px" |               minHeight="200px" | ||||||
|               sortBy={[{ id: 'imageDate', desc: true }]} |               sortBy={[{ id: 'imageDate', desc: true }]} | ||||||
|  |               onRowClick={(firmw) => handleViewDetailsClick(firmw)()} | ||||||
|             /> |             /> | ||||||
|           </LoadingOverlay> |           </LoadingOverlay> | ||||||
|         </Box> |         </Box> | ||||||
|   | |||||||
| @@ -48,7 +48,10 @@ 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 === 4) return t('login.waiting_for_email_verification'); |     if (loginError?.response?.data?.ErrorCode === 5) return t('login.waiting_for_email_verification'); | ||||||
|  |     if (loginError?.response?.data?.ErrorCode === 15) { | ||||||
|  |       return t('login.suspended_error'); | ||||||
|  |     } | ||||||
|     return t('login.invalid_credentials'); |     return t('login.invalid_credentials'); | ||||||
|   }, [t, error]); |   }, [t, error]); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react'; | import { Box, Flex, HStack, IconButton, Select, Spacer, Table, Text, Th, Thead, Tooltip, Tr } from '@chakra-ui/react'; | ||||||
| import { Download } from 'phosphor-react'; | import { Download } from 'phosphor-react'; | ||||||
| import { CSVLink } from 'react-csv'; | import { CSVLink } from 'react-csv'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @@ -127,9 +127,9 @@ const LogsCard = () => { | |||||||
|             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} |             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} | ||||||
|             data={downloadableLogs as object[]} |             data={downloadableLogs as object[]} | ||||||
|           > |           > | ||||||
|             <Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> |             <Tooltip label={t('logs.export')} hasArrow> | ||||||
|               {t('logs.export')} |               <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" /> | ||||||
|             </Button> |             </Tooltip> | ||||||
|           </CSVLink> |           </CSVLink> | ||||||
|         </HStack> |         </HStack> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|   | |||||||
| @@ -1,5 +1,19 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react'; | import { | ||||||
|  |   Badge, | ||||||
|  |   Box, | ||||||
|  |   Flex, | ||||||
|  |   HStack, | ||||||
|  |   IconButton, | ||||||
|  |   Select, | ||||||
|  |   Spacer, | ||||||
|  |   Table, | ||||||
|  |   Text, | ||||||
|  |   Th, | ||||||
|  |   Thead, | ||||||
|  |   Tooltip, | ||||||
|  |   Tr, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import { Download } from 'phosphor-react'; | import { Download } from 'phosphor-react'; | ||||||
| import { CSVLink } from 'react-csv'; | import { CSVLink } from 'react-csv'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @@ -128,9 +142,9 @@ const FmsLogsCard = () => { | |||||||
|             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} |             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} | ||||||
|             data={downloadableLogs as object[]} |             data={downloadableLogs as object[]} | ||||||
|           > |           > | ||||||
|             <Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> |             <Tooltip label={t('logs.export')} hasArrow> | ||||||
|               {t('logs.export')} |               <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" /> | ||||||
|             </Button> |             </Tooltip> | ||||||
|           </CSVLink> |           </CSVLink> | ||||||
|         </HStack> |         </HStack> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|   | |||||||
| @@ -1,5 +1,19 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react'; | import { | ||||||
|  |   Badge, | ||||||
|  |   Box, | ||||||
|  |   Flex, | ||||||
|  |   HStack, | ||||||
|  |   IconButton, | ||||||
|  |   Select, | ||||||
|  |   Spacer, | ||||||
|  |   Table, | ||||||
|  |   Text, | ||||||
|  |   Th, | ||||||
|  |   Thead, | ||||||
|  |   Tooltip, | ||||||
|  |   Tr, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import { Download } from 'phosphor-react'; | import { Download } from 'phosphor-react'; | ||||||
| import { CSVLink } from 'react-csv'; | import { CSVLink } from 'react-csv'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @@ -128,9 +142,9 @@ const GeneralLogsCard = () => { | |||||||
|             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} |             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} | ||||||
|             data={downloadableLogs as object[]} |             data={downloadableLogs as object[]} | ||||||
|           > |           > | ||||||
|             <Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> |             <Tooltip label={t('logs.export')} hasArrow> | ||||||
|               {t('logs.export')} |               <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" /> | ||||||
|             </Button> |             </Tooltip> | ||||||
|           </CSVLink> |           </CSVLink> | ||||||
|         </HStack> |         </HStack> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|   | |||||||
| @@ -1,5 +1,19 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react'; | import { | ||||||
|  |   Badge, | ||||||
|  |   Box, | ||||||
|  |   Flex, | ||||||
|  |   HStack, | ||||||
|  |   IconButton, | ||||||
|  |   Select, | ||||||
|  |   Spacer, | ||||||
|  |   Table, | ||||||
|  |   Text, | ||||||
|  |   Th, | ||||||
|  |   Thead, | ||||||
|  |   Tooltip, | ||||||
|  |   Tr, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import { Download } from 'phosphor-react'; | import { Download } from 'phosphor-react'; | ||||||
| import { CSVLink } from 'react-csv'; | import { CSVLink } from 'react-csv'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @@ -128,9 +142,9 @@ const SecLogsCard = () => { | |||||||
|             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} |             filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`} | ||||||
|             data={downloadableLogs as object[]} |             data={downloadableLogs as object[]} | ||||||
|           > |           > | ||||||
|             <Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}> |             <Tooltip label={t('logs.export')} hasArrow> | ||||||
|               {t('logs.export')} |               <IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" /> | ||||||
|             </Button> |             </Tooltip> | ||||||
|           </CSVLink> |           </CSVLink> | ||||||
|         </HStack> |         </HStack> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
|  | import { Box } from '@chakra-ui/react'; | ||||||
| import ApiKeyTable from './Table'; | import ApiKeyTable from './Table'; | ||||||
| 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'; | ||||||
| @@ -10,7 +11,9 @@ const ApiKeysCard = () => { | |||||||
|   return ( |   return ( | ||||||
|     <Card p={4}> |     <Card p={4}> | ||||||
|       <CardBody> |       <CardBody> | ||||||
|  |         <Box w="100%"> | ||||||
|           <ApiKeyTable userId={user?.id ?? ''} /> |           <ApiKeyTable userId={user?.id ?? ''} /> | ||||||
|  |         </Box> | ||||||
|       </CardBody> |       </CardBody> | ||||||
|     </Card> |     </Card> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react'; | import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { useParams } from 'react-router-dom'; | ||||||
| import ScriptTableActions from './Actions'; | import ScriptTableActions from './Actions'; | ||||||
| import CreateScriptButton from './CreateButton'; | import CreateScriptButton from './CreateButton'; | ||||||
| import useScriptsTable from './useScriptsTable'; | import useScriptsTable from './useScriptsTable'; | ||||||
| @@ -21,6 +22,7 @@ type Props = { | |||||||
| const ScriptTableCard = ({ onIdSelect }: Props) => { | const ScriptTableCard = ({ onIdSelect }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { query, hiddenColumns } = useScriptsTable(); |   const { query, hiddenColumns } = useScriptsTable(); | ||||||
|  |   const { id } = useParams(); | ||||||
|  |  | ||||||
|   const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []); |   const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []); | ||||||
|   const actionCell = React.useCallback( |   const actionCell = React.useCallback( | ||||||
| @@ -108,8 +110,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => { | |||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody> |       <CardBody> | ||||||
|         <Box w="100%" h="300px" overflowY="auto"> |         <Box w="100%" h="300px" overflowY="auto"> | ||||||
|           <DataTable |           <DataTable<Script> | ||||||
|             columns={columns as Column<object>[]} |             columns={columns} | ||||||
|             saveSettingsId="apiKeys.profile.table" |             saveSettingsId="apiKeys.profile.table" | ||||||
|             data={query.data ?? []} |             data={query.data ?? []} | ||||||
|             obj={t('script.other')} |             obj={t('script.other')} | ||||||
| @@ -118,6 +120,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => { | |||||||
|             hiddenColumns={hiddenColumns[0]} |             hiddenColumns={hiddenColumns[0]} | ||||||
|             showAllRows |             showAllRows | ||||||
|             hideControls |             hideControls | ||||||
|  |             onRowClick={(script) => onIdSelect(script.id)} | ||||||
|  |             isRowClickable={(script) => script.id !== id} | ||||||
|           /> |           /> | ||||||
|         </Box> |         </Box> | ||||||
|       </CardBody> |       </CardBody> | ||||||
|   | |||||||
							
								
								
									
										139
									
								
								src/pages/SystemPage/SystemSecrets/Actions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/pages/SystemPage/SystemSecrets/Actions.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { CopyIcon } from '@chakra-ui/icons'; | ||||||
|  | import { | ||||||
|  |   IconButton, | ||||||
|  |   Tooltip, | ||||||
|  |   Popover, | ||||||
|  |   PopoverArrow, | ||||||
|  |   PopoverBody, | ||||||
|  |   PopoverCloseButton, | ||||||
|  |   PopoverContent, | ||||||
|  |   PopoverFooter, | ||||||
|  |   PopoverHeader, | ||||||
|  |   PopoverTrigger, | ||||||
|  |   Center, | ||||||
|  |   Box, | ||||||
|  |   Button, | ||||||
|  |   useDisclosure, | ||||||
|  |   HStack, | ||||||
|  |   Text, | ||||||
|  |   useClipboard, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import { Eye, Trash } from 'phosphor-react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import EditSecretButton from './EditButton'; | ||||||
|  | import { Secret, useDeleteSystemSecret } from 'hooks/Network/Secrets'; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  |   secret: Secret; | ||||||
|  |   isDisabled?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const SystemSecretActions = ({ secret, isDisabled }: Props) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||||
|  |   const deleteSecret = useDeleteSystemSecret(); | ||||||
|  |   const { hasCopied, onCopy } = useClipboard(secret.value); | ||||||
|  |  | ||||||
|  |   const handleDeleteClick = React.useCallback(() => { | ||||||
|  |     deleteSecret.mutate(secret.key, { | ||||||
|  |       onSuccess: () => { | ||||||
|  |         onClose(); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <HStack mx="auto"> | ||||||
|  |       <Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}> | ||||||
|  |         <Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}> | ||||||
|  |           <Box> | ||||||
|  |             <PopoverTrigger> | ||||||
|  |               <IconButton | ||||||
|  |                 aria-label="delete-device" | ||||||
|  |                 colorScheme="red" | ||||||
|  |                 icon={<Trash size={20} />} | ||||||
|  |                 size="sm" | ||||||
|  |                 isDisabled={isDisabled} | ||||||
|  |               /> | ||||||
|  |             </PopoverTrigger> | ||||||
|  |           </Box> | ||||||
|  |         </Tooltip> | ||||||
|  |         <PopoverContent w="340px"> | ||||||
|  |           <PopoverArrow /> | ||||||
|  |           <PopoverCloseButton /> | ||||||
|  |           <PopoverHeader> | ||||||
|  |             {t('crud.delete')} {secret.key} | ||||||
|  |           </PopoverHeader> | ||||||
|  |           <PopoverBody> | ||||||
|  |             <Text whiteSpace="break-spaces">{t('crud.delete_confirm', { obj: t('system.secrets_one') })}</Text> | ||||||
|  |           </PopoverBody> | ||||||
|  |           <PopoverFooter> | ||||||
|  |             <Center> | ||||||
|  |               <Button colorScheme="gray" mr="1" onClick={onClose}> | ||||||
|  |                 {t('common.cancel')} | ||||||
|  |               </Button> | ||||||
|  |               <Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteSecret.isLoading}> | ||||||
|  |                 {t('common.yes')} | ||||||
|  |               </Button> | ||||||
|  |             </Center> | ||||||
|  |           </PopoverFooter> | ||||||
|  |         </PopoverContent> | ||||||
|  |       </Popover> | ||||||
|  |       <Tooltip | ||||||
|  |         label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`} | ||||||
|  |         hasArrow | ||||||
|  |         closeOnClick={false} | ||||||
|  |       > | ||||||
|  |         <IconButton | ||||||
|  |           aria-label={t('common.copy')} | ||||||
|  |           icon={<CopyIcon h={5} w={5} />} | ||||||
|  |           onClick={onCopy} | ||||||
|  |           size="sm" | ||||||
|  |           colorScheme="teal" | ||||||
|  |           mr={2} | ||||||
|  |         /> | ||||||
|  |       </Tooltip> | ||||||
|  |       <EditSecretButton secret={secret} /> | ||||||
|  |       <Popover> | ||||||
|  |         <Tooltip label={`${t('common.view')} ${t('system.secrets_one')}`} hasArrow closeOnClick={false}> | ||||||
|  |           <Box> | ||||||
|  |             <PopoverTrigger> | ||||||
|  |               <IconButton aria-label={t('common.view')} icon={<Eye size={20} />} size="sm" colorScheme="purple" /> | ||||||
|  |             </PopoverTrigger> | ||||||
|  |           </Box> | ||||||
|  |         </Tooltip> | ||||||
|  |         <PopoverContent w="560px"> | ||||||
|  |           <PopoverArrow /> | ||||||
|  |           <PopoverCloseButton /> | ||||||
|  |           <PopoverHeader> | ||||||
|  |             {t('common.view')} {secret.key} | ||||||
|  |             <Tooltip | ||||||
|  |               label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`} | ||||||
|  |               hasArrow | ||||||
|  |               closeOnClick={false} | ||||||
|  |             > | ||||||
|  |               <IconButton | ||||||
|  |                 aria-label={t('common.copy')} | ||||||
|  |                 icon={<CopyIcon h={4} w={4} />} | ||||||
|  |                 onClick={onCopy} | ||||||
|  |                 size="xs" | ||||||
|  |                 colorScheme="teal" | ||||||
|  |                 ml={2} | ||||||
|  |               /> | ||||||
|  |             </Tooltip> | ||||||
|  |           </PopoverHeader> | ||||||
|  |           <PopoverBody> | ||||||
|  |             <Text whiteSpace="break-spaces"> | ||||||
|  |               <Center> | ||||||
|  |                 <pre style={{ fontFamily: 'monospace' }}>{secret.value}</pre> | ||||||
|  |               </Center> | ||||||
|  |             </Text> | ||||||
|  |           </PopoverBody> | ||||||
|  |         </PopoverContent> | ||||||
|  |       </Popover> | ||||||
|  |     </HStack> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default SystemSecretActions; | ||||||
							
								
								
									
										118
									
								
								src/pages/SystemPage/SystemSecrets/CreateButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/pages/SystemPage/SystemSecrets/CreateButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { | ||||||
|  |   Box, | ||||||
|  |   FormControl, | ||||||
|  |   FormErrorMessage, | ||||||
|  |   FormLabel, | ||||||
|  |   Input, | ||||||
|  |   Textarea, | ||||||
|  |   useDisclosure, | ||||||
|  |   useToast, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { CreateButton } from '../../../components/Buttons/CreateButton'; | ||||||
|  | import { SaveButton } from '../../../components/Buttons/SaveButton'; | ||||||
|  | import { Modal } from '../../../components/Modals/Modal'; | ||||||
|  | import { useCreateSystemSecret } from 'hooks/Network/Secrets'; | ||||||
|  | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
|  | type FormValues = { | ||||||
|  |   key: string; | ||||||
|  |   value: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const DEFAULT_FORM_VALUES: FormValues = { | ||||||
|  |   key: '', | ||||||
|  |   value: '', | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const SystemSecretCreateButton = () => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|  |   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||||
|  |   const [form, setForm] = React.useState<FormValues>(DEFAULT_FORM_VALUES); | ||||||
|  |   const [isNameChanged, setIsNameChanged] = React.useState(false); | ||||||
|  |   const [isValueChanged, setIsValueChanged] = React.useState(false); | ||||||
|  |   const create = useCreateSystemSecret(); | ||||||
|  |  | ||||||
|  |   const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|  |     setForm({ ...form, key: e.target.value }); | ||||||
|  |     if (!isNameChanged) setIsNameChanged(true); | ||||||
|  |   }; | ||||||
|  |   const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||
|  |     setForm({ ...form, value: e.target.value }); | ||||||
|  |     if (!isValueChanged) setIsValueChanged(true); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const isNameError = form.key.length === 0; | ||||||
|  |   const isValueError = form.value.length === 0; | ||||||
|  |  | ||||||
|  |   const onSubmit = () => { | ||||||
|  |     create.mutate(form, { | ||||||
|  |       onSuccess: () => { | ||||||
|  |         toast({ | ||||||
|  |           id: 'create-system-secret-success', | ||||||
|  |           title: t('common.success'), | ||||||
|  |           description: t('crud.success_update_obj', { | ||||||
|  |             obj: t('system.secrets_one'), | ||||||
|  |           }), | ||||||
|  |           status: 'success', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|  |         }); | ||||||
|  |         onClose(); | ||||||
|  |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         toast({ | ||||||
|  |           id: 'create-system-secret-error', | ||||||
|  |           title: t('common.error'), | ||||||
|  |           description: (e as AxiosError)?.response?.data?.ErrorDescription, | ||||||
|  |           status: 'error', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleOpenClick = () => { | ||||||
|  |     setIsNameChanged(false); | ||||||
|  |     setIsValueChanged(false); | ||||||
|  |     setForm(DEFAULT_FORM_VALUES); | ||||||
|  |     onOpen(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <CreateButton onClick={handleOpenClick} isCompact /> | ||||||
|  |       <Modal | ||||||
|  |         isOpen={isOpen} | ||||||
|  |         onClose={onClose} | ||||||
|  |         title={t('system.secrets_create')} | ||||||
|  |         topRightButtons={ | ||||||
|  |           <SaveButton onClick={onSubmit} isDisabled={isNameError || isValueError} isLoading={create.isLoading} /> | ||||||
|  |         } | ||||||
|  |         options={{ | ||||||
|  |           modalSize: 'sm', | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         <Box> | ||||||
|  |           <FormControl mb={2} isInvalid={isNameError && isNameChanged}> | ||||||
|  |             <FormLabel>{t('common.name')}</FormLabel> | ||||||
|  |             <Input value={form.key} onChange={onKeyChange} /> | ||||||
|  |             <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||||
|  |           </FormControl> | ||||||
|  |           <FormControl mb={2} isInvalid={isValueError && isValueChanged}> | ||||||
|  |             <FormLabel>{t('common.value')}</FormLabel> | ||||||
|  |             <Textarea value={form.value} onChange={onValueChange} rows={2} /> | ||||||
|  |             <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||||
|  |           </FormControl> | ||||||
|  |         </Box> | ||||||
|  |       </Modal> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default SystemSecretCreateButton; | ||||||
							
								
								
									
										146
									
								
								src/pages/SystemPage/SystemSecrets/EditButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/pages/SystemPage/SystemSecrets/EditButton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { | ||||||
|  |   Box, | ||||||
|  |   Button, | ||||||
|  |   Center, | ||||||
|  |   FormControl, | ||||||
|  |   FormErrorMessage, | ||||||
|  |   FormLabel, | ||||||
|  |   IconButton, | ||||||
|  |   Input, | ||||||
|  |   Popover, | ||||||
|  |   PopoverArrow, | ||||||
|  |   PopoverBody, | ||||||
|  |   PopoverCloseButton, | ||||||
|  |   PopoverContent, | ||||||
|  |   PopoverFooter, | ||||||
|  |   PopoverHeader, | ||||||
|  |   PopoverTrigger, | ||||||
|  |   Text, | ||||||
|  |   Textarea, | ||||||
|  |   Tooltip, | ||||||
|  |   useDisclosure, | ||||||
|  |   useToast, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import { Pencil } from 'phosphor-react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { Secret, useUpdateSystemSecret } from 'hooks/Network/Secrets'; | ||||||
|  | import { AxiosError } from 'models/Axios'; | ||||||
|  |  | ||||||
|  | type FormValues = { | ||||||
|  |   key: string; | ||||||
|  |   value: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   secret: Secret; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const EditSecretButton = ({ secret }: Props) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|  |   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||||
|  |   const [form, setForm] = React.useState<FormValues>({ | ||||||
|  |     key: secret.key, | ||||||
|  |     value: secret.value, | ||||||
|  |   }); | ||||||
|  |   const [isNameChanged, setIsNameChanged] = React.useState(false); | ||||||
|  |   const [isValueChanged, setIsValueChanged] = React.useState(false); | ||||||
|  |   const update = useUpdateSystemSecret(); | ||||||
|  |  | ||||||
|  |   const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|  |     setForm({ ...form, key: e.target.value }); | ||||||
|  |     if (!isNameChanged) setIsNameChanged(true); | ||||||
|  |   }; | ||||||
|  |   const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||||
|  |     setForm({ ...form, value: e.target.value }); | ||||||
|  |     if (!isValueChanged) setIsValueChanged(true); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const isNameError = form.key.length === 0; | ||||||
|  |   const isValueError = form.value.length === 0; | ||||||
|  |  | ||||||
|  |   const onSubmit = () => { | ||||||
|  |     update.mutate(form, { | ||||||
|  |       onSuccess: () => { | ||||||
|  |         toast({ | ||||||
|  |           id: 'create-system-secret-success', | ||||||
|  |           title: t('common.success'), | ||||||
|  |           status: 'success', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|  |         }); | ||||||
|  |         onClose(); | ||||||
|  |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         toast({ | ||||||
|  |           id: 'create-system-secret-error', | ||||||
|  |           title: t('common.error'), | ||||||
|  |           description: (e as AxiosError)?.response?.data?.ErrorDescription, | ||||||
|  |           status: 'error', | ||||||
|  |           duration: 5000, | ||||||
|  |           isClosable: true, | ||||||
|  |           position: 'top-right', | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleOpenClick = () => { | ||||||
|  |     setIsNameChanged(false); | ||||||
|  |     setIsValueChanged(false); | ||||||
|  |     setForm({ | ||||||
|  |       key: secret.key, | ||||||
|  |       value: secret.value, | ||||||
|  |     }); | ||||||
|  |     onOpen(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Popover isOpen={isOpen} onOpen={handleOpenClick} onClose={onClose}> | ||||||
|  |       <Tooltip hasArrow label={t('crud.edit')} placement="top" isDisabled={isOpen}> | ||||||
|  |         <Box> | ||||||
|  |           <PopoverTrigger> | ||||||
|  |             <IconButton aria-label="delete-device" colorScheme="blue" icon={<Pencil size={20} />} size="sm" /> | ||||||
|  |           </PopoverTrigger> | ||||||
|  |         </Box> | ||||||
|  |       </Tooltip> | ||||||
|  |       <PopoverContent w="340px"> | ||||||
|  |         <PopoverArrow /> | ||||||
|  |         <PopoverCloseButton /> | ||||||
|  |         <PopoverHeader> | ||||||
|  |           {t('crud.edit')} {secret.key} | ||||||
|  |         </PopoverHeader> | ||||||
|  |         <PopoverBody> | ||||||
|  |           <Text whiteSpace="break-spaces"> | ||||||
|  |             <Box> | ||||||
|  |               <FormControl mb={2} isInvalid={isNameError && isNameChanged}> | ||||||
|  |                 <FormLabel>{t('common.name')}</FormLabel> | ||||||
|  |                 <Input value={form.key} onChange={onKeyChange} /> | ||||||
|  |                 <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||||
|  |               </FormControl> | ||||||
|  |               <FormControl mb={2} isInvalid={isValueError && isValueChanged}> | ||||||
|  |                 <FormLabel>{t('common.value')}</FormLabel> | ||||||
|  |                 <Textarea value={form.value} onChange={onValueChange} rows={2} /> | ||||||
|  |                 <FormErrorMessage>{t('form.required')}</FormErrorMessage> | ||||||
|  |               </FormControl> | ||||||
|  |             </Box> | ||||||
|  |           </Text> | ||||||
|  |         </PopoverBody> | ||||||
|  |         <PopoverFooter> | ||||||
|  |           <Center> | ||||||
|  |             <Button colorScheme="gray" mr="1" onClick={onClose}> | ||||||
|  |               {t('common.cancel')} | ||||||
|  |             </Button> | ||||||
|  |             <Button colorScheme="blue" ml="1" onClick={onSubmit} isLoading={update.isLoading}> | ||||||
|  |               {t('common.save')} | ||||||
|  |             </Button> | ||||||
|  |           </Center> | ||||||
|  |         </PopoverFooter> | ||||||
|  |       </PopoverContent> | ||||||
|  |     </Popover> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default EditSecretButton; | ||||||
							
								
								
									
										70
									
								
								src/pages/SystemPage/SystemSecrets/Table.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/pages/SystemPage/SystemSecrets/Table.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Box } from '@chakra-ui/react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { DataTable } from '../../../components/DataTables/DataTable'; | ||||||
|  | import SystemSecretActions from './Actions'; | ||||||
|  | import { Secret, useGetAllSystemSecrets, useGetSystemSecretsDictionary } from 'hooks/Network/Secrets'; | ||||||
|  | import { Column } from 'models/Table'; | ||||||
|  |  | ||||||
|  | const SystemSecretsTable = () => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const getSecrets = useGetAllSystemSecrets(); | ||||||
|  |   const getDictionary = useGetSystemSecretsDictionary(); | ||||||
|  |  | ||||||
|  |   const descriptionCell = React.useCallback( | ||||||
|  |     (secret: Secret) => { | ||||||
|  |       if (!getDictionary.data) return '-'; | ||||||
|  |  | ||||||
|  |       return getDictionary.data.find((d) => d.key === secret.key)?.description ?? '-'; | ||||||
|  |     }, | ||||||
|  |     [getDictionary.data], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const actionCell = React.useCallback((secret: Secret) => <SystemSecretActions secret={secret} />, []); | ||||||
|  |  | ||||||
|  |   const columns = React.useMemo( | ||||||
|  |     (): Column<Secret>[] => [ | ||||||
|  |       { | ||||||
|  |         id: 'key', | ||||||
|  |         Header: t('common.name'), | ||||||
|  |         Footer: '', | ||||||
|  |         accessor: 'key', | ||||||
|  |         alwaysShow: true, | ||||||
|  |         customWidth: '120px', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'description', | ||||||
|  |         Header: t('common.description'), | ||||||
|  |         Footer: '', | ||||||
|  |         Cell: ({ cell }) => descriptionCell(cell.row.original), | ||||||
|  |         accessor: 'description', | ||||||
|  |         hasPopover: true, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'actions', | ||||||
|  |         Header: t('common.actions'), | ||||||
|  |         Footer: '', | ||||||
|  |         Cell: (v) => actionCell(v.cell.row.original), | ||||||
|  |         disableSortBy: true, | ||||||
|  |         customWidth: '120px', | ||||||
|  |         alwaysShow: true, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     [t, descriptionCell], | ||||||
|  |   ); | ||||||
|  |   return ( | ||||||
|  |     <Box w="100%"> | ||||||
|  |       <DataTable | ||||||
|  |         columns={columns as Column<object>[]} | ||||||
|  |         saveSettingsId="system.secrets.table" | ||||||
|  |         data={getSecrets.data ?? []} | ||||||
|  |         obj={t('keys.other')} | ||||||
|  |         sortBy={[{ id: 'key', desc: false }]} | ||||||
|  |         showAllRows | ||||||
|  |         hideControls | ||||||
|  |       /> | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default SystemSecretsTable; | ||||||
							
								
								
									
										53
									
								
								src/pages/SystemPage/SystemSecrets/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/pages/SystemPage/SystemSecrets/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { | ||||||
|  |   BackgroundProps, | ||||||
|  |   Box, | ||||||
|  |   EffectProps, | ||||||
|  |   Heading, | ||||||
|  |   InteractivityProps, | ||||||
|  |   LayoutProps, | ||||||
|  |   PositionProps, | ||||||
|  |   SpaceProps, | ||||||
|  |   Spacer, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import SystemSecretCreateButton from './CreateButton'; | ||||||
|  | import SystemSecretsTable from './Table'; | ||||||
|  | import { Card } from 'components/Containers/Card'; | ||||||
|  | import { CardBody } from 'components/Containers/Card/CardBody'; | ||||||
|  | import { CardHeader } from 'components/Containers/Card/CardHeader'; | ||||||
|  | import { useAuth } from 'contexts/AuthProvider'; | ||||||
|  |  | ||||||
|  | export interface SystemSecretsCardProps | ||||||
|  |   extends LayoutProps, | ||||||
|  |     SpaceProps, | ||||||
|  |     BackgroundProps, | ||||||
|  |     InteractivityProps, | ||||||
|  |     PositionProps, | ||||||
|  |     EffectProps {} | ||||||
|  |  | ||||||
|  | export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { user } = useAuth(); | ||||||
|  |  | ||||||
|  |   if (!user || user.userRole !== 'root') { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Box px={4} py={4}> | ||||||
|  |       <Card variant="widget" {...props}> | ||||||
|  |         <CardHeader> | ||||||
|  |           <Heading size="md" my="auto"> | ||||||
|  |             {t('system.secrets')} | ||||||
|  |           </Heading> | ||||||
|  |           <Spacer /> | ||||||
|  |           <SystemSecretCreateButton /> | ||||||
|  |         </CardHeader> | ||||||
|  |         <CardBody p={4}> | ||||||
|  |           <SystemSecretsTable /> | ||||||
|  |         </CardBody> | ||||||
|  |       </Card> | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -21,8 +21,8 @@ import axios from 'axios'; | |||||||
| import { FloppyDisk } from 'phosphor-react'; | import { 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,5 +1,6 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Button, useDisclosure } from '@chakra-ui/react'; | import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react'; | ||||||
|  | import { Article } from 'phosphor-react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import SystemLoggingModal from './Modal'; | import SystemLoggingModal from './Modal'; | ||||||
| import { EndpointApiResponse } from 'hooks/Network/Endpoints'; | import { EndpointApiResponse } from 'hooks/Network/Endpoints'; | ||||||
| @@ -15,9 +16,17 @@ const SystemLoggingButton = ({ endpoint, token }: Props) => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto"> |       <Tooltip label={t('system.logging')} hasArrow> | ||||||
|         {t('system.logging')} |         <IconButton | ||||||
|       </Button> |           aria-label={t('system.logging')} | ||||||
|  |           colorScheme="teal" | ||||||
|  |           type="button" | ||||||
|  |           my="auto" | ||||||
|  |           onClick={modalProps.onOpen} | ||||||
|  |           icon={<Article size={20} />} | ||||||
|  |           mr={2} | ||||||
|  |         /> | ||||||
|  |       </Tooltip> | ||||||
|       <SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} /> |       <SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} /> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -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,12 @@ 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 { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
| 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,21 +66,12 @@ const SystemTile = ({ endpoint, token }: Props) => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Card> |       <Card variant="widget"> | ||||||
|         <Box display="flex" mb={2}> |         <Box display="flex" mb={2}> | ||||||
|           <Heading pt={0}>{endpoint.type}</Heading> |           <Heading pt={0}>{endpoint.type}</Heading> | ||||||
|           <Spacer /> |           <Spacer /> | ||||||
|           <SystemLoggingButton endpoint={endpoint} token={token} /> |           <SystemLoggingButton endpoint={endpoint} token={token} /> | ||||||
|           <Button |           <RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} /> | ||||||
|             mt={1} |  | ||||||
|             minWidth="112px" |  | ||||||
|             colorScheme="gray" |  | ||||||
|             rightIcon={<ArrowsClockwise />} |  | ||||||
|             onClick={refresh} |  | ||||||
|             isLoading={isFetchingSystem || isFetchingSubsystems} |  | ||||||
|           > |  | ||||||
|             {t('common.refresh')} |  | ||||||
|           </Button> |  | ||||||
|         </Box> |         </Box> | ||||||
|         <CardBody> |         <CardBody> | ||||||
|           <VStack w="100%"> |           <VStack w="100%"> | ||||||
| @@ -179,7 +171,7 @@ const SystemTile = ({ endpoint, token }: Props) => { | |||||||
|                   ml={2} |                   ml={2} | ||||||
|                   onClick={handleReloadClick} |                   onClick={handleReloadClick} | ||||||
|                   icon={<ArrowsClockwise size={20} />} |                   icon={<ArrowsClockwise size={20} />} | ||||||
|                   colorScheme="gray" |                   colorScheme="blue" | ||||||
|                   isLoading={isReloading} |                   isLoading={isReloading} | ||||||
|                   isDisabled={subs.length === 0} |                   isDisabled={subs.length === 0} | ||||||
|                 /> |                 /> | ||||||
|   | |||||||
| @@ -1,27 +1,47 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { Heading, SimpleGrid, Spacer } from '@chakra-ui/react'; | import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { 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 SystemPage = () => { | const getDefaultTabIndex = () => { | ||||||
|  |   const index = localStorage.getItem('system-tab-index') || '0'; | ||||||
|  |   try { | ||||||
|  |     return parseInt(index, 10); | ||||||
|  |   } catch { | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | type Props = { | ||||||
|  |   isOnlySec?: boolean; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const SystemPage = ({ isOnlySec }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { token } = useAuth(); |   const { token, user } = 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 (!endpoints || !token) return null; |     if (!token || (!isOnlySec && !endpoints)) return null; | ||||||
|  |  | ||||||
|     const endpointList = [...endpoints]; |     const endpointList = endpoints ? [...endpoints] : []; | ||||||
|     endpointList.push({ |     endpointList.push({ | ||||||
|       uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '', |       uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '', | ||||||
|       type: 'owsec', |       type: isOnlySec ? '' : 'owsec', | ||||||
|       id: 0, |       id: 0, | ||||||
|       vendor: 'owsec', |       vendor: 'owsec', | ||||||
|       authenticationType: '', |       authenticationType: '', | ||||||
| @@ -37,20 +57,51 @@ const SystemPage = () => { | |||||||
|   }, [endpoints, token]); |   }, [endpoints, token]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <Card p={0}> | ||||||
|       <Card mb={4} py={2} px={4}> |       <Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy> | ||||||
|  |         <TabList> | ||||||
|           <CardHeader> |           <CardHeader> | ||||||
|           <Heading size="md" my="auto"> |             <Tab>{t('system.services')}</Tab> | ||||||
|             {t('controller.firmware.endpoints')} |             <Tab hidden={!isRoot}>{t('system.configuration')}</Tab> | ||||||
|           </Heading> |           </CardHeader> | ||||||
|  |         </TabList> | ||||||
|  |         <TabPanels> | ||||||
|  |           <TabPanel p={0}> | ||||||
|  |             <Box | ||||||
|  |               borderLeft="1px solid" | ||||||
|  |               borderRight="1px solid" | ||||||
|  |               borderBottom="1px solid" | ||||||
|  |               borderColor="var(--chakra-colors-chakra-border-color)" | ||||||
|  |               borderBottomLeftRadius="15px" | ||||||
|  |               borderBottomRightRadius="15px" | ||||||
|  |             > | ||||||
|  |               {!isOnlySec && ( | ||||||
|  |                 <CardHeader px={4} pt={4}> | ||||||
|                   <Spacer /> |                   <Spacer /> | ||||||
|                   <RefreshButton onClick={refetch} isFetching={isFetching} /> |                   <RefreshButton onClick={refetch} isFetching={isFetching} /> | ||||||
|                 </CardHeader> |                 </CardHeader> | ||||||
|       </Card> |               )} | ||||||
|       <SimpleGrid minChildWidth="500px" spacing="20px" mb={3}> |               <SimpleGrid minChildWidth="500px" spacing="20px" p={4}> | ||||||
|                 {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,9 +12,11 @@ interface Props { | |||||||
|   isSuspended: boolean; |   isSuspended: boolean; | ||||||
|   isWaitingForCheck: boolean; |   isWaitingForCheck: boolean; | ||||||
|   refresh: () => void; |   refresh: () => void; | ||||||
|  |   isDisabled?: boolean; | ||||||
|  |   size?: 'sm' | 'md' | 'lg'; | ||||||
| } | } | ||||||
|  |  | ||||||
| const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refresh }) => { | const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', isDisabled }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|   const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh }); |   const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh }); | ||||||
| @@ -75,10 +77,17 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Menu> |     <Menu> | ||||||
|       <Tooltip label={t('commands.other')}> |       <Tooltip label={t('common.actions')}> | ||||||
|         <MenuButton as={IconButton} aria-label="Commands" icon={<Wrench size={20} />} size="sm" ml={2} /> |         <MenuButton | ||||||
|  |           as={IconButton} | ||||||
|  |           aria-label="Commands" | ||||||
|  |           icon={<Wrench size={20} />} | ||||||
|  |           size={size} | ||||||
|  |           ml={2} | ||||||
|  |           isDisabled={isDisabled} | ||||||
|  |         /> | ||||||
|       </Tooltip> |       </Tooltip> | ||||||
|       <MenuList> |       <MenuList fontSize="md"> | ||||||
|         <MenuItem onClick={handleSuspendClick}> |         <MenuItem onClick={handleSuspendClick}> | ||||||
|           {isSuspended ? t('users.reactivate_user') : t('users.suspend')} |           {isSuspended ? t('users.reactivate_user') : t('users.suspend')} | ||||||
|         </MenuItem> |         </MenuItem> | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { AddIcon } from '@chakra-ui/icons'; | import { useDisclosure } from '@chakra-ui/react'; | ||||||
| import { Button, useDisclosure } from '@chakra-ui/react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| 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 CreateUserForm, { CreateUserFormValues } from './Form'; | import CreateUserForm, { CreateUserFormValues } from './Form'; | ||||||
|  | import { CreateButton } from 'components/Buttons/CreateButton'; | ||||||
| import { useAuth } from 'contexts/AuthProvider'; | import { useAuth } from 'contexts/AuthProvider'; | ||||||
| import { useFormRef } from 'hooks/useFormRef'; | import { useFormRef } from 'hooks/useFormRef'; | ||||||
|  |  | ||||||
| @@ -25,16 +25,7 @@ const CreateUserModal = () => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Button |       {user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />} | ||||||
|         hidden={user?.userRole === 'csr'} |  | ||||||
|         alignItems="center" |  | ||||||
|         colorScheme="blue" |  | ||||||
|         rightIcon={<AddIcon />} |  | ||||||
|         onClick={onOpen} |  | ||||||
|         ml={2} |  | ||||||
|       > |  | ||||||
|         {t('crud.create')} |  | ||||||
|       </Button> |  | ||||||
|       <Modal |       <Modal | ||||||
|         isOpen={isOpen} |         isOpen={isOpen} | ||||||
|         onClose={closeModal} |         onClose={closeModal} | ||||||
|   | |||||||
| @@ -62,7 +62,7 @@ const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Pro | |||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     setFormKey(uuid()); |     setFormKey(uuid()); | ||||||
|   }, [isOpen]); |   }, [isOpen, editing]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Formik |     <Formik | ||||||
|   | |||||||
| @@ -1,12 +1,14 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { useEffect } from 'react'; | import { useEffect } from 'react'; | ||||||
| import { Spinner, Center, useDisclosure, useBoolean } from '@chakra-ui/react'; | import { Spinner, Center, useDisclosure, useBoolean, Tag } from '@chakra-ui/react'; | ||||||
|  | import { useQueryClient } from '@tanstack/react-query'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { 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'; | ||||||
|  |  | ||||||
| @@ -19,10 +21,11 @@ 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 } = useGetUser({ id: userId ?? '', enabled: canFetchUser }); |   const { data: user, isFetching, refetch } = useGetUser({ id: userId ?? '', enabled: canFetchUser }); | ||||||
|  |  | ||||||
|   const closeModal = () => (form.dirty ? openConfirm() : onClose()); |   const closeModal = () => (form.dirty ? openConfirm() : onClose()); | ||||||
|  |  | ||||||
| @@ -31,6 +34,11 @@ 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]); | ||||||
| @@ -40,15 +48,40 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => { | |||||||
|       <Modal |       <Modal | ||||||
|         isOpen={isOpen} |         isOpen={isOpen} | ||||||
|         onClose={closeModal} |         onClose={closeModal} | ||||||
|         title={t('crud.edit_obj', { obj: t('user.title') })} |         title={user?.name ?? t('crud.edit_obj', { obj: t('user.title') })} | ||||||
|  |         tags={ | ||||||
|  |           <> | ||||||
|  |             {user?.suspended ? ( | ||||||
|  |               <Tag colorScheme="yellow" size="lg"> | ||||||
|  |                 {t('user.suspended')} | ||||||
|  |               </Tag> | ||||||
|  |             ) : null} | ||||||
|  |             {user?.waitingForEmailCheck ? ( | ||||||
|  |               <Tag colorScheme="blue" size="lg"> | ||||||
|  |                 {t('user.email_not_validated')} | ||||||
|  |               </Tag> | ||||||
|  |             ) : null} | ||||||
|  |           </> | ||||||
|  |         } | ||||||
|         topRightButtons={ |         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} | ||||||
|             /> |             /> | ||||||
|             <EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact /> |             <ToggleEditButton ml={2} isEditing={editing} toggleEdit={setEditing.toggle} isDirty={form.dirty} /> | ||||||
|  |             {user ? ( | ||||||
|  |               <ActionsDropdown | ||||||
|  |                 id={user?.id} | ||||||
|  |                 isSuspended={user?.suspended} | ||||||
|  |                 isWaitingForCheck={user?.waitingForEmailCheck} | ||||||
|  |                 refresh={refresh} | ||||||
|  |                 size="md" | ||||||
|  |                 isDisabled={editing} | ||||||
|  |               /> | ||||||
|  |             ) : null} | ||||||
|           </> |           </> | ||||||
|         } |         } | ||||||
|       > |       > | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import React, { useCallback, useState } from 'react'; | import React, { useCallback, useState } from 'react'; | ||||||
| import { Avatar, Box, Button, Flex, useDisclosure } from '@chakra-ui/react'; | import { Avatar, Box, Flex, useDisclosure } from '@chakra-ui/react'; | ||||||
| import { ArrowsClockwise } 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 { ColumnPicker } from '../../../components/DataTables/ColumnPicker'; | import { ColumnPicker } from '../../../components/DataTables/ColumnPicker'; | ||||||
| @@ -9,6 +8,7 @@ import FormattedDate from '../../../components/InformationDisplays/FormattedDate | |||||||
| import CreateUserModal from './CreateUserModal'; | import CreateUserModal from './CreateUserModal'; | ||||||
| import EditUserModal from './EditUserModal'; | import EditUserModal from './EditUserModal'; | ||||||
| import UserActions from './UserActions'; | import UserActions from './UserActions'; | ||||||
|  | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
| 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 { CardHeader } from 'components/Containers/Card/CardHeader'; | import { CardHeader } from 'components/Containers/Card/CardHeader'; | ||||||
| @@ -25,10 +25,10 @@ const UserTable = () => { | |||||||
|   const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure(); |   const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure(); | ||||||
|   const { data: users, refetch: refreshUsers, isFetching } = useGetUsers(); |   const { data: users, refetch: refreshUsers, isFetching } = useGetUsers(); | ||||||
|  |  | ||||||
|   const openEditModal = (editUser: User) => { |   const openEditModal = React.useCallback((editUser: User) => { | ||||||
|     setEditId(editUser.id); |     setEditId(editUser.id); | ||||||
|     openEdit(); |     openEdit(); | ||||||
|   }; |   }, []); | ||||||
|  |  | ||||||
|   const memoizedActions = useCallback( |   const memoizedActions = useCallback( | ||||||
|     (userActions: User) => ( |     (userActions: User) => ( | ||||||
| @@ -99,7 +99,7 @@ const UserTable = () => { | |||||||
|     ]; |     ]; | ||||||
|     if (user?.userRole !== 'csr') |     if (user?.userRole !== 'csr') | ||||||
|       baseColumns.push({ |       baseColumns.push({ | ||||||
|         id: 'user', |         id: 'actions', | ||||||
|         Header: t('common.actions'), |         Header: t('common.actions'), | ||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'Id', |         accessor: 'Id', | ||||||
| @@ -125,28 +125,21 @@ const UserTable = () => { | |||||||
|                 preference="provisioning.userTable.hiddenColumns" |                 preference="provisioning.userTable.hiddenColumns" | ||||||
|               /> |               /> | ||||||
|               <CreateUserModal /> |               <CreateUserModal /> | ||||||
|               <Button |               <RefreshButton onClick={refreshUsers} isFetching={isFetching} ml={2} /> | ||||||
|                 colorScheme="gray" |  | ||||||
|                 onClick={() => refreshUsers()} |  | ||||||
|                 rightIcon={<ArrowsClockwise />} |  | ||||||
|                 ml={2} |  | ||||||
|                 isLoading={isFetching} |  | ||||||
|               > |  | ||||||
|                 {t('common.refresh')} |  | ||||||
|               </Button> |  | ||||||
|             </Box> |             </Box> | ||||||
|           </Flex> |           </Flex> | ||||||
|         </CardHeader> |         </CardHeader> | ||||||
|         <CardBody> |         <CardBody> | ||||||
|           <Box overflowX="auto" w="100%"> |           <Box overflowX="auto" w="100%"> | ||||||
|             <DataTable |             <DataTable<User> | ||||||
|               columns={columns as Column<object>[]} |               columns={columns} | ||||||
|               data={users?.filter((curr) => curr.email !== user?.email) ?? []} |               data={users ?? []} | ||||||
|               isLoading={isFetching} |               isLoading={isFetching} | ||||||
|               obj={t('users.title')} |               obj={t('users.title')} | ||||||
|               sortBy={[{ id: 'email', desc: false }]} |               sortBy={[{ id: 'email', desc: false }]} | ||||||
|               hiddenColumns={hiddenColumns} |               hiddenColumns={hiddenColumns} | ||||||
|               fullScreen |               fullScreen | ||||||
|  |               onRowClick={openEditModal} | ||||||
|             /> |             /> | ||||||
|           </Box> |           </Box> | ||||||
|         </CardBody> |         </CardBody> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { extendTheme, type ThemeConfig } from '@chakra-ui/react'; | import { extendTheme, Tooltip, type ThemeConfig } from '@chakra-ui/react'; | ||||||
| import CardComponent from './additions/card/Card'; | import CardComponent from './additions/card/Card'; | ||||||
| import CardBodyComponent from './additions/card/CardBody'; | import CardBodyComponent from './additions/card/CardBody'; | ||||||
| import CardHeaderComponent from './additions/card/CardHeader'; | import CardHeaderComponent from './additions/card/CardHeader'; | ||||||
| @@ -37,4 +37,6 @@ const theme = extendTheme({ | |||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | Tooltip.defaultProps = { ...Tooltip.defaultProps, hasArrow: true }; | ||||||
|  |  | ||||||
| export default theme; | export default theme; | ||||||
|   | |||||||
| @@ -1,49 +1,9 @@ | |||||||
| import { defineConfig } from 'vite'; | import { defineConfig } from 'vite'; | ||||||
| import tsconfigPaths from 'vite-tsconfig-paths'; | import tsconfigPaths from 'vite-tsconfig-paths'; | ||||||
| import { VitePWA } from 'vite-plugin-pwa'; |  | ||||||
| import react from '@vitejs/plugin-react'; | import react from '@vitejs/plugin-react'; | ||||||
|  |  | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   plugins: [ |   plugins: [tsconfigPaths(), react()], | ||||||
|     tsconfigPaths(), |  | ||||||
|     react(), |  | ||||||
|     VitePWA({ |  | ||||||
|       registerType: 'autoUpdate', |  | ||||||
|       devOptions: { |  | ||||||
|         enabled: true, |  | ||||||
|         /* other options */ |  | ||||||
|       }, |  | ||||||
|       manifest: { |  | ||||||
|         name: 'OpenWiFi Controller App', |  | ||||||
|         short_name: 'OpenWiFiController', |  | ||||||
|         description: 'OpenWiFi Controller App', |  | ||||||
|         theme_color: '#000000', |  | ||||||
|         icons: [ |  | ||||||
|           { |  | ||||||
|             src: 'android-chrome-192x192.png', |  | ||||||
|             sizes: '192x192', |  | ||||||
|             type: 'image/png', |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             src: 'android-chrome-384x384.png', |  | ||||||
|             sizes: '384x384', |  | ||||||
|             type: 'image/png', |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             src: 'android-chrome-512x512.png', |  | ||||||
|             sizes: '512x512', |  | ||||||
|             type: 'image/png', |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             src: 'android-chrome-512x512.png', |  | ||||||
|             sizes: '512x512', |  | ||||||
|             type: 'image/png', |  | ||||||
|             purpose: 'any maskable', |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }, |  | ||||||
|     }), |  | ||||||
|   ], |  | ||||||
|   build: { |   build: { | ||||||
|     outDir: './build', |     outDir: './build', | ||||||
|     chunkSizeWarningLimit: 1000, |     chunkSizeWarningLimit: 1000, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user