Compare commits
	
		
			57 Commits
		
	
	
		
			v2.11.0
			...
			release/v3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f23abf0f4a | ||
|   | eccff13fb2 | ||
|   | 8eede7b559 | ||
|   | caab40b08e | ||
|   | 18fa320b19 | ||
|   | 6f9f6638d6 | ||
|   | 5688e2f7bc | ||
|   | 4738097178 | ||
|   | 591ecc3664 | ||
|   | b9089a39ac | ||
|   | b7bdf89d37 | ||
|   | 849ea9f7b2 | ||
|   | bd737ef563 | ||
|   | e250bd38f8 | ||
|   | 7083da702a | ||
|   | 3d01c20339 | ||
|   | 3b74649206 | ||
|   | a10f0c992e | ||
|   | 32974620c4 | ||
|   | 0781e3ad8e | ||
|   | 0ce107eea0 | ||
|   | 73e3efd92f | ||
|   | 69bff8d8fe | ||
|   | 22b223f82f | ||
|   | 7b0d43c8b8 | ||
|   | 7c64fb7a11 | ||
|   | 61f8b69f02 | ||
|   | c32fedeb4c | ||
|   | 4ba3bed742 | ||
|   | 810318b584 | ||
|   | 863fda3ef3 | ||
|   | deb7715ea1 | ||
|   | adaebb17e7 | ||
|   | e3f6ab43ff | ||
|   | cf977b7612 | ||
|   | fedb60fc8f | ||
|   | f8ddf88b8c | ||
|   | 301581da63 | ||
|   | 88cb945760 | ||
|   | c61d0052a9 | ||
|   | 147c3a1153 | ||
|   | e9f1e4d8da | ||
|   | f3a995f68f | ||
|   | a967163d28 | ||
|   | d3514213ca | ||
|   | a55341f406 | ||
|   | 1c9a5bfa18 | ||
|   | 179900fab0 | ||
|   | 9011e30521 | ||
|   | 418f4ce576 | ||
|   | 9eb65237f9 | ||
|   | 89a667569b | ||
|   | b87091a33a | ||
|   | d9a659acbc | ||
|   | ec8347fd7d | ||
|   | b161729c46 | ||
|   | 2194a7fc23 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -18,3 +18,4 @@ | |||||||
| .env.production.local | .env.production.local | ||||||
|  |  | ||||||
| npm-debug.log* | npm-debug.log* | ||||||
|  | .vscode/settings.json | ||||||
|   | |||||||
| @@ -17,7 +17,9 @@ metadata: | |||||||
|   {{- end }} |   {{- end }} | ||||||
|  |  | ||||||
| spec: | spec: | ||||||
|  | {{- if $ingressValue.className }} | ||||||
|  |   ingressClassName: {{ $ingressValue.className }} | ||||||
|  | {{- end }} | ||||||
| {{- if $ingressValue.tls }} | {{- if $ingressValue.tls }} | ||||||
|   tls: |   tls: | ||||||
|   {{- range $ingressValue.tls }} |   {{- range $ingressValue.tls }} | ||||||
|   | |||||||
| @@ -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: v3.1.0 | ||||||
|     pullPolicy: Always |     pullPolicy: Always | ||||||
|  |  | ||||||
| services: | services: | ||||||
|   | |||||||
							
								
								
									
										501
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
|   "name": "ucentral-client", |   "name": "ucentral-client", | ||||||
|   "version": "2.11.0(7)", |   "version": "3.1.0(5)", | ||||||
|   "lockfileVersion": 3, |   "lockfileVersion": 3, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "ucentral-client", |       "name": "ucentral-client", | ||||||
|       "version": "2.11.0(7)", |       "version": "3.1.0(5)", | ||||||
|       "license": "ISC", |       "license": "ISC", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@chakra-ui/anatomy": "^2.1.1", |         "@chakra-ui/anatomy": "^2.1.1", | ||||||
| @@ -88,6 +88,7 @@ | |||||||
|         "lint-staged": "^13.2.1", |         "lint-staged": "^13.2.1", | ||||||
|         "prettier": "^2.8.7", |         "prettier": "^2.8.7", | ||||||
|         "vite-plugin-pwa": "^0.14.7", |         "vite-plugin-pwa": "^0.14.7", | ||||||
|  |         "vite-plugin-svgr": "^4.2.0", | ||||||
|         "vite-tsconfig-paths": "^4.2.0" |         "vite-tsconfig-paths": "^4.2.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
| @@ -104,11 +105,12 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/code-frame": { |     "node_modules/@babel/code-frame": { | ||||||
|       "version": "7.21.4", |       "version": "7.22.13", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", |       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", | ||||||
|       "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", |       "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/highlight": "^7.18.6" |         "@babel/highlight": "^7.22.13", | ||||||
|  |         "chalk": "^2.4.2" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
| @@ -154,12 +156,12 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/generator": { |     "node_modules/@babel/generator": { | ||||||
|       "version": "7.21.5", |       "version": "7.23.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", |       "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", | ||||||
|       "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", |       "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/types": "^7.21.5", |         "@babel/types": "^7.23.0", | ||||||
|         "@jridgewell/gen-mapping": "^0.3.2", |         "@jridgewell/gen-mapping": "^0.3.2", | ||||||
|         "@jridgewell/trace-mapping": "^0.3.17", |         "@jridgewell/trace-mapping": "^0.3.17", | ||||||
|         "jsesc": "^2.5.1" |         "jsesc": "^2.5.1" | ||||||
| @@ -297,33 +299,34 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/helper-environment-visitor": { |     "node_modules/@babel/helper-environment-visitor": { | ||||||
|       "version": "7.21.5", |       "version": "7.22.20", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", |       "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", | ||||||
|       "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", |       "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/helper-function-name": { |     "node_modules/@babel/helper-function-name": { | ||||||
|       "version": "7.21.0", |       "version": "7.23.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", |       "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", | ||||||
|       "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", |       "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/template": "^7.20.7", |         "@babel/template": "^7.22.15", | ||||||
|         "@babel/types": "^7.21.0" |         "@babel/types": "^7.23.0" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/helper-hoist-variables": { |     "node_modules/@babel/helper-hoist-variables": { | ||||||
|       "version": "7.18.6", |       "version": "7.22.5", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", | ||||||
|  |       "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/types": "^7.18.6" |         "@babel/types": "^7.22.5" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
| @@ -452,27 +455,29 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/helper-split-export-declaration": { |     "node_modules/@babel/helper-split-export-declaration": { | ||||||
|       "version": "7.18.6", |       "version": "7.22.6", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", | ||||||
|  |       "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT", |  | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/types": "^7.18.6" |         "@babel/types": "^7.22.5" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/helper-string-parser": { |     "node_modules/@babel/helper-string-parser": { | ||||||
|       "version": "7.21.5", |       "version": "7.22.5", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", |       "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", | ||||||
|       "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", |       "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/helper-validator-identifier": { |     "node_modules/@babel/helper-validator-identifier": { | ||||||
|       "version": "7.19.1", |       "version": "7.22.20", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", | ||||||
|  |       "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
|       } |       } | ||||||
| @@ -516,11 +521,12 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/highlight": { |     "node_modules/@babel/highlight": { | ||||||
|       "version": "7.18.6", |       "version": "7.22.20", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", | ||||||
|  |       "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/helper-validator-identifier": "^7.18.6", |         "@babel/helper-validator-identifier": "^7.22.20", | ||||||
|         "chalk": "^2.0.0", |         "chalk": "^2.4.2", | ||||||
|         "js-tokens": "^4.0.0" |         "js-tokens": "^4.0.0" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
| @@ -528,9 +534,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/parser": { |     "node_modules/@babel/parser": { | ||||||
|       "version": "7.21.8", |       "version": "7.23.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", |       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", | ||||||
|       "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", |       "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "bin": { |       "bin": { | ||||||
|         "parser": "bin/babel-parser.js" |         "parser": "bin/babel-parser.js" | ||||||
| @@ -1684,33 +1690,33 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/template": { |     "node_modules/@babel/template": { | ||||||
|       "version": "7.20.7", |       "version": "7.22.15", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", |       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", | ||||||
|       "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", |       "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/code-frame": "^7.18.6", |         "@babel/code-frame": "^7.22.13", | ||||||
|         "@babel/parser": "^7.20.7", |         "@babel/parser": "^7.22.15", | ||||||
|         "@babel/types": "^7.20.7" |         "@babel/types": "^7.22.15" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=6.9.0" |         "node": ">=6.9.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/traverse": { |     "node_modules/@babel/traverse": { | ||||||
|       "version": "7.21.5", |       "version": "7.23.2", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", |       "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", | ||||||
|       "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", |       "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/code-frame": "^7.21.4", |         "@babel/code-frame": "^7.22.13", | ||||||
|         "@babel/generator": "^7.21.5", |         "@babel/generator": "^7.23.0", | ||||||
|         "@babel/helper-environment-visitor": "^7.21.5", |         "@babel/helper-environment-visitor": "^7.22.20", | ||||||
|         "@babel/helper-function-name": "^7.21.0", |         "@babel/helper-function-name": "^7.23.0", | ||||||
|         "@babel/helper-hoist-variables": "^7.18.6", |         "@babel/helper-hoist-variables": "^7.22.5", | ||||||
|         "@babel/helper-split-export-declaration": "^7.18.6", |         "@babel/helper-split-export-declaration": "^7.22.6", | ||||||
|         "@babel/parser": "^7.21.5", |         "@babel/parser": "^7.23.0", | ||||||
|         "@babel/types": "^7.21.5", |         "@babel/types": "^7.23.0", | ||||||
|         "debug": "^4.1.0", |         "debug": "^4.1.0", | ||||||
|         "globals": "^11.1.0" |         "globals": "^11.1.0" | ||||||
|       }, |       }, | ||||||
| @@ -1719,12 +1725,12 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@babel/types": { |     "node_modules/@babel/types": { | ||||||
|       "version": "7.21.5", |       "version": "7.23.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", |       "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", | ||||||
|       "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", |       "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@babel/helper-string-parser": "^7.21.5", |         "@babel/helper-string-parser": "^7.22.5", | ||||||
|         "@babel/helper-validator-identifier": "^7.19.1", |         "@babel/helper-validator-identifier": "^7.22.20", | ||||||
|         "to-fast-properties": "^2.0.0" |         "to-fast-properties": "^2.0.0" | ||||||
|       }, |       }, | ||||||
|       "engines": { |       "engines": { | ||||||
| @@ -3950,9 +3956,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@rollup/pluginutils": { |     "node_modules/@rollup/pluginutils": { | ||||||
|       "version": "5.0.2", |       "version": "5.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", |       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", | ||||||
|       "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", |       "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@types/estree": "^1.0.0", |         "@types/estree": "^1.0.0", | ||||||
| @@ -3963,7 +3969,7 @@ | |||||||
|         "node": ">=14.0.0" |         "node": ">=14.0.0" | ||||||
|       }, |       }, | ||||||
|       "peerDependencies": { |       "peerDependencies": { | ||||||
|         "rollup": "^1.20.0||^2.0.0||^3.0.0" |         "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" | ||||||
|       }, |       }, | ||||||
|       "peerDependenciesMeta": { |       "peerDependenciesMeta": { | ||||||
|         "rollup": { |         "rollup": { | ||||||
| @@ -3992,6 +3998,245 @@ | |||||||
|         "sourcemap-codec": "^1.4.8" |         "sourcemap-codec": "^1.4.8" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-add-jsx-attribute": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-svg-dynamic-title": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-svg-em-dimensions": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-transform-react-native-svg": { | ||||||
|  |       "version": "8.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", | ||||||
|  |       "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-plugin-transform-svg-component": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=12" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/babel-preset": { | ||||||
|  |       "version": "8.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", | ||||||
|  |       "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", | ||||||
|  |         "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", | ||||||
|  |         "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", | ||||||
|  |         "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", | ||||||
|  |         "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", | ||||||
|  |         "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", | ||||||
|  |         "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", | ||||||
|  |         "@svgr/babel-plugin-transform-svg-component": "8.0.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@babel/core": "^7.0.0-0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/core": { | ||||||
|  |       "version": "8.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", | ||||||
|  |       "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "@babel/core": "^7.21.3", | ||||||
|  |         "@svgr/babel-preset": "8.1.0", | ||||||
|  |         "camelcase": "^6.2.0", | ||||||
|  |         "cosmiconfig": "^8.1.3", | ||||||
|  |         "snake-case": "^3.0.4" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/core/node_modules/cosmiconfig": { | ||||||
|  |       "version": "8.3.6", | ||||||
|  |       "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", | ||||||
|  |       "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "import-fresh": "^3.3.0", | ||||||
|  |         "js-yaml": "^4.1.0", | ||||||
|  |         "parse-json": "^5.2.0", | ||||||
|  |         "path-type": "^4.0.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/d-fischer" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "typescript": ">=4.9.5" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "typescript": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/hast-util-to-babel-ast": { | ||||||
|  |       "version": "8.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", | ||||||
|  |       "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "@babel/types": "^7.21.3", | ||||||
|  |         "entities": "^4.4.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@svgr/plugin-jsx": { | ||||||
|  |       "version": "8.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", | ||||||
|  |       "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "@babel/core": "^7.21.3", | ||||||
|  |         "@svgr/babel-preset": "8.1.0", | ||||||
|  |         "@svgr/hast-util-to-babel-ast": "8.0.0", | ||||||
|  |         "svg-parser": "^2.0.4" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=14" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/gregberge" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@svgr/core": "*" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@tanstack/query-core": { |     "node_modules/@tanstack/query-core": { | ||||||
|       "version": "4.29.1", |       "version": "4.29.1", | ||||||
|       "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.1.tgz", |       "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.1.tgz", | ||||||
| @@ -4579,7 +4824,8 @@ | |||||||
|     }, |     }, | ||||||
|     "node_modules/ansi-styles": { |     "node_modules/ansi-styles": { | ||||||
|       "version": "3.2.1", |       "version": "3.2.1", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", | ||||||
|  |       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "color-convert": "^1.9.0" |         "color-convert": "^1.9.0" | ||||||
|       }, |       }, | ||||||
| @@ -4754,9 +5000,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/axios": { |     "node_modules/axios": { | ||||||
|       "version": "1.3.5", |       "version": "1.6.1", | ||||||
|       "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", |       "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", | ||||||
|       "integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", |       "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "follow-redirects": "^1.15.0", |         "follow-redirects": "^1.15.0", | ||||||
|         "form-data": "^4.0.0", |         "form-data": "^4.0.0", | ||||||
| @@ -4963,6 +5209,18 @@ | |||||||
|         "node": ">=6" |         "node": ">=6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/camelcase": { | ||||||
|  |       "version": "6.3.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", | ||||||
|  |       "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=10" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/caniuse-lite": { |     "node_modules/caniuse-lite": { | ||||||
|       "version": "1.0.30001480", |       "version": "1.0.30001480", | ||||||
|       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz", |       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz", | ||||||
| @@ -5005,7 +5263,8 @@ | |||||||
|     }, |     }, | ||||||
|     "node_modules/chalk": { |     "node_modules/chalk": { | ||||||
|       "version": "2.4.2", |       "version": "2.4.2", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", | ||||||
|  |       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "ansi-styles": "^3.2.1", |         "ansi-styles": "^3.2.1", | ||||||
|         "escape-string-regexp": "^1.0.5", |         "escape-string-regexp": "^1.0.5", | ||||||
| @@ -5017,7 +5276,8 @@ | |||||||
|     }, |     }, | ||||||
|     "node_modules/chalk/node_modules/escape-string-regexp": { |     "node_modules/chalk/node_modules/escape-string-regexp": { | ||||||
|       "version": "1.0.5", |       "version": "1.0.5", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", | ||||||
|  |       "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=0.8.0" |         "node": ">=0.8.0" | ||||||
|       } |       } | ||||||
| @@ -5108,14 +5368,16 @@ | |||||||
|     }, |     }, | ||||||
|     "node_modules/color-convert": { |     "node_modules/color-convert": { | ||||||
|       "version": "1.9.3", |       "version": "1.9.3", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", | ||||||
|  |       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "color-name": "1.1.3" |         "color-name": "1.1.3" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/color-name": { |     "node_modules/color-name": { | ||||||
|       "version": "1.1.3", |       "version": "1.1.3", | ||||||
|       "license": "MIT" |       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", | ||||||
|  |       "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/colorette": { |     "node_modules/colorette": { | ||||||
|       "version": "2.0.20", |       "version": "2.0.20", | ||||||
| @@ -5389,6 +5651,16 @@ | |||||||
|         "csstype": "^3.0.2" |         "csstype": "^3.0.2" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/dot-case": { | ||||||
|  |       "version": "3.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", | ||||||
|  |       "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "no-case": "^3.0.4", | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/duplexer": { |     "node_modules/duplexer": { | ||||||
|       "version": "0.1.2", |       "version": "0.1.2", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
| @@ -5399,8 +5671,9 @@ | |||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/ejs": { |     "node_modules/ejs": { | ||||||
|       "version": "3.1.8", |       "version": "3.1.10", | ||||||
|       "license": "Apache-2.0", |       "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", | ||||||
|  |       "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "jake": "^10.8.5" |         "jake": "^10.8.5" | ||||||
|       }, |       }, | ||||||
| @@ -5421,6 +5694,18 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/entities": { | ||||||
|  |       "version": "4.5.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", | ||||||
|  |       "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", | ||||||
|  |       "dev": true, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.12" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/fb55/entities?sponsor=1" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/error-ex": { |     "node_modules/error-ex": { | ||||||
|       "version": "1.3.2", |       "version": "1.3.2", | ||||||
|       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", |       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", | ||||||
| @@ -6398,14 +6683,15 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/follow-redirects": { |     "node_modules/follow-redirects": { | ||||||
|       "version": "1.15.2", |       "version": "1.15.6", | ||||||
|  |       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", | ||||||
|  |       "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", | ||||||
|       "funding": [ |       "funding": [ | ||||||
|         { |         { | ||||||
|           "type": "individual", |           "type": "individual", | ||||||
|           "url": "https://github.com/sponsors/RubenVerborgh" |           "url": "https://github.com/sponsors/RubenVerborgh" | ||||||
|         } |         } | ||||||
|       ], |       ], | ||||||
|       "license": "MIT", |  | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=4.0" |         "node": ">=4.0" | ||||||
|       }, |       }, | ||||||
| @@ -6769,7 +7055,8 @@ | |||||||
|     }, |     }, | ||||||
|     "node_modules/has-flag": { |     "node_modules/has-flag": { | ||||||
|       "version": "3.0.0", |       "version": "3.0.0", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", | ||||||
|  |       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", | ||||||
|       "engines": { |       "engines": { | ||||||
|         "node": ">=4" |         "node": ">=4" | ||||||
|       } |       } | ||||||
| @@ -7934,6 +8221,15 @@ | |||||||
|         "loose-envify": "cli.js" |         "loose-envify": "cli.js" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/lower-case": { | ||||||
|  |       "version": "2.0.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", | ||||||
|  |       "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/lru-cache": { |     "node_modules/lru-cache": { | ||||||
|       "version": "6.0.0", |       "version": "6.0.0", | ||||||
|       "dev": true, |       "dev": true, | ||||||
| @@ -8073,6 +8369,16 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/no-case": { | ||||||
|  |       "version": "3.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", | ||||||
|  |       "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "lower-case": "^2.0.2", | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/node-fetch": { |     "node_modules/node-fetch": { | ||||||
|       "version": "2.6.7", |       "version": "2.6.7", | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
| @@ -8438,9 +8744,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/postcss": { |     "node_modules/postcss": { | ||||||
|       "version": "8.4.28", |       "version": "8.4.31", | ||||||
|       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", |       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", | ||||||
|       "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", |       "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", | ||||||
|       "funding": [ |       "funding": [ | ||||||
|         { |         { | ||||||
|           "type": "opencollective", |           "type": "opencollective", | ||||||
| @@ -9357,6 +9663,16 @@ | |||||||
|         "url": "https://github.com/chalk/ansi-styles?sponsor=1" |         "url": "https://github.com/chalk/ansi-styles?sponsor=1" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/snake-case": { | ||||||
|  |       "version": "3.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", | ||||||
|  |       "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "dot-case": "^3.0.4", | ||||||
|  |         "tslib": "^2.0.3" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/source-map": { |     "node_modules/source-map": { | ||||||
|       "version": "0.5.7", |       "version": "0.5.7", | ||||||
|       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", |       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", | ||||||
| @@ -9665,7 +9981,8 @@ | |||||||
|     }, |     }, | ||||||
|     "node_modules/supports-color": { |     "node_modules/supports-color": { | ||||||
|       "version": "5.5.0", |       "version": "5.5.0", | ||||||
|       "license": "MIT", |       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", | ||||||
|  |       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "has-flag": "^3.0.0" |         "has-flag": "^3.0.0" | ||||||
|       }, |       }, | ||||||
| @@ -9683,6 +10000,12 @@ | |||||||
|         "url": "https://github.com/sponsors/ljharb" |         "url": "https://github.com/sponsors/ljharb" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/svg-parser": { | ||||||
|  |       "version": "2.0.4", | ||||||
|  |       "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", | ||||||
|  |       "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", | ||||||
|  |       "dev": true | ||||||
|  |     }, | ||||||
|     "node_modules/temp": { |     "node_modules/temp": { | ||||||
|       "version": "0.9.4", |       "version": "0.9.4", | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
| @@ -10120,9 +10443,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/vite": { |     "node_modules/vite": { | ||||||
|       "version": "4.4.9", |       "version": "4.5.3", | ||||||
|       "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", |       "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", | ||||||
|       "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", |       "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "esbuild": "^0.18.10", |         "esbuild": "^0.18.10", | ||||||
|         "postcss": "^8.4.27", |         "postcss": "^8.4.27", | ||||||
| @@ -10196,6 +10519,20 @@ | |||||||
|         "workbox-window": "^6.5.4" |         "workbox-window": "^6.5.4" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/vite-plugin-svgr": { | ||||||
|  |       "version": "4.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz", | ||||||
|  |       "integrity": "sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "@rollup/pluginutils": "^5.0.5", | ||||||
|  |         "@svgr/core": "^8.1.0", | ||||||
|  |         "@svgr/plugin-jsx": "^8.1.0" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "vite": "^2.6.0 || 3 || 4 || 5" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/vite-tsconfig-paths": { |     "node_modules/vite-tsconfig-paths": { | ||||||
|       "version": "4.2.0", |       "version": "4.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.0.tgz", |       "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.0.tgz", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "ucentral-client", |   "name": "ucentral-client", | ||||||
|   "version": "2.11.0(7)", |   "version": "3.1.0(5)", | ||||||
|   "description": "", |   "description": "", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "main": "index.tsx", |   "main": "index.tsx", | ||||||
| @@ -94,6 +94,7 @@ | |||||||
|     "lint-staged": "^13.2.1", |     "lint-staged": "^13.2.1", | ||||||
|     "prettier": "^2.8.7", |     "prettier": "^2.8.7", | ||||||
|     "vite-plugin-pwa": "^0.14.7", |     "vite-plugin-pwa": "^0.14.7", | ||||||
|  |     "vite-plugin-svgr": "^4.2.0", | ||||||
|     "vite-tsconfig-paths": "^4.2.0" |     "vite-tsconfig-paths": "^4.2.0" | ||||||
|   }, |   }, | ||||||
|   "browserslist": { |   "browserslist": { | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								public/devices/asterfusion_CX204Y-24GT-M-SWP4.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 294 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/asterfusion_CX204Y-48GT-M-SWP4.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 394 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf186h.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 141 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf186w.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 138 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf188n-ca-ath12.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 245 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf188n-ca.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 245 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf188n-us.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 245 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf196-ca-ath12.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 239 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf196-ca.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 239 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf196-us.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 239 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf610d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 100 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cig_wf660a.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 100 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cybertan_eww631-a1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 123 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cybertan_eww631-b1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 79 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cybertan_skf224-c1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 31 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/cybertan_skf424-c1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 31 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_eap101-ath12.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 140 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_eap102-ath12.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 121 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_eap104.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 129 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_eap104_ath12.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 129 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_eap111.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 286 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs2100-10p.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 502 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs2100-10t.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 637 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs2100-28p.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 324 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs2100-28pp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 313 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs2100-28t.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 314 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs2100-52t.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 283 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs4125-10p.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 374 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs4125.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 33 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs4150-28t.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 267 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_ecs4150-58p.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 331 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_oap101-6e.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 194 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_oap101.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 194 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_oap101e-6e.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 133 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_oap101e.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 133 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/devices/edgecore_oap102.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 194 KiB | 
| @@ -269,6 +269,7 @@ | |||||||
| 		"map": "Karte", | 		"map": "Karte", | ||||||
| 		"max": "Max", | 		"max": "Max", | ||||||
| 		"min": "MINDEST", | 		"min": "MINDEST", | ||||||
|  | 		"miscellaneous": "Verschiedenes", | ||||||
| 		"mode": "Modus", | 		"mode": "Modus", | ||||||
| 		"model": "Modell", | 		"model": "Modell", | ||||||
| 		"modified": "Geändert", | 		"modified": "Geändert", | ||||||
| @@ -737,6 +738,7 @@ | |||||||
| 	"form": { | 	"form": { | ||||||
| 		"captive_web_root_explanation": "Bitte verwenden Sie nur .tar-Dateien (keine komprimierten Dateien wie z. B. .targz)", | 		"captive_web_root_explanation": "Bitte verwenden Sie nur .tar-Dateien (keine komprimierten Dateien wie z. B. .targz)", | ||||||
| 		"certificate_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN CERTIFICATE-----“ beginnt und mit „-----END CERTIFICATE-----“ endet.", | 		"certificate_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN CERTIFICATE-----“ beginnt und mit „-----END CERTIFICATE-----“ endet.", | ||||||
|  | 		"invalid_alphanumeric_with_dash": "Akzeptierte Zeichen. sind nur alphanumerisch (Buchstaben & Zahlen)", | ||||||
| 		"invalid_cidr": "Ungültige CIDR-IPv4-Adresse. Beispiel: 192.168.0.1/12", | 		"invalid_cidr": "Ungültige CIDR-IPv4-Adresse. Beispiel: 192.168.0.1/12", | ||||||
| 		"invalid_email": "Ungültige E-Mail", | 		"invalid_email": "Ungültige E-Mail", | ||||||
| 		"invalid_file_content": "Ungültiger Dateiinhalt, bitte bestätigen Sie, dass es sich um ein gültiges Format handelt", | 		"invalid_file_content": "Ungültiger Dateiinhalt, bitte bestätigen Sie, dass es sich um ein gültiges Format handelt", | ||||||
| @@ -763,7 +765,11 @@ | |||||||
| 		"invalid_static_ipv4_e": "Ungültige Adresse, dieser Bereich ist für Experimente reserviert (Klasse E). Das erste Oktett sollte 223 oder niedriger sein", | 		"invalid_static_ipv4_e": "Ungültige Adresse, dieser Bereich ist für Experimente reserviert (Klasse E). Das erste Oktett sollte 223 oder niedriger sein", | ||||||
| 		"invalid_third_party": "Ungültige Drittanbieter-JSON-Zeichenfolge. Bitte bestätigen Sie, dass Ihr Wert ein gültiges JSON ist", | 		"invalid_third_party": "Ungültige Drittanbieter-JSON-Zeichenfolge. Bitte bestätigen Sie, dass Ihr Wert ein gültiges JSON ist", | ||||||
| 		"key_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN PRIVATE KEY-----“ beginnt und mit „-----END PRIVATE KEY-----“ endet.", | 		"key_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN PRIVATE KEY-----“ beginnt und mit „-----END PRIVATE KEY-----“ endet.", | ||||||
|  | 		"max_length": "Maximale Länge von {{max}} Zeichen.", | ||||||
|  | 		"max_value": "Maximalwert von {{max}}", | ||||||
|  | 		"min_length": "Mindestlänge von {{min}} Zeichen.", | ||||||
| 		"min_max_string": "Der Wert muss eine Länge zwischen {{min}} (einschließlich) und {{max}} (einschließlich) haben.", | 		"min_max_string": "Der Wert muss eine Länge zwischen {{min}} (einschließlich) und {{max}} (einschließlich) haben.", | ||||||
|  | 		"min_value": "Mindestwert von {{min}}", | ||||||
| 		"missing_interface_upstream": "Sie müssen mindestens eine Upstream-Schnittstelle haben. Im Moment sind alle Ihre Schnittstellen nachgelagert", | 		"missing_interface_upstream": "Sie müssen mindestens eine Upstream-Schnittstelle haben. Im Moment sind alle Ihre Schnittstellen nachgelagert", | ||||||
| 		"new_email_to_notify": "Neue E-Mail zur Benachrichtigung", | 		"new_email_to_notify": "Neue E-Mail zur Benachrichtigung", | ||||||
| 		"new_phone_to_notify": "Neues Telefon zu benachrichtigen", | 		"new_phone_to_notify": "Neues Telefon zu benachrichtigen", | ||||||
| @@ -905,6 +911,11 @@ | |||||||
| 		"one": "Benachrichtigung", | 		"one": "Benachrichtigung", | ||||||
| 		"other": "Benachrichtigungen" | 		"other": "Benachrichtigungen" | ||||||
| 	}, | 	}, | ||||||
|  | 	"openroaming": { | ||||||
|  | 		"pool_strategy": "Pool-Strategie", | ||||||
|  | 		"radius_endpoint_one": "Radiusendpunkt", | ||||||
|  | 		"radius_endpoint_other": "Radiusendpunkte" | ||||||
|  | 	}, | ||||||
| 	"operator": { | 	"operator": { | ||||||
| 		"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar", | 		"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar", | ||||||
| 		"delete_operator": "Betreiber löschen", | 		"delete_operator": "Betreiber löschen", | ||||||
| @@ -970,6 +981,27 @@ | |||||||
| 		"title": "Beschränkungen", | 		"title": "Beschränkungen", | ||||||
| 		"tty": "TTY-Zugriff" | 		"tty": "TTY-Zugriff" | ||||||
| 	}, | 	}, | ||||||
|  | 	"roaming": { | ||||||
|  | 		"account_created": "Neues Konto erstellt!", | ||||||
|  | 		"account_deleted": "Konto gelöscht!", | ||||||
|  | 		"account_one": "Konto", | ||||||
|  | 		"account_other": "Konten", | ||||||
|  | 		"certificate_deleted": "Zertifikat gelöscht!", | ||||||
|  | 		"certificate_one": "Zertifikat", | ||||||
|  | 		"certificate_other": "Zertifikate", | ||||||
|  | 		"city": "Stadt", | ||||||
|  | 		"common_name": "Gemeinsamen Namen", | ||||||
|  | 		"country": "Land", | ||||||
|  | 		"global_reach": "Globale Reichweite", | ||||||
|  | 		"global_reach_account_id": "Konto-ID", | ||||||
|  | 		"invalid_certificate": "Ungültiges Zertifikat", | ||||||
|  | 		"invalid_key": "Ungültiger privater Schlüssel", | ||||||
|  | 		"location_details_title": "Ort", | ||||||
|  | 		"organization": "Organisation", | ||||||
|  | 		"private_key": "Privat Schlüssel", | ||||||
|  | 		"province": "Provinz", | ||||||
|  | 		"state": "Zustand" | ||||||
|  | 	}, | ||||||
| 	"rrm": { | 	"rrm": { | ||||||
| 		"algorithm": "Algorithmus", | 		"algorithm": "Algorithmus", | ||||||
| 		"algorithm_other": "Algorithmen", | 		"algorithm_other": "Algorithmen", | ||||||
| @@ -1091,6 +1123,7 @@ | |||||||
| 		"title": "Abonnenten" | 		"title": "Abonnenten" | ||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
|  | 		"advanced": "Erweitert", | ||||||
| 		"backend_logs": "Back-End-Protokolle", | 		"backend_logs": "Back-End-Protokolle", | ||||||
| 		"configuration": "Aufbau", | 		"configuration": "Aufbau", | ||||||
| 		"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden", | 		"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden", | ||||||
|   | |||||||
| @@ -269,6 +269,7 @@ | |||||||
| 		"map": "Map", | 		"map": "Map", | ||||||
| 		"max": "Max", | 		"max": "Max", | ||||||
| 		"min": "Min", | 		"min": "Min", | ||||||
|  | 		"miscellaneous": "Miscellaneous", | ||||||
| 		"mode": "Mode", | 		"mode": "Mode", | ||||||
| 		"model": "Model", | 		"model": "Model", | ||||||
| 		"modified": "Modified", | 		"modified": "Modified", | ||||||
| @@ -737,6 +738,7 @@ | |||||||
| 	"form": { | 	"form": { | ||||||
| 		"captive_web_root_explanation": "Please use .tar files only (no compressed files like .targz, for example)", | 		"captive_web_root_explanation": "Please use .tar files only (no compressed files like .targz, for example)", | ||||||
| 		"certificate_file_explanation": "Please use a .pem file that starts with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"", | 		"certificate_file_explanation": "Please use a .pem file that starts with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"", | ||||||
|  | 		"invalid_alphanumeric_with_dash": "Accepted chars. are only alphanumeric (letters & numbers)", | ||||||
| 		"invalid_cidr": "Invalid CIDR IPv4 address. Example: 192.168.0.1/12", | 		"invalid_cidr": "Invalid CIDR IPv4 address. Example: 192.168.0.1/12", | ||||||
| 		"invalid_email": "Invalid Email", | 		"invalid_email": "Invalid Email", | ||||||
| 		"invalid_file_content": "Invalid file content, please confirm that it is of the valid format", | 		"invalid_file_content": "Invalid file content, please confirm that it is of the valid format", | ||||||
| @@ -763,7 +765,11 @@ | |||||||
| 		"invalid_static_ipv4_e": "Invalid address, this range reserved for experiments (class E). The first octet should be 223 or lower", | 		"invalid_static_ipv4_e": "Invalid address, this range reserved for experiments (class E). The first octet should be 223 or lower", | ||||||
| 		"invalid_third_party": "Invalid Third Party JSON string. Please confirm that your value is a valid JSON", | 		"invalid_third_party": "Invalid Third Party JSON string. Please confirm that your value is a valid JSON", | ||||||
| 		"key_file_explanation": "Please use a .pem file that starts with \"-----BEGIN PRIVATE KEY-----\" and ends with \"-----END PRIVATE KEY-----\"", | 		"key_file_explanation": "Please use a .pem file that starts with \"-----BEGIN PRIVATE KEY-----\" and ends with \"-----END PRIVATE KEY-----\"", | ||||||
|  | 		"max_length": "Maximum length of {{max}} chars.", | ||||||
|  | 		"max_value": "Maximum value of {{max}}", | ||||||
|  | 		"min_length": "Minimum length of {{min}} chars.", | ||||||
| 		"min_max_string": "Value needs to be of a length between {{min}} (inclusive) and {{max}} (inclusive)", | 		"min_max_string": "Value needs to be of a length between {{min}} (inclusive) and {{max}} (inclusive)", | ||||||
|  | 		"min_value": "Minimum value of {{min}}", | ||||||
| 		"missing_interface_upstream": "You need to have at least one upstream interface. At the moment, all your interfaces are downstream", | 		"missing_interface_upstream": "You need to have at least one upstream interface. At the moment, all your interfaces are downstream", | ||||||
| 		"new_email_to_notify": "New email to notify", | 		"new_email_to_notify": "New email to notify", | ||||||
| 		"new_phone_to_notify": "New phone to notify", | 		"new_phone_to_notify": "New phone to notify", | ||||||
| @@ -905,6 +911,11 @@ | |||||||
| 		"one": "Notification", | 		"one": "Notification", | ||||||
| 		"other": "Notifications" | 		"other": "Notifications" | ||||||
| 	}, | 	}, | ||||||
|  | 	"openroaming": { | ||||||
|  | 		"pool_strategy": "Pool Strategy", | ||||||
|  | 		"radius_endpoint_one": "Radius Endpoint", | ||||||
|  | 		"radius_endpoint_other": "Radius Endpoints" | ||||||
|  | 	}, | ||||||
| 	"operator": { | 	"operator": { | ||||||
| 		"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible", | 		"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible", | ||||||
| 		"delete_operator": "Delete Operator", | 		"delete_operator": "Delete Operator", | ||||||
| @@ -970,6 +981,27 @@ | |||||||
| 		"title": "Restrictions", | 		"title": "Restrictions", | ||||||
| 		"tty": "TTY Access" | 		"tty": "TTY Access" | ||||||
| 	}, | 	}, | ||||||
|  | 	"roaming": { | ||||||
|  | 		"account_created": "New account created!", | ||||||
|  | 		"account_deleted": "Deleted account!", | ||||||
|  | 		"account_one": "Account", | ||||||
|  | 		"account_other": "Accounts", | ||||||
|  | 		"certificate_deleted": "Deleted certificate!", | ||||||
|  | 		"certificate_one": "Certificate", | ||||||
|  | 		"certificate_other": "Certificates", | ||||||
|  | 		"city": "City", | ||||||
|  | 		"common_name": "Common Name", | ||||||
|  | 		"country": "Country", | ||||||
|  | 		"global_reach": "GlobalReach", | ||||||
|  | 		"global_reach_account_id": " Account ID", | ||||||
|  | 		"invalid_certificate": "Invalid certificate", | ||||||
|  | 		"invalid_key": "Invalid private key", | ||||||
|  | 		"location_details_title": "Location", | ||||||
|  | 		"organization": "Organization", | ||||||
|  | 		"private_key": "Private Key", | ||||||
|  | 		"province": "Province", | ||||||
|  | 		"state": "State" | ||||||
|  | 	}, | ||||||
| 	"rrm": { | 	"rrm": { | ||||||
| 		"algorithm": "Algorithm", | 		"algorithm": "Algorithm", | ||||||
| 		"algorithm_other": "Algorithms", | 		"algorithm_other": "Algorithms", | ||||||
| @@ -1091,6 +1123,7 @@ | |||||||
| 		"title": "Subscribers" | 		"title": "Subscribers" | ||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
|  | 		"advanced": "Advanced", | ||||||
| 		"backend_logs": "Back-End Logs", | 		"backend_logs": "Back-End Logs", | ||||||
| 		"configuration": "Configuration", | 		"configuration": "Configuration", | ||||||
| 		"could_not_retrieve": "Error: could not retrieve {{name}} system information", | 		"could_not_retrieve": "Error: could not retrieve {{name}} system information", | ||||||
|   | |||||||
| @@ -269,6 +269,7 @@ | |||||||
| 		"map": "Mapa", | 		"map": "Mapa", | ||||||
| 		"max": "Max", | 		"max": "Max", | ||||||
| 		"min": "Min", | 		"min": "Min", | ||||||
|  | 		"miscellaneous": "Diverso", | ||||||
| 		"mode": "Modo", | 		"mode": "Modo", | ||||||
| 		"model": "Modelo", | 		"model": "Modelo", | ||||||
| 		"modified": "Modificado", | 		"modified": "Modificado", | ||||||
| @@ -737,6 +738,7 @@ | |||||||
| 	"form": { | 	"form": { | ||||||
| 		"captive_web_root_explanation": "Utilice únicamente archivos .tar (no archivos comprimidos como .targz, por ejemplo)", | 		"captive_web_root_explanation": "Utilice únicamente archivos .tar (no archivos comprimidos como .targz, por ejemplo)", | ||||||
| 		"certificate_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN CERTIFICATE-----\" y termine con \"-----END CERTIFICATE-----\"", | 		"certificate_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN CERTIFICATE-----\" y termine con \"-----END CERTIFICATE-----\"", | ||||||
|  | 		"invalid_alphanumeric_with_dash": "Caracteres aceptados. son solo alfanuméricos (letras y números)", | ||||||
| 		"invalid_cidr": "Dirección IPv4 CIDR no válida. Ejemplo: 192.168.0.1/12", | 		"invalid_cidr": "Dirección IPv4 CIDR no válida. Ejemplo: 192.168.0.1/12", | ||||||
| 		"invalid_email": "Email inválido", | 		"invalid_email": "Email inválido", | ||||||
| 		"invalid_file_content": "Contenido de archivo no válido, confirme que tiene un formato válido", | 		"invalid_file_content": "Contenido de archivo no válido, confirme que tiene un formato válido", | ||||||
| @@ -763,7 +765,11 @@ | |||||||
| 		"invalid_static_ipv4_e": "Dirección no válida, este rango reservado para experimentos (clase E). El primer octeto debe ser 223 o inferior", | 		"invalid_static_ipv4_e": "Dirección no válida, este rango reservado para experimentos (clase E). El primer octeto debe ser 223 o inferior", | ||||||
| 		"invalid_third_party": "Cadena JSON de terceros no válida. Confirme que su valor es un JSON válido", | 		"invalid_third_party": "Cadena JSON de terceros no válida. Confirme que su valor es un JSON válido", | ||||||
| 		"key_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN PRIVATE KEY-----\" y termine con \"-----END PRIVATE KEY-----\"", | 		"key_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN PRIVATE KEY-----\" y termine con \"-----END PRIVATE KEY-----\"", | ||||||
|  | 		"max_length": "Longitud máxima de {{max}} caracteres.", | ||||||
|  | 		"max_value": "Valor máximo de {{max}}", | ||||||
|  | 		"min_length": "Longitud mínima de {{min}} caracteres.", | ||||||
| 		"min_max_string": "El valor debe tener una longitud entre {{min}} (inclusive) y {{max}} (inclusive)", | 		"min_max_string": "El valor debe tener una longitud entre {{min}} (inclusive) y {{max}} (inclusive)", | ||||||
|  | 		"min_value": "Valor mínimo de {{min}}", | ||||||
| 		"missing_interface_upstream": "Debe tener al menos una interfaz ascendente. Por el momento, todas sus interfaces están en sentido descendente", | 		"missing_interface_upstream": "Debe tener al menos una interfaz ascendente. Por el momento, todas sus interfaces están en sentido descendente", | ||||||
| 		"new_email_to_notify": "Nuevo correo electrónico para notificar", | 		"new_email_to_notify": "Nuevo correo electrónico para notificar", | ||||||
| 		"new_phone_to_notify": "Nuevo teléfono para avisar", | 		"new_phone_to_notify": "Nuevo teléfono para avisar", | ||||||
| @@ -905,6 +911,11 @@ | |||||||
| 		"one": "Notificación", | 		"one": "Notificación", | ||||||
| 		"other": "Notificaciones" | 		"other": "Notificaciones" | ||||||
| 	}, | 	}, | ||||||
|  | 	"openroaming": { | ||||||
|  | 		"pool_strategy": "Estrategia de piscina", | ||||||
|  | 		"radius_endpoint_one": "Punto final del radio", | ||||||
|  | 		"radius_endpoint_other": "Puntos finales de radio" | ||||||
|  | 	}, | ||||||
| 	"operator": { | 	"operator": { | ||||||
| 		"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.", | 		"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.", | ||||||
| 		"delete_operator": "Eliminar operador", | 		"delete_operator": "Eliminar operador", | ||||||
| @@ -970,6 +981,27 @@ | |||||||
| 		"title": "Las restricciones", | 		"title": "Las restricciones", | ||||||
| 		"tty": "Acceso TTY" | 		"tty": "Acceso TTY" | ||||||
| 	}, | 	}, | ||||||
|  | 	"roaming": { | ||||||
|  | 		"account_created": "¡Nueva cuenta creada!", | ||||||
|  | 		"account_deleted": "¡Cuenta eliminada!", | ||||||
|  | 		"account_one": "Cuenta", | ||||||
|  | 		"account_other": "Cuentas", | ||||||
|  | 		"certificate_deleted": "Certificado eliminado!", | ||||||
|  | 		"certificate_one": "Certificado", | ||||||
|  | 		"certificate_other": "Certificados", | ||||||
|  | 		"city": "ciudad", | ||||||
|  | 		"common_name": "Nombre común", | ||||||
|  | 		"country": "País", | ||||||
|  | 		"global_reach": "Alcance global", | ||||||
|  | 		"global_reach_account_id": "ID de cuenta ", | ||||||
|  | 		"invalid_certificate": "Certificado inválido", | ||||||
|  | 		"invalid_key": "Clave privada no válida", | ||||||
|  | 		"location_details_title": "Ubicación", | ||||||
|  | 		"organization": "Organización", | ||||||
|  | 		"private_key": "Llave privada", | ||||||
|  | 		"province": "Provincia", | ||||||
|  | 		"state": "Estado" | ||||||
|  | 	}, | ||||||
| 	"rrm": { | 	"rrm": { | ||||||
| 		"algorithm": "Algoritmo", | 		"algorithm": "Algoritmo", | ||||||
| 		"algorithm_other": "Algoritmos", | 		"algorithm_other": "Algoritmos", | ||||||
| @@ -1091,6 +1123,7 @@ | |||||||
| 		"title": "Suscriptores" | 		"title": "Suscriptores" | ||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
|  | 		"advanced": "Avanzado", | ||||||
| 		"backend_logs": "Registros de back-end", | 		"backend_logs": "Registros de back-end", | ||||||
| 		"configuration": "Configuración", | 		"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}} ", | ||||||
|   | |||||||
| @@ -269,6 +269,7 @@ | |||||||
| 		"map": "Carte", | 		"map": "Carte", | ||||||
| 		"max": "Max", | 		"max": "Max", | ||||||
| 		"min": "Min", | 		"min": "Min", | ||||||
|  | 		"miscellaneous": "Divers", | ||||||
| 		"mode": "Mode", | 		"mode": "Mode", | ||||||
| 		"model": "Modèle", | 		"model": "Modèle", | ||||||
| 		"modified": "Modifié", | 		"modified": "Modifié", | ||||||
| @@ -737,6 +738,7 @@ | |||||||
| 	"form": { | 	"form": { | ||||||
| 		"captive_web_root_explanation": "Veuillez utiliser uniquement des fichiers .tar (pas de fichiers compressés comme .targz, par exemple)", | 		"captive_web_root_explanation": "Veuillez utiliser uniquement des fichiers .tar (pas de fichiers compressés comme .targz, par exemple)", | ||||||
| 		"certificate_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN CERTIFICATE-----\" et se termine par \"-----END CERTIFICATE-----\"", | 		"certificate_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN CERTIFICATE-----\" et se termine par \"-----END CERTIFICATE-----\"", | ||||||
|  | 		"invalid_alphanumeric_with_dash": "Caractères acceptés. sont uniquement alphanumériques (lettres et chiffres)", | ||||||
| 		"invalid_cidr": "Adresse IPv4 CIDR non valide. Exemple : 192.168.0.1/12", | 		"invalid_cidr": "Adresse IPv4 CIDR non valide. Exemple : 192.168.0.1/12", | ||||||
| 		"invalid_email": "Email Invalide", | 		"invalid_email": "Email Invalide", | ||||||
| 		"invalid_file_content": "Contenu de fichier non valide, veuillez confirmer qu'il est au format valide", | 		"invalid_file_content": "Contenu de fichier non valide, veuillez confirmer qu'il est au format valide", | ||||||
| @@ -763,7 +765,11 @@ | |||||||
| 		"invalid_static_ipv4_e": "Adresse invalide, cette plage est réservée aux expérimentations (classe E). Le premier octet doit être 223 ou moins", | 		"invalid_static_ipv4_e": "Adresse invalide, cette plage est réservée aux expérimentations (classe E). Le premier octet doit être 223 ou moins", | ||||||
| 		"invalid_third_party": "Chaîne JSON tierce non valide. Veuillez confirmer que votre valeur est un JSON valide", | 		"invalid_third_party": "Chaîne JSON tierce non valide. Veuillez confirmer que votre valeur est un JSON valide", | ||||||
| 		"key_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN PRIVATE KEY-----\" et se termine par \"-----END PRIVATE KEY-----\"", | 		"key_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN PRIVATE KEY-----\" et se termine par \"-----END PRIVATE KEY-----\"", | ||||||
|  | 		"max_length": "Longueur maximale de {{max}}  caractères.", | ||||||
|  | 		"max_value": "Valeur maximale de  {{max}}", | ||||||
|  | 		"min_length": "Longueur minimale de {{min}}  caractères.", | ||||||
| 		"min_max_string": "La valeur doit être d'une longueur comprise entre {{min}} (inclus) et {{max}} (inclus)", | 		"min_max_string": "La valeur doit être d'une longueur comprise entre {{min}} (inclus) et {{max}} (inclus)", | ||||||
|  | 		"min_value": "Valeur minimale de  {{min}}", | ||||||
| 		"missing_interface_upstream": "Vous devez avoir au moins une interface en amont. Pour le moment, toutes vos interfaces sont en aval", | 		"missing_interface_upstream": "Vous devez avoir au moins une interface en amont. Pour le moment, toutes vos interfaces sont en aval", | ||||||
| 		"new_email_to_notify": "Nouvel e-mail à notifier", | 		"new_email_to_notify": "Nouvel e-mail à notifier", | ||||||
| 		"new_phone_to_notify": "Nouveau téléphone à notifier", | 		"new_phone_to_notify": "Nouveau téléphone à notifier", | ||||||
| @@ -905,6 +911,11 @@ | |||||||
| 		"one": "Notification", | 		"one": "Notification", | ||||||
| 		"other": "Les notifications" | 		"other": "Les notifications" | ||||||
| 	}, | 	}, | ||||||
|  | 	"openroaming": { | ||||||
|  | 		"pool_strategy": "Stratégie de pool", | ||||||
|  | 		"radius_endpoint_one": "Point final de rayon", | ||||||
|  | 		"radius_endpoint_other": "Points de terminaison du rayon" | ||||||
|  | 	}, | ||||||
| 	"operator": { | 	"operator": { | ||||||
| 		"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible", | 		"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible", | ||||||
| 		"delete_operator": "Supprimer l'opérateur", | 		"delete_operator": "Supprimer l'opérateur", | ||||||
| @@ -970,6 +981,27 @@ | |||||||
| 		"title": "Restrictions", | 		"title": "Restrictions", | ||||||
| 		"tty": "Accès ATS" | 		"tty": "Accès ATS" | ||||||
| 	}, | 	}, | ||||||
|  | 	"roaming": { | ||||||
|  | 		"account_created": "Nouveau compte créé !", | ||||||
|  | 		"account_deleted": "Compte supprimé !", | ||||||
|  | 		"account_one": "Compte", | ||||||
|  | 		"account_other": "Comptes", | ||||||
|  | 		"certificate_deleted": "Certificat supprimé !", | ||||||
|  | 		"certificate_one": "Certificat", | ||||||
|  | 		"certificate_other": "Certificats", | ||||||
|  | 		"city": "Ville", | ||||||
|  | 		"common_name": "Nom commun", | ||||||
|  | 		"country": "Pays", | ||||||
|  | 		"global_reach": "Portée mondiale", | ||||||
|  | 		"global_reach_account_id": "ID de compte ", | ||||||
|  | 		"invalid_certificate": "certificat invalide", | ||||||
|  | 		"invalid_key": "Clé privée invalide", | ||||||
|  | 		"location_details_title": "Emplacement", | ||||||
|  | 		"organization": "Organisation", | ||||||
|  | 		"private_key": "Clé privée", | ||||||
|  | 		"province": "province", | ||||||
|  | 		"state": "Etat" | ||||||
|  | 	}, | ||||||
| 	"rrm": { | 	"rrm": { | ||||||
| 		"algorithm": "Algorithme", | 		"algorithm": "Algorithme", | ||||||
| 		"algorithm_other": "Algorithmes", | 		"algorithm_other": "Algorithmes", | ||||||
| @@ -1091,6 +1123,7 @@ | |||||||
| 		"title": "Les abonnés" | 		"title": "Les abonnés" | ||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
|  | 		"advanced": "Avancée", | ||||||
| 		"backend_logs": "Journaux principaux", | 		"backend_logs": "Journaux principaux", | ||||||
| 		"configuration": "Configuration", | 		"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}} ", | ||||||
|   | |||||||
| @@ -269,6 +269,7 @@ | |||||||
| 		"map": "Mapa", | 		"map": "Mapa", | ||||||
| 		"max": "máximo", | 		"max": "máximo", | ||||||
| 		"min": "minuto", | 		"min": "minuto", | ||||||
|  | 		"miscellaneous": "Diversos", | ||||||
| 		"mode": "Modo", | 		"mode": "Modo", | ||||||
| 		"model": "Modelo", | 		"model": "Modelo", | ||||||
| 		"modified": "Modificado", | 		"modified": "Modificado", | ||||||
| @@ -737,6 +738,7 @@ | |||||||
| 	"form": { | 	"form": { | ||||||
| 		"captive_web_root_explanation": "Por favor, use apenas arquivos .tar (sem arquivos compactados como .targz, por exemplo)", | 		"captive_web_root_explanation": "Por favor, use apenas arquivos .tar (sem arquivos compactados como .targz, por exemplo)", | ||||||
| 		"certificate_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN CERTIFICATE-----\" e termine com \"-----END CERTIFICATE-----\"", | 		"certificate_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN CERTIFICATE-----\" e termine com \"-----END CERTIFICATE-----\"", | ||||||
|  | 		"invalid_alphanumeric_with_dash": "Caracteres aceitos. são apenas alfanuméricos (letras e números)", | ||||||
| 		"invalid_cidr": "Endereço CIDR IPv4 inválido. Exemplo: 192.168.0.1/12", | 		"invalid_cidr": "Endereço CIDR IPv4 inválido. Exemplo: 192.168.0.1/12", | ||||||
| 		"invalid_email": "E-mail inválido", | 		"invalid_email": "E-mail inválido", | ||||||
| 		"invalid_file_content": "Conteúdo de arquivo inválido. Confirme se está no formato válido", | 		"invalid_file_content": "Conteúdo de arquivo inválido. Confirme se está no formato válido", | ||||||
| @@ -763,7 +765,11 @@ | |||||||
| 		"invalid_static_ipv4_e": "Endereço inválido, este intervalo é reservado para experimentos (classe E). O primeiro octeto deve ser 223 ou inferior", | 		"invalid_static_ipv4_e": "Endereço inválido, este intervalo é reservado para experimentos (classe E). O primeiro octeto deve ser 223 ou inferior", | ||||||
| 		"invalid_third_party": "String JSON de terceiros inválida. Confirme se seu valor é um JSON válido", | 		"invalid_third_party": "String JSON de terceiros inválida. Confirme se seu valor é um JSON válido", | ||||||
| 		"key_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN PRIVATE KEY-----\" e termine com \"-----END PRIVATE KEY-----\"", | 		"key_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN PRIVATE KEY-----\" e termine com \"-----END PRIVATE KEY-----\"", | ||||||
|  | 		"max_length": "Comprimento máximo de {{max}} caracteres.", | ||||||
|  | 		"max_value": "Valor máximo de {{max}}", | ||||||
|  | 		"min_length": "Comprimento mínimo de {{min}} caracteres.", | ||||||
| 		"min_max_string": "O valor precisa ter um comprimento entre {{min}} (inclusive) e {{max}} (inclusive)", | 		"min_max_string": "O valor precisa ter um comprimento entre {{min}} (inclusive) e {{max}} (inclusive)", | ||||||
|  | 		"min_value": "Valor mínimo de {{min}}", | ||||||
| 		"missing_interface_upstream": "Você precisa ter pelo menos uma interface upstream. No momento, todas as suas interfaces estão downstream", | 		"missing_interface_upstream": "Você precisa ter pelo menos uma interface upstream. No momento, todas as suas interfaces estão downstream", | ||||||
| 		"new_email_to_notify": "Novo e-mail para notificar", | 		"new_email_to_notify": "Novo e-mail para notificar", | ||||||
| 		"new_phone_to_notify": "Novo telefone para notificar", | 		"new_phone_to_notify": "Novo telefone para notificar", | ||||||
| @@ -905,6 +911,11 @@ | |||||||
| 		"one": "Notificação", | 		"one": "Notificação", | ||||||
| 		"other": "Notificações" | 		"other": "Notificações" | ||||||
| 	}, | 	}, | ||||||
|  | 	"openroaming": { | ||||||
|  | 		"pool_strategy": "Estratégia de pool", | ||||||
|  | 		"radius_endpoint_one": "Ponto final do raio", | ||||||
|  | 		"radius_endpoint_other": "Pontos finais de raio" | ||||||
|  | 	}, | ||||||
| 	"operator": { | 	"operator": { | ||||||
| 		"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível", | 		"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível", | ||||||
| 		"delete_operator": "Excluir operador", | 		"delete_operator": "Excluir operador", | ||||||
| @@ -970,6 +981,27 @@ | |||||||
| 		"title": "RESTRIÇÕES", | 		"title": "RESTRIÇÕES", | ||||||
| 		"tty": "Acesso TTY" | 		"tty": "Acesso TTY" | ||||||
| 	}, | 	}, | ||||||
|  | 	"roaming": { | ||||||
|  | 		"account_created": "Nova conta criada!", | ||||||
|  | 		"account_deleted": "Conta excluída!", | ||||||
|  | 		"account_one": "Conta", | ||||||
|  | 		"account_other": "Contas", | ||||||
|  | 		"certificate_deleted": "Certificado excluído!", | ||||||
|  | 		"certificate_one": "Certificado", | ||||||
|  | 		"certificate_other": "Certificados", | ||||||
|  | 		"city": "Cidade", | ||||||
|  | 		"common_name": "Nome comum", | ||||||
|  | 		"country": "País", | ||||||
|  | 		"global_reach": "Alcance global", | ||||||
|  | 		"global_reach_account_id": "ID da conta", | ||||||
|  | 		"invalid_certificate": "Certificado inválido", | ||||||
|  | 		"invalid_key": "Chave privada inválida", | ||||||
|  | 		"location_details_title": "Localização", | ||||||
|  | 		"organization": "Organização", | ||||||
|  | 		"private_key": "Chave privada", | ||||||
|  | 		"province": "província", | ||||||
|  | 		"state": "Estado" | ||||||
|  | 	}, | ||||||
| 	"rrm": { | 	"rrm": { | ||||||
| 		"algorithm": "Algoritmo", | 		"algorithm": "Algoritmo", | ||||||
| 		"algorithm_other": "Algoritmos", | 		"algorithm_other": "Algoritmos", | ||||||
| @@ -1091,6 +1123,7 @@ | |||||||
| 		"title": "Inscritos" | 		"title": "Inscritos" | ||||||
| 	}, | 	}, | ||||||
| 	"system": { | 	"system": { | ||||||
|  | 		"advanced": "Avançado", | ||||||
| 		"backend_logs": "Registros de back-end", | 		"backend_logs": "Registros de back-end", | ||||||
| 		"configuration": "Configuração", | 		"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", | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								src/@tanstack.react-table.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -4,18 +4,22 @@ import '@tanstack/react-table'; | |||||||
|  |  | ||||||
| declare module '@tanstack/table-core' { | declare module '@tanstack/table-core' { | ||||||
|   interface ColumnMeta<TData extends RowData, TValue> { |   interface ColumnMeta<TData extends RowData, TValue> { | ||||||
|  |     ref?: React.MutableRefObject<HTMLTableCellElement | null>; | ||||||
|  |     customMinWidth?: string; | ||||||
|  |     anchored?: boolean; | ||||||
|     stopPropagation?: boolean; |     stopPropagation?: boolean; | ||||||
|     alwaysShow?: boolean; |     alwaysShow?: boolean; | ||||||
|     anchored?: boolean; |  | ||||||
|     hasPopover?: boolean; |     hasPopover?: boolean; | ||||||
|     customMaxWidth?: string; |     customMaxWidth?: string; | ||||||
|     customMinWidth?: string; |  | ||||||
|     customWidth?: string; |     customWidth?: string; | ||||||
|     isMonospace?: boolean; |     isMonospace?: boolean; | ||||||
|     isCentered?: boolean; |     isCentered?: boolean; | ||||||
|     columnSelectorOptions?: { |     columnSelectorOptions?: { | ||||||
|       label?: string; |       label?: string; | ||||||
|     }; |     }; | ||||||
|  |     rowContentOptions?: { | ||||||
|  |       style?: React.CSSProperties; | ||||||
|  |     }; | ||||||
|     headerOptions?: { |     headerOptions?: { | ||||||
|       tooltip?: string; |       tooltip?: string; | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -54,6 +54,7 @@ const DeviceActionDropdown = ({ | |||||||
| }: Props) => { | }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |   const deviceType = device?.deviceType ?? 'ap'; | ||||||
|   const connectColor = useColorModeValue('blackAlpha', 'gray'); |   const connectColor = useColorModeValue('blackAlpha', 'gray'); | ||||||
|   const addEventListeners = useControllerStore((state) => state.addEventListeners); |   const addEventListeners = useControllerStore((state) => state.addEventListeners); | ||||||
|   const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({ |   const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({ | ||||||
| @@ -205,7 +206,7 @@ const DeviceActionDropdown = ({ | |||||||
|           isDisabled={isDisabled} |           isDisabled={isDisabled} | ||||||
|           onClick={handleOpenScan} |           onClick={handleOpenScan} | ||||||
|           colorScheme="teal" |           colorScheme="teal" | ||||||
|           hidden={isCompact} |           hidden={isCompact || deviceType !== 'ap'} | ||||||
|         /> |         /> | ||||||
|       </Tooltip> |       </Tooltip> | ||||||
|       <Menu> |       <Menu> | ||||||
| @@ -221,7 +222,7 @@ const DeviceActionDropdown = ({ | |||||||
|         <Portal> |         <Portal> | ||||||
|           <MenuList maxH="315px" overflowY="auto"> |           <MenuList maxH="315px" overflowY="auto"> | ||||||
|             <MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem> |             <MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem> | ||||||
|             <MenuItem onClick={handleOpenConfigure} hidden={!isCompact}> |             <MenuItem onClick={handleOpenConfigure} hidden={!isCompact || deviceType !== 'ap'}> | ||||||
|               {t('controller.configure.title')} |               {t('controller.configure.title')} | ||||||
|             </MenuItem> |             </MenuItem> | ||||||
|             <MenuItem onClick={handleConnectClick} hidden={!isCompact}> |             <MenuItem onClick={handleConnectClick} hidden={!isCompact}> | ||||||
| @@ -239,7 +240,7 @@ const DeviceActionDropdown = ({ | |||||||
|             <MenuItem onClick={handleUpdateToLatest} hidden> |             <MenuItem onClick={handleUpdateToLatest} hidden> | ||||||
|               {t('premium.toolbox.upgrade_to_latest')} |               {t('premium.toolbox.upgrade_to_latest')} | ||||||
|             </MenuItem> |             </MenuItem> | ||||||
|             <MenuItem onClick={handleOpenScan} hidden={!isCompact}> |             <MenuItem onClick={handleOpenScan} hidden={!isCompact || deviceType !== 'ap'}> | ||||||
|               {t('commands.wifiscan')} |               {t('commands.wifiscan')} | ||||||
|             </MenuItem> |             </MenuItem> | ||||||
|           </MenuList> |           </MenuList> | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ export const ResponsiveTag = React.memo(({ label, icon, tooltip, isCompact, ...p | |||||||
|   return ( |   return ( | ||||||
|     <Tooltip label={tooltip ?? label}> |     <Tooltip label={tooltip ?? label}> | ||||||
|       <Tag size="lg" colorScheme="blue" {...props}> |       <Tag size="lg" colorScheme="blue" {...props}> | ||||||
|         <TagLeftIcon boxSize="18px" as={icon} /> |         <TagLeftIcon boxSize="18px" as={icon} mt={-0.5} /> | ||||||
|         <TagLabel>{label}</TagLabel> |         <TagLabel>{label}</TagLabel> | ||||||
|       </Tag> |       </Tag> | ||||||
|     </Tooltip> |     </Tooltip> | ||||||
|   | |||||||
| @@ -24,7 +24,6 @@ export const DataGridCellRow = <TValue extends object>({ | |||||||
|         backgroundColor: hoveredRowBg, |         backgroundColor: hoveredRowBg, | ||||||
|       }} |       }} | ||||||
|       onClick={onClick} |       onClick={onClick} | ||||||
|       borderRight="1px solid gray" |  | ||||||
|     > |     > | ||||||
|       {row.getVisibleCells().map((cell) => ( |       {row.getVisibleCells().map((cell) => ( | ||||||
|         <Td |         <Td | ||||||
| @@ -55,6 +54,7 @@ export const DataGridCellRow = <TValue extends object>({ | |||||||
|               : undefined |               : undefined | ||||||
|           } |           } | ||||||
|           border="0.5px solid gray" |           border="0.5px solid gray" | ||||||
|  |           style={cell.column.columnDef.meta?.rowContentOptions?.style} | ||||||
|         > |         > | ||||||
|           {flexRender(cell.column.columnDef.cell, cell.getContext())} |           {flexRender(cell.column.columnDef.cell, cell.getContext())} | ||||||
|         </Td> |         </Td> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export type DataGridHeaderRowProps<TValue extends object> = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => ( | export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => ( | ||||||
|   <Tr p={0} borderRight="1px solid gray"> |   <Tr p={0}> | ||||||
|     {headerGroup.headers.map((header) => ( |     {headerGroup.headers.map((header) => ( | ||||||
|       <Th |       <Th | ||||||
|         color="gray.400" |         color="gray.400" | ||||||
|   | |||||||
| @@ -40,13 +40,16 @@ export type DataGridOptions<TValue extends object> = { | |||||||
|   onRowClick?: (row: TValue) => (() => void) | undefined; |   onRowClick?: (row: TValue) => (() => void) | undefined; | ||||||
|   refetch?: () => void; |   refetch?: () => void; | ||||||
|   showAsCard?: boolean; |   showAsCard?: boolean; | ||||||
|  |   hideTablePreferences?: boolean; | ||||||
|  |   hideTableTitleRow?: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type DataGridProps<TValue extends object> = { | export type DataGridProps<TValue extends object> = { | ||||||
|  |   innerTableKey?: string | number; | ||||||
|   controller: UseDataGridReturn; |   controller: UseDataGridReturn; | ||||||
|   columns: DataGridColumn<TValue>[]; |   columns: DataGridColumn<TValue>[]; | ||||||
|   header: { |   header: { | ||||||
|     title: string; |     title: string | React.ReactNode; | ||||||
|     objectListed: string; |     objectListed: string; | ||||||
|     leftContent?: React.ReactNode; |     leftContent?: React.ReactNode; | ||||||
|     addButton?: React.ReactNode; |     addButton?: React.ReactNode; | ||||||
| @@ -58,6 +61,7 @@ export type DataGridProps<TValue extends object> = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const DataGrid = <TValue extends object>({ | export const DataGrid = <TValue extends object>({ | ||||||
|  |   innerTableKey, | ||||||
|   controller, |   controller, | ||||||
|   columns, |   columns, | ||||||
|   header, |   header, | ||||||
| @@ -149,6 +153,20 @@ export const DataGrid = <TValue extends object>({ | |||||||
|     ...tableOptions, |     ...tableOptions, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   // If this is a manual DataTable, with a page index that is higher than 0 and higher than the max possible page, we send to index 0 | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if ( | ||||||
|  |       options.isManual && | ||||||
|  |       !isLoading && | ||||||
|  |       data && | ||||||
|  |       pagination.pageIndex > 0 && | ||||||
|  |       options.count !== undefined && | ||||||
|  |       Math.ceil(options.count / pagination.pageSize) - 1 < pagination.pageIndex | ||||||
|  |     ) { | ||||||
|  |       controller.onPaginationChange({ pageIndex: 0, pageSize: pagination.pageSize }); | ||||||
|  |     } | ||||||
|  |   }, [options.count, isLoading, pagination, data]); | ||||||
|  |  | ||||||
|   if (isLoading && !options.showAsCard && data.length === 0) { |   if (isLoading && !options.showAsCard && data.length === 0) { | ||||||
|     return ( |     return ( | ||||||
|       <Center> |       <Center> | ||||||
| @@ -160,25 +178,29 @@ export const DataGrid = <TValue extends object>({ | |||||||
|   return options.showAsCard ? ( |   return options.showAsCard ? ( | ||||||
|     <Card> |     <Card> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
|         <Heading size="md" my="auto" mr={2}> |         {typeof header.title === 'string' ? ( | ||||||
|           {header.title} |           <Heading size="md" my="auto" mr={2}> | ||||||
|         </Heading> |             {header.title} | ||||||
|  |           </Heading> | ||||||
|  |         ) : ( | ||||||
|  |           header.title | ||||||
|  |         )} | ||||||
|         {header.leftContent} |         {header.leftContent} | ||||||
|         <Spacer /> |         <Spacer /> | ||||||
|         <HStack spacing={2}> |         <HStack spacing={2}> | ||||||
|           {header.otherButtons} |           {header.otherButtons} | ||||||
|           {header.addButton} |           {header.addButton} | ||||||
|           { |           {options.hideTablePreferences ? null : ( | ||||||
|             // @ts-ignore |             // @ts-ignore | ||||||
|             <TableSettingsModal<TValue> controller={controller} columns={columns} /> |             <TableSettingsModal<TValue> controller={controller} columns={columns} /> | ||||||
|           } |           )} | ||||||
|           {options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null} |           {options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null} | ||||||
|         </HStack> |         </HStack> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody display="flex" flexDirection="column"> |       <CardBody display="flex" flexDirection="column"> | ||||||
|         <LoadingOverlay isLoading={isLoading}> |         <LoadingOverlay isLoading={isLoading}> | ||||||
|           <TableContainer minH={minimumHeight}> |           <TableContainer minH={minimumHeight}> | ||||||
|             <Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px"> |             <Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}> | ||||||
|               <Thead> |               <Thead> | ||||||
|                 {table.getHeaderGroups().map((headerGroup) => ( |                 {table.getHeaderGroups().map((headerGroup) => ( | ||||||
|                   <DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} /> |                   <DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} /> | ||||||
| @@ -206,7 +228,7 @@ export const DataGrid = <TValue extends object>({ | |||||||
|     </Card> |     </Card> | ||||||
|   ) : ( |   ) : ( | ||||||
|     <Box w="100%"> |     <Box w="100%"> | ||||||
|       <Flex mb={2}> |       <Flex mb={2} hidden={options.hideTableTitleRow}> | ||||||
|         <Heading size="md" my="auto" mr={2}> |         <Heading size="md" my="auto" mr={2}> | ||||||
|           {header.title} |           {header.title} | ||||||
|         </Heading> |         </Heading> | ||||||
| @@ -215,16 +237,16 @@ export const DataGrid = <TValue extends object>({ | |||||||
|         <HStack spacing={2}> |         <HStack spacing={2}> | ||||||
|           {header.otherButtons} |           {header.otherButtons} | ||||||
|           {header.addButton} |           {header.addButton} | ||||||
|           { |           {options.hideTablePreferences ? null : ( | ||||||
|             // @ts-ignore |             // @ts-ignore | ||||||
|             <TableSettingsModal<TValue> controller={controller} columns={columns} /> |             <TableSettingsModal<TValue> controller={controller} columns={columns} /> | ||||||
|           } |           )} | ||||||
|           {options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null} |           {options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null} | ||||||
|         </HStack> |         </HStack> | ||||||
|       </Flex> |       </Flex> | ||||||
|       <LoadingOverlay isLoading={isLoading}> |       <LoadingOverlay isLoading={isLoading}> | ||||||
|         <TableContainer minH={minimumHeight}> |         <TableContainer minH={minimumHeight}> | ||||||
|           <Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px"> |           <Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}> | ||||||
|             <Thead> |             <Thead> | ||||||
|               {table.getHeaderGroups().map((headerGroup) => ( |               {table.getHeaderGroups().map((headerGroup) => ( | ||||||
|                 <DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} /> |                 <DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} /> | ||||||
|   | |||||||
| @@ -2,7 +2,8 @@ import * as React from 'react'; | |||||||
| import { ColumnDef, PaginationState, SortingColumnDef, SortingState, VisibilityState } from '@tanstack/react-table'; | import { ColumnDef, PaginationState, SortingColumnDef, SortingState, VisibilityState } from '@tanstack/react-table'; | ||||||
| import { useAuth } from 'contexts/AuthProvider'; | import { useAuth } from 'contexts/AuthProvider'; | ||||||
|  |  | ||||||
| const getDefaultSettings = (settings?: string) => { | const getDefaultSettings = ({ settings, showAllRows }: { settings?: string; showAllRows?: boolean }) => { | ||||||
|  |   if (showAllRows) return { pageSize: 1000, pageIndex: 0 }; | ||||||
|   let limit = 10; |   let limit = 10; | ||||||
|   let index = 0; |   let index = 0; | ||||||
|  |  | ||||||
| @@ -54,9 +55,10 @@ export type UseDataGridProps = { | |||||||
|   tableSettingsId: string; |   tableSettingsId: string; | ||||||
|   defaultOrder: string[]; |   defaultOrder: string[]; | ||||||
|   defaultSortBy?: SortingState; |   defaultSortBy?: SortingState; | ||||||
|  |   showAllRows?: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: UseDataGridProps) => { | export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder, showAllRows }: UseDataGridProps) => { | ||||||
|   const orderSetting = `${tableSettingsId}.order`; |   const orderSetting = `${tableSettingsId}.order`; | ||||||
|   const hiddenColumnSetting = `${tableSettingsId}.hiddenColumns`; |   const hiddenColumnSetting = `${tableSettingsId}.hiddenColumns`; | ||||||
|   const pageSetting = `${tableSettingsId}.page`; |   const pageSetting = `${tableSettingsId}.page`; | ||||||
| @@ -66,8 +68,9 @@ export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: Us | |||||||
|   const [columnOrder, setColumnOrder] = React.useState<string[]>( |   const [columnOrder, setColumnOrder] = React.useState<string[]>( | ||||||
|     getSavedColumnOrder(defaultOrder ?? [], tableSettingsId), |     getSavedColumnOrder(defaultOrder ?? [], tableSettingsId), | ||||||
|   ); |   ); | ||||||
|   const [pageInfo, setPageInfo] = React.useState<PaginationState>(getDefaultSettings(tableSettingsId)); |   const [pageInfo, setPageInfo] = React.useState<PaginationState>( | ||||||
|  |     getDefaultSettings({ settings: tableSettingsId, showAllRows }), | ||||||
|  |   ); | ||||||
|   const setNewColumnOrder = React.useCallback( |   const setNewColumnOrder = React.useCallback( | ||||||
|     (newOrder: string[]) => { |     (newOrder: string[]) => { | ||||||
|       setColumnOrder(newOrder); |       setColumnOrder(newOrder); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react'; | import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react'; | ||||||
| import { Select } from 'chakra-react-select'; | import { CreatableSelect, Select } from 'chakra-react-select'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| import isEqual from 'react-fast-compare'; | import isEqual from 'react-fast-compare'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @@ -25,6 +25,7 @@ const propTypes = { | |||||||
|   isHidden: PropTypes.bool, |   isHidden: PropTypes.bool, | ||||||
|   isPortal: PropTypes.bool.isRequired, |   isPortal: PropTypes.bool.isRequired, | ||||||
|   definitionKey: PropTypes.string, |   definitionKey: PropTypes.string, | ||||||
|  |   isCreatable: PropTypes.bool, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const defaultProps = { | const defaultProps = { | ||||||
| @@ -36,6 +37,7 @@ const defaultProps = { | |||||||
|   isDisabled: false, |   isDisabled: false, | ||||||
|   isHidden: false, |   isHidden: false, | ||||||
|   definitionKey: null, |   definitionKey: null, | ||||||
|  |   isCreatable: false, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const FastMultiSelectInput = ({ | const FastMultiSelectInput = ({ | ||||||
| @@ -50,6 +52,7 @@ const FastMultiSelectInput = ({ | |||||||
|   isRequired, |   isRequired, | ||||||
|   isDisabled, |   isDisabled, | ||||||
|   isHidden, |   isHidden, | ||||||
|  |   isCreatable, | ||||||
|   isPortal, |   isPortal, | ||||||
|   definitionKey, |   definitionKey, | ||||||
| }) => { | }) => { | ||||||
| @@ -61,35 +64,62 @@ const FastMultiSelectInput = ({ | |||||||
|         {label} |         {label} | ||||||
|         <ConfigurationFieldExplanation definitionKey={definitionKey} /> |         <ConfigurationFieldExplanation definitionKey={definitionKey} /> | ||||||
|       </FormLabel> |       </FormLabel> | ||||||
|       <Select |       {isCreatable ? ( | ||||||
|         chakraStyles={{ |         <CreatableSelect | ||||||
|           control: (provided, { isDisabled: isControlDisabled }) => ({ |           chakraStyles={{ | ||||||
|             ...provided, |             control: (provided, { isDisabled: isControlDisabled }) => ({ | ||||||
|             borderRadius: '15px', |               ...provided, | ||||||
|             opacity: isControlDisabled ? '0.8 !important' : '1', |               borderRadius: '15px', | ||||||
|             border: '2px solid', |               opacity: isControlDisabled ? '0.8 !important' : '1', | ||||||
|           }), |               border: '2px solid', | ||||||
|           dropdownIndicator: (provided) => ({ |             }), | ||||||
|             ...provided, |             dropdownIndicator: (provided) => ({ | ||||||
|             backgroundColor: 'unset', |               ...provided, | ||||||
|             border: 'unset', |               backgroundColor: 'unset', | ||||||
|           }), |               border: 'unset', | ||||||
|         }} |             }), | ||||||
|         classNamePrefix={isPortal ? 'chakra-react-select' : ''} |           }} | ||||||
|         menuPortalTarget={isPortal ? document.body : undefined} |           classNamePrefix={isPortal ? 'chakra-react-select' : ''} | ||||||
|         isMulti |           menuPortalTarget={isPortal ? document.body : undefined} | ||||||
|         closeMenuOnSelect={false} |           isMulti | ||||||
|         options={canSelectAll ? [{ value: '*', label: t('common.all') }, ...options] : options} |           closeMenuOnSelect={false} | ||||||
|         value={ |           options={options} | ||||||
|           value?.map((val) => { |           value={value} | ||||||
|             if (val === '*') return { value: val, label: t('common.all') }; |           onChange={onChange} | ||||||
|             return options.find((opt) => opt.value === val); |           onBlur={onBlur} | ||||||
|           }) ?? [] |           isDisabled={isDisabled} | ||||||
|         } |         /> | ||||||
|         onChange={onChange} |       ) : ( | ||||||
|         onBlur={onBlur} |         <Select | ||||||
|         isDisabled={isDisabled} |           chakraStyles={{ | ||||||
|       /> |             control: (provided, { isDisabled: isControlDisabled }) => ({ | ||||||
|  |               ...provided, | ||||||
|  |               borderRadius: '15px', | ||||||
|  |               opacity: isControlDisabled ? '0.8 !important' : '1', | ||||||
|  |               border: '2px solid', | ||||||
|  |             }), | ||||||
|  |             dropdownIndicator: (provided) => ({ | ||||||
|  |               ...provided, | ||||||
|  |               backgroundColor: 'unset', | ||||||
|  |               border: 'unset', | ||||||
|  |             }), | ||||||
|  |           }} | ||||||
|  |           classNamePrefix={isPortal ? 'chakra-react-select' : ''} | ||||||
|  |           menuPortalTarget={isPortal ? document.body : undefined} | ||||||
|  |           isMulti | ||||||
|  |           closeMenuOnSelect={false} | ||||||
|  |           options={canSelectAll ? [{ value: '*', label: t('common.all') }, ...options] : options} | ||||||
|  |           value={ | ||||||
|  |             value?.map((val) => { | ||||||
|  |               if (val === '*') return { value: val, label: t('common.all') }; | ||||||
|  |               return options.find((opt) => opt.value === val); | ||||||
|  |             }) ?? [] | ||||||
|  |           } | ||||||
|  |           onChange={onChange} | ||||||
|  |           onBlur={onBlur} | ||||||
|  |           isDisabled={isDisabled} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|       <FormErrorMessage>{error}</FormErrorMessage> |       <FormErrorMessage>{error}</FormErrorMessage> | ||||||
|     </FormControl> |     </FormControl> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ const propTypes = { | |||||||
|   canSelectAll: PropTypes.bool, |   canSelectAll: PropTypes.bool, | ||||||
|   isPortal: PropTypes.bool, |   isPortal: PropTypes.bool, | ||||||
|   definitionKey: PropTypes.string, |   definitionKey: PropTypes.string, | ||||||
|  |   isCreatable: PropTypes.bool, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const defaultProps = { | const defaultProps = { | ||||||
| @@ -31,6 +32,7 @@ const defaultProps = { | |||||||
|   canSelectAll: false, |   canSelectAll: false, | ||||||
|   isPortal: false, |   isPortal: false, | ||||||
|   definitionKey: null, |   definitionKey: null, | ||||||
|  |   isCreatable: false, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const MultiSelectField = ({ | const MultiSelectField = ({ | ||||||
| @@ -43,25 +45,39 @@ const MultiSelectField = ({ | |||||||
|   emptyIsUndefined, |   emptyIsUndefined, | ||||||
|   canSelectAll, |   canSelectAll, | ||||||
|   hasVirtualAll, |   hasVirtualAll, | ||||||
|  |   isCreatable, | ||||||
|   isPortal, |   isPortal, | ||||||
|   definitionKey, |   definitionKey, | ||||||
| }) => { | }) => { | ||||||
|   const [{ value }, { touched, error }, { setValue, setTouched }] = useField(name); |   const [{ value }, { touched, error }, { setValue, setTouched }] = useField(name); | ||||||
|  |  | ||||||
|   const onChange = useCallback((option) => { |   const onChange = useCallback( | ||||||
|     const allIndex = option.findIndex((opt) => opt.value === '*'); |     (option) => { | ||||||
|     if (option.length === 0 && emptyIsUndefined) { |       if (isCreatable) { | ||||||
|       setValue(undefined); |         if (typeof option === 'string') { | ||||||
|     } else if (allIndex === 0 && option.length > 1) { |           setValue([...value, option]); | ||||||
|       const newValues = option.slice(1); |         } else { | ||||||
|       setValue(newValues.map((val) => val.value)); |           setValue(option); | ||||||
|     } else if (allIndex >= 0) { |         } | ||||||
|       if (!hasVirtualAll) setValue(['*']); |  | ||||||
|       else setValue(options.map(({ value: v }) => v)); |         // setValue([...value, option]); | ||||||
|     } else if (option.length > 0) setValue(option.map((val) => val.value)); |       } else { | ||||||
|     else setValue([]); |         const allIndex = option.findIndex((opt) => opt.value === '*'); | ||||||
|     setTouched(true); |         if (option.length === 0 && emptyIsUndefined) { | ||||||
|   }, []); |           setValue(undefined); | ||||||
|  |         } else if (allIndex === 0 && option.length > 1) { | ||||||
|  |           const newValues = option.slice(1); | ||||||
|  |           setValue(newValues.map((val) => val.value)); | ||||||
|  |         } else if (allIndex >= 0) { | ||||||
|  |           if (!hasVirtualAll) setValue(['*']); | ||||||
|  |           else setValue(options.map(({ value: v }) => v)); | ||||||
|  |         } else if (option.length > 0) setValue(option.map((val) => val.value)); | ||||||
|  |         else setValue([]); | ||||||
|  |         setTouched(true); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [value], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   const onFieldBlur = useCallback(() => { |   const onFieldBlur = useCallback(() => { | ||||||
|     setTouched(true); |     setTouched(true); | ||||||
| @@ -82,6 +98,7 @@ const MultiSelectField = ({ | |||||||
|       isHidden={isHidden} |       isHidden={isHidden} | ||||||
|       isPortal={isPortal} |       isPortal={isPortal} | ||||||
|       definitionKey={definitionKey} |       definitionKey={definitionKey} | ||||||
|  |       isCreatable={isCreatable} | ||||||
|     /> |     /> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -104,18 +104,43 @@ const GlobalSearchBar = () => { | |||||||
|           .then(() => callback([])); |           .then(() => callback([])); | ||||||
|       } |       } | ||||||
|       if (v.match('^[a-fA-F0-9-*]+$')) { |       if (v.match('^[a-fA-F0-9-*]+$')) { | ||||||
|  |         let result: { label: string; value: string; type: 'serial' }[] = []; | ||||||
|  |         let tryAgain = true; | ||||||
|  |  | ||||||
|         await store |         await store | ||||||
|           .searchSerialNumber(v) |           .searchSerialNumber(v) | ||||||
|           .then((res) => { |           .then((res) => { | ||||||
|             callback( |             result = res.map((r) => ({ | ||||||
|               res.map((r) => ({ |               label: r, | ||||||
|  |               value: r, | ||||||
|  |               type: 'serial', | ||||||
|  |             })); | ||||||
|  |             tryAgain = false; | ||||||
|  |           }) | ||||||
|  |           .catch(() => { | ||||||
|  |             result = []; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |         if (tryAgain) { | ||||||
|  |           // Wait 1 second and try again | ||||||
|  |           await new Promise((resolve) => setTimeout(resolve, 1000)); | ||||||
|  |  | ||||||
|  |           await store | ||||||
|  |             .searchSerialNumber(v) | ||||||
|  |             .then((res) => { | ||||||
|  |               result = res.map((r) => ({ | ||||||
|                 label: r, |                 label: r, | ||||||
|                 value: r, |                 value: r, | ||||||
|                 type: 'serial', |                 type: 'serial', | ||||||
|               })), |               })); | ||||||
|             ); |               tryAgain = false; | ||||||
|           }) |             }) | ||||||
|           .catch(() => []); |             .catch(() => { | ||||||
|  |               result = []; | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         callback(result); | ||||||
|       } |       } | ||||||
|       return callback([]); |       return callback([]); | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ export type ConfigureModalProps = { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps) => { | 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 }); | ||||||
| @@ -45,6 +45,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|   const onImportConfiguration = () => { |   const onImportConfiguration = () => { | ||||||
|     setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : ''); |     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); | ||||||
| @@ -58,22 +59,59 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|     try { |     try { | ||||||
|       const config = JSON.parse(newConfig); |       const config = JSON.parse(newConfig); | ||||||
|       configure.mutate(config, { |       configure.mutate(config, { | ||||||
|         onSuccess: () => { |         onSuccess: (data) => { | ||||||
|           toast({ |           if (data.errorCode === 0) { | ||||||
|             id: `configure-success-${serialNumber}`, |             toast({ | ||||||
|             title: t('common.success'), |               id: `configure-success-${serialNumber}`, | ||||||
|             description: t('controller.configure.success'), |               title: t('common.success'), | ||||||
|             status: 'success', |               description: | ||||||
|             duration: 5000, |                 data.status === 'pending' | ||||||
|             isClosable: true, |                   ? 'Command is pending! It will execute once the device connects' | ||||||
|             position: 'top-right', |                   : t('controller.configure.success'), | ||||||
|           }); |               status: 'success', | ||||||
|  |               duration: 5000, | ||||||
|  |               isClosable: true, | ||||||
|  |               position: 'top-right', | ||||||
|  |             }); | ||||||
|  |             modalProps.onClose(); | ||||||
|  |           } else if (data.errorCode === 1) { | ||||||
|  |             toast({ | ||||||
|  |               id: `configure-warning-${serialNumber}`, | ||||||
|  |               title: 'Warning', | ||||||
|  |               description: `${data?.errorText ?? 'Unknown Warning'}`, | ||||||
|  |               status: 'warning', | ||||||
|  |               duration: 5000, | ||||||
|  |               isClosable: true, | ||||||
|  |               position: 'top-right', | ||||||
|  |             }); | ||||||
|  |             modalProps.onClose(); | ||||||
|  |           } else { | ||||||
|  |             toast({ | ||||||
|  |               id: `config-error-${serialNumber}`, | ||||||
|  |               title: t('common.error'), | ||||||
|  |               description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`, | ||||||
|  |               status: 'error', | ||||||
|  |               duration: 5000, | ||||||
|  |               isClosable: true, | ||||||
|  |               position: 'top-right', | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|           modalProps.onClose(); |           modalProps.onClose(); | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     } catch (e) {} |     } catch (e) { | ||||||
|  |       // do nothing | ||||||
|  |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (modalProps.isOpen) { | ||||||
|  |       getDevice.refetch(); | ||||||
|  |     } else { | ||||||
|  |       setNewConfig(''); | ||||||
|  |     } | ||||||
|  |   }, [modalProps.isOpen]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       {...modalProps} |       {...modalProps} | ||||||
| @@ -124,3 +162,5 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps | |||||||
|     </Modal> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const ConfigureModal = React.memo(_ConfigureModal); | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next'; | |||||||
| import { Modal } from '../Modal'; | import { Modal } from '../Modal'; | ||||||
| import { lowercaseFirstLetter } from 'helpers/stringHelper'; | import { lowercaseFirstLetter } from 'helpers/stringHelper'; | ||||||
| import { useTelemetry } from 'hooks/Network/Telemetry'; | import { useTelemetry } from 'hooks/Network/Telemetry'; | ||||||
|  | import { secondsDuration } from 'helpers/dateFormatting'; | ||||||
|  |  | ||||||
| export type TelemetryModalProps = { | export type TelemetryModalProps = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| @@ -146,8 +147,7 @@ const _TelemetryModal = ({ serialNumber, modalProps }: TelemetryModalProps) => { | |||||||
|               {t('controller.telemetry.interval')}: {form.interval} {lowercaseFirstLetter(t('common.seconds'))} |               {t('controller.telemetry.interval')}: {form.interval} {lowercaseFirstLetter(t('common.seconds'))} | ||||||
|             </p> |             </p> | ||||||
|             <p> |             <p> | ||||||
|               {t('controller.telemetry.duration')}: {form.interval}{' '} |               {t('controller.telemetry.duration')}: {secondsDuration(form.lifetime, t)} | ||||||
|               {lowercaseFirstLetter(t('controller.telemetry.minutes'))} |  | ||||||
|             </p> |             </p> | ||||||
|             <p> |             <p> | ||||||
|               {t('controller.telemetry.types')}: {form.types.join(', ')} |               {t('controller.telemetry.types')}: {form.types.join(', ')} | ||||||
|   | |||||||
| @@ -1,18 +1,5 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { | import { Box, Button, Center, Heading, IconButton, Spacer, useColorMode } from '@chakra-ui/react'; | ||||||
|   Box, |  | ||||||
|   Button, |  | ||||||
|   Heading, |  | ||||||
|   IconButton, |  | ||||||
|   Spacer, |  | ||||||
|   Table, |  | ||||||
|   Tbody, |  | ||||||
|   Td, |  | ||||||
|   Th, |  | ||||||
|   Thead, |  | ||||||
|   Tr, |  | ||||||
|   useColorMode, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import { JsonViewer } from '@textea/json-viewer'; | import { JsonViewer } from '@textea/json-viewer'; | ||||||
| import { ArrowLeft } from '@phosphor-icons/react'; | import { ArrowLeft } from '@phosphor-icons/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @@ -20,21 +7,124 @@ import { v4 as uuid } from 'uuid'; | |||||||
| 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'; | ||||||
| import { ScanChannel } from 'models/Device'; | import { DeviceScanResult, ScanChannel } from 'models/Device'; | ||||||
|  | import { DataGrid } from 'components/DataTables/DataGrid'; | ||||||
|  | import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
|   channelInfo: ScanChannel; |   channelInfo: ScanChannel; | ||||||
| } | } | ||||||
| const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => { |  | ||||||
|  | const ueCell = (ies: DeviceScanResult['ies'], setIes: (ies: DeviceScanResult['ies']) => void) => ( | ||||||
|  |   <Button size="sm" colorScheme="blue" onClick={() => setIes(ies)} w="100%"> | ||||||
|  |     {ies.length} | ||||||
|  |   </Button> | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const centerIfUndefinedCell = (v?: string | number, suffix?: string) => | ||||||
|  |   v !== undefined ? `${v}${suffix ? `${suffix}` : ''}` : <Center>-</Center>; | ||||||
|  |  | ||||||
|  | const ResultCard = ({ channelInfo: { channel, devices } }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { colorMode } = useColorMode(); |   const { colorMode } = useColorMode(); | ||||||
|   const [ies, setIes] = React.useState<{ content: unknown; name: string; type: number }[] | undefined>(); |   const [ies, setIes] = React.useState<{ content: unknown; name: string; type: number }[] | undefined>(); | ||||||
|  |   const tableController = useDataGrid({ | ||||||
|  |     tableSettingsId: 'wifiscan.devices.table', | ||||||
|  |     defaultOrder: ['ssid', 'signal', 'actions'], | ||||||
|  |     defaultSortBy: [ | ||||||
|  |       { | ||||||
|  |         desc: false, | ||||||
|  |         id: 'ssid', | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const columns: DataGridColumn<DeviceScanResult>[] = React.useMemo( | ||||||
|  |     (): DataGridColumn<DeviceScanResult>[] => [ | ||||||
|  |       { | ||||||
|  |         id: 'ssid', | ||||||
|  |         header: 'SSID', | ||||||
|  |         footer: '', | ||||||
|  |         accessorKey: 'ssid', | ||||||
|  |         meta: { | ||||||
|  |           anchored: true, | ||||||
|  |           alwaysShow: true, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'signal', | ||||||
|  |         header: 'Signal', | ||||||
|  |         footer: '', | ||||||
|  |         accessorKey: 'signal', | ||||||
|  |         cell: (v) => `${v.cell.row.original.signal} db`, | ||||||
|  |         meta: { | ||||||
|  |           anchored: true, | ||||||
|  |           customWidth: '80px', | ||||||
|  |           alwaysShow: true, | ||||||
|  |           rowContentOptions: { | ||||||
|  |             style: { | ||||||
|  |               textAlign: 'right', | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'station', | ||||||
|  |         header: 'UEs', | ||||||
|  |         accessorKey: 'sta_count', | ||||||
|  |         cell: (v) => centerIfUndefinedCell(v.cell.row.original.sta_count), | ||||||
|  |         meta: { | ||||||
|  |           anchored: true, | ||||||
|  |           customWidth: '40px', | ||||||
|  |           alwaysShow: true, | ||||||
|  |           rowContentOptions: { | ||||||
|  |             style: { | ||||||
|  |               textAlign: 'right', | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'utilization', | ||||||
|  |         header: 'Ch. Util.', | ||||||
|  |         accessorKey: 'ch_util', | ||||||
|  |         cell: (v) => centerIfUndefinedCell(v.cell.row.original.ch_util, '%'), | ||||||
|  |         meta: { | ||||||
|  |           anchored: true, | ||||||
|  |           customWidth: '60px', | ||||||
|  |           alwaysShow: true, | ||||||
|  |           headerOptions: { | ||||||
|  |             tooltip: 'Channel Utilization (%)', | ||||||
|  |           }, | ||||||
|  |           rowContentOptions: { | ||||||
|  |             style: { | ||||||
|  |               textAlign: 'right', | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'ies', | ||||||
|  |         header: 'Ies', | ||||||
|  |         footer: '', | ||||||
|  |         accessorKey: 'actions', | ||||||
|  |         cell: (v) => ueCell(v.cell.row.original.ies ?? [], setIes), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '50px', | ||||||
|  |           isCentered: true, | ||||||
|  |           alwaysShow: true, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     [t], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card variant="widget"> |     <Card> | ||||||
|       <CardHeader display="flex"> |       <CardHeader display="flex"> | ||||||
|         <Heading size="md" my="auto"> |         <Heading size="md" my="auto"> | ||||||
|           {t('commands.channel')} #{channel} ({devices.length} {t('devices.title')}) |           {t('commands.channel')} #{channel} ({devices.length}{' '} | ||||||
|  |           {devices.length === 1 ? t('devices.one') : t('devices.title')}) | ||||||
|         </Heading> |         </Heading> | ||||||
|         <Spacer /> |         <Spacer /> | ||||||
|         {ies && ( |         {ies && ( | ||||||
| @@ -49,52 +139,43 @@ const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => { | |||||||
|         )} |         )} | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody> |       <CardBody> | ||||||
|         <Box h="400px" w="100%" overflowY="auto" overflowX="auto" px={0}> |         {ies ? ( | ||||||
|           {ies ? ( |           <Box w="800px"> | ||||||
|             <Box w="800px"> |             {ies.map(({ content, name, type }) => ( | ||||||
|               {ies.map(({ content, name, type }) => ( |               <Box key={uuid()} my={2}> | ||||||
|                 <Box key={uuid()} my={2}> |                 <Heading size="sm" mb={2} textDecor="underline"> | ||||||
|                   <Heading size="sm" mb={2} textDecor="underline"> |                   {name} ({type}) | ||||||
|                     {name} ({type}) |                 </Heading> | ||||||
|                   </Heading> |                 <JsonViewer | ||||||
|                   <JsonViewer |                   rootName={false} | ||||||
|                     rootName={false} |                   displayDataTypes={false} | ||||||
|                     displayDataTypes={false} |                   enableClipboard | ||||||
|                     enableClipboard |                   theme={colorMode === 'light' ? undefined : 'dark'} | ||||||
|                     theme={colorMode === 'light' ? undefined : 'dark'} |                   value={content as object} | ||||||
|                     value={content as object} |                   style={{ background: 'unset', display: 'unset' }} | ||||||
|                     style={{ background: 'unset', display: 'unset' }} |                 /> | ||||||
|                   /> |               </Box> | ||||||
|                 </Box> |             ))} | ||||||
|               ))} |           </Box> | ||||||
|             </Box> |         ) : ( | ||||||
|           ) : ( |           <DataGrid<DeviceScanResult> | ||||||
|             <Table variant="simple" px={0}> |             controller={tableController} | ||||||
|               <Thead> |             header={{ | ||||||
|                 <Tr> |               title: '', | ||||||
|                   <Th>SSID</Th> |               objectListed: t('devices.title'), | ||||||
|                   <Th width="110px" isNumeric> |             }} | ||||||
|                     {t('commands.signal')} |             columns={columns} | ||||||
|                   </Th> |             data={devices} | ||||||
|                   <Th w="10px">IEs</Th> |             options={{ | ||||||
|                 </Tr> |               count: devices.length, | ||||||
|               </Thead> |               onRowClick: (device) => () => setIes(device.ies ?? []), | ||||||
|               <Tbody> |               hideTablePreferences: true, | ||||||
|                 {devices.map((dev) => ( |               isHidingControls: true, | ||||||
|                   <Tr key={uuid()}> |               minimumHeight: '0px', | ||||||
|                     <Td>{dev.ssid}</Td> |               hideTableTitleRow: true, | ||||||
|                     <Td width="110px">{dev.signal} db</Td> |             }} | ||||||
|                     <Td w="10px"> |           /> | ||||||
|                       <Button size="sm" colorScheme="blue" onClick={() => setIes(dev.ies ?? [])}> |         )} | ||||||
|                         {dev.ies?.length ?? 0} |  | ||||||
|                       </Button> |  | ||||||
|                     </Td> |  | ||||||
|                   </Tr> |  | ||||||
|                 ))} |  | ||||||
|               </Tbody> |  | ||||||
|             </Table> |  | ||||||
|           )} |  | ||||||
|         </Box> |  | ||||||
|       </CardBody> |       </CardBody> | ||||||
|     </Card> |     </Card> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import React, { useEffect, useMemo } from 'react'; | import React, { useEffect, useMemo } from 'react'; | ||||||
| import { Alert, Heading, SimpleGrid } from '@chakra-ui/react'; | import { Alert, Heading, VStack } 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 ResultCard from './ResultCard'; | import ResultCard from './ResultCard'; | ||||||
| @@ -11,7 +11,7 @@ interface Props { | |||||||
|   setCsvData: (data: DeviceScanResult[]) => void; |   setCsvData: (data: DeviceScanResult[]) => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => { | const WifiScanResultDisplay = ({ results, setCsvData }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |  | ||||||
|   const scanResults = useMemo(() => { |   const scanResults = useMemo(() => { | ||||||
| @@ -54,18 +54,18 @@ const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => { | |||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       {results.errorCode === 1 && ( |       {results.errorCode === 1 && ( | ||||||
|         <Heading size="sm"> |         <Heading size="md"> | ||||||
|           <Alert colorScheme="red">{t('commands.wifiscan_error_1')}</Alert> |           <Alert colorScheme="red">{t('commands.wifiscan_error_1')}</Alert> | ||||||
|         </Heading> |         </Heading> | ||||||
|       )} |       )} | ||||||
|       <Heading size="sm"> |       <Heading size="md" mb={2}> | ||||||
|         {t('commands.execution_time')}: {Math.floor(results.executionTime / 1000)}s |         {t('commands.execution_time')}: {Math.floor(results.executionTime / 1000)}s | ||||||
|       </Heading> |       </Heading> | ||||||
|       <SimpleGrid minChildWidth="360px" spacing={2}> |       <VStack spacing={4} align="stretch"> | ||||||
|         {scanResults?.scanList.map((channel) => ( |         {scanResults?.scanList.map((channel) => ( | ||||||
|           <ResultCard key={uuid()} channelInfo={channel} /> |           <ResultCard key={uuid()} channelInfo={channel} /> | ||||||
|         ))} |         ))} | ||||||
|       </SimpleGrid> |       </VStack> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								src/custom.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -8,3 +8,4 @@ declare module '*.png' { | |||||||
|   const value: string; |   const value: string; | ||||||
|   export = value; |   export = value; | ||||||
| } | } | ||||||
|  | /// <reference types="vite-plugin-svgr/client" /> | ||||||
|   | |||||||
| @@ -174,12 +174,37 @@ export const useGetEventQueue = () => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const configureDevice = (serialNumber: string) => async (configuration: Record<string, unknown>) => | const configureDevice = (serialNumber: string) => async (configuration: Record<string, unknown>) => | ||||||
|   axiosGw.post<unknown>(`device/${serialNumber}/configure`, { |   axiosGw | ||||||
|     when: 0, |     .post<unknown>(`device/${serialNumber}/configure`, { | ||||||
|     UUID: 1, |       when: 0, | ||||||
|     serialNumber, |       UUID: 1, | ||||||
|     configuration, |       serialNumber, | ||||||
|   }); |       configuration, | ||||||
|  |     }) | ||||||
|  |     .then( | ||||||
|  |       (res) => | ||||||
|  |         res.data as Partial<{ | ||||||
|  |           UUID: string; | ||||||
|  |           attachFile: number; | ||||||
|  |           command: string; | ||||||
|  |           completed: number; | ||||||
|  |           custom: number; | ||||||
|  |           deferred: boolean; | ||||||
|  |           details: Record<string, unknown>; | ||||||
|  |           errorCode: number; | ||||||
|  |           errorText: string; | ||||||
|  |           executed: number; | ||||||
|  |           executionTime: number; | ||||||
|  |           lastTry: number; | ||||||
|  |           results: Record<string, unknown>; | ||||||
|  |           serialNumber: string; | ||||||
|  |           status: string; | ||||||
|  |           submitted: number; | ||||||
|  |           submittedBy: string; | ||||||
|  |           waitingForFile: number; | ||||||
|  |           when: number; | ||||||
|  |         }>, | ||||||
|  |     ); | ||||||
|  |  | ||||||
| export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) => { | export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) => { | ||||||
|   const queryClient = useQueryClient(); |   const queryClient = useQueryClient(); | ||||||
| @@ -187,6 +212,8 @@ export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) = | |||||||
|   return useMutation(configureDevice(serialNumber), { |   return useMutation(configureDevice(serialNumber), { | ||||||
|     onSuccess: () => { |     onSuccess: () => { | ||||||
|       queryClient.invalidateQueries(['commands', serialNumber]); |       queryClient.invalidateQueries(['commands', serialNumber]); | ||||||
|  |       queryClient.invalidateQueries(['device', serialNumber]); | ||||||
|  |       queryClient.invalidateQueries(['devices']); | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| @@ -248,27 +275,14 @@ const startScript = (data: { serialNumber: string; timeout?: number; [k: string] | |||||||
|     }) |     }) | ||||||
|     .then((response: { data: DeviceCommandHistory }) => response.data); |     .then((response: { data: DeviceCommandHistory }) => response.data); | ||||||
| export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => { | export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => { | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const toast = useToast(); |  | ||||||
|   const queryClient = useQueryClient(); |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|   return useMutation(startScript, { |   return useMutation(startScript, { | ||||||
|     onSuccess: () => { |     onSuccess: () => { | ||||||
|       queryClient.invalidateQueries(['commands', serialNumber]); |       queryClient.invalidateQueries(['commands', serialNumber]); | ||||||
|     }, |     }, | ||||||
|     onError: (e) => { |     onError: () => { | ||||||
|       queryClient.invalidateQueries(['commands', serialNumber]); |       queryClient.invalidateQueries(['commands', serialNumber]); | ||||||
|       if (axios.isAxiosError(e)) { |  | ||||||
|         toast({ |  | ||||||
|           id: 'script-error', |  | ||||||
|           title: t('common.error'), |  | ||||||
|           description: e?.response?.data?.ErrorDescription, |  | ||||||
|           status: 'error', |  | ||||||
|           duration: 5000, |  | ||||||
|           isClosable: true, |  | ||||||
|           position: 'top-right', |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -3,10 +3,12 @@ import { axiosGw } from 'constants/axiosInstances'; | |||||||
| import { useEndpointStatus } from 'hooks/useEndpointStatus'; | import { useEndpointStatus } from 'hooks/useEndpointStatus'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
| import { DeviceConfiguration } from 'models/Device'; | import { DeviceConfiguration } from 'models/Device'; | ||||||
|  | import { DevicePlatform } from './Devices'; | ||||||
|  |  | ||||||
| export type DefaultConfigurationResponse = { | export type DefaultConfigurationResponse = { | ||||||
|   configuration: DeviceConfiguration; |   configuration: DeviceConfiguration; | ||||||
|   created: number; |   created: number; | ||||||
|  |   platform: DevicePlatform; | ||||||
|   description: string; |   description: string; | ||||||
|   lastModified: number; |   lastModified: number; | ||||||
|   modelIds: string[]; |   modelIds: string[]; | ||||||
|   | |||||||
| @@ -9,15 +9,21 @@ import { AxiosError } from 'models/Axios'; | |||||||
| import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device'; | import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device'; | ||||||
| import { Note } from 'models/Note'; | import { Note } from 'models/Note'; | ||||||
| import { PageInfo } from 'models/Table'; | import { PageInfo } from 'models/Table'; | ||||||
|  | import { DeviceCommandHistory } from './Commands'; | ||||||
|  |  | ||||||
| const getDeviceCount = () => | export const DEVICE_PLATFORMS = ['all', 'ap', 'switch'] as const; | ||||||
|   axiosGw.get('devices?countOnly=true').then((response) => response.data) as Promise<{ count: number }>; | export type DevicePlatform = (typeof DEVICE_PLATFORMS)[number]; | ||||||
|  |  | ||||||
| export const useGetDeviceCount = ({ enabled }: { enabled: boolean }) => { | const getDeviceCount = (platform: DevicePlatform) => | ||||||
|  |   axiosGw.get(`devices?countOnly=true&platform=${platform}`).then((response) => response.data) as Promise<{ | ||||||
|  |     count: number; | ||||||
|  |   }>; | ||||||
|  |  | ||||||
|  | export const useGetDeviceCount = ({ enabled, platform = 'all' }: { enabled: boolean; platform?: DevicePlatform }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const toast = useToast(); |   const toast = useToast(); | ||||||
|  |  | ||||||
|   return useQuery(['devices', 'count'], getDeviceCount, { |   return useQuery(['devices', 'count', { platform }], () => getDeviceCount(platform), { | ||||||
|     enabled, |     enabled, | ||||||
|     onError: (e: AxiosError) => { |     onError: (e: AxiosError) => { | ||||||
|       if (!toast.isActive('inventory-fetching-error')) |       if (!toast.isActive('inventory-fetching-error')) | ||||||
| @@ -42,13 +48,14 @@ export type DeviceWithStatus = { | |||||||
|   associations_2G: number; |   associations_2G: number; | ||||||
|   associations_5G: number; |   associations_5G: number; | ||||||
|   associations_6G: number; |   associations_6G: number; | ||||||
|  |   blackListed?: boolean; | ||||||
|   compatible: string; |   compatible: string; | ||||||
|   connected: boolean; |   connected: boolean; | ||||||
|   connectReason?: string; |   connectReason?: string; | ||||||
|   certificateExpiryDate?: number; |   certificateExpiryDate?: number; | ||||||
|   createdTimestamp: number; |   createdTimestamp: number; | ||||||
|   devicePassword: string; |   devicePassword: string; | ||||||
|   deviceType: 'AP' | 'SWITCH' | 'IOT' | 'MESH'; |   deviceType: 'ap' | 'switch'; | ||||||
|   entity: string; |   entity: string; | ||||||
|   firmware: string; |   firmware: string; | ||||||
|   fwUpdatePolicy: string; |   fwUpdatePolicy: string; | ||||||
| @@ -95,25 +102,27 @@ export const getSingleDeviceWithStatus = (serialNumber: string) => | |||||||
|     }) |     }) | ||||||
|     .catch(() => undefined); |     .catch(() => undefined); | ||||||
|  |  | ||||||
| const getDevices = (limit: number, offset: number) => | const getDevices = (limit: number, offset: number, platform: DevicePlatform) => | ||||||
|   axiosGw |   axiosGw | ||||||
|     .get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`) |     .get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}&platform=${platform}`) | ||||||
|     .then((response) => response.data) as Promise<{ devicesWithStatus: DeviceWithStatus[] }>; |     .then((response) => response.data) as Promise<{ devicesWithStatus: DeviceWithStatus[] }>; | ||||||
|  |  | ||||||
| export const useGetDevices = ({ | export const useGetDevices = ({ | ||||||
|   pageInfo, |   pageInfo, | ||||||
|   enabled, |   enabled, | ||||||
|   onError, |   onError, | ||||||
|  |   platform = 'all', | ||||||
| }: { | }: { | ||||||
|   pageInfo?: PageInfo; |   pageInfo?: PageInfo; | ||||||
|   enabled: boolean; |   enabled: boolean; | ||||||
|   onError?: (e: AxiosError) => void; |   onError?: (e: AxiosError) => void; | ||||||
|  |   platform?: DevicePlatform; | ||||||
| }) => { | }) => { | ||||||
|   const offset = pageInfo?.limit !== undefined ? pageInfo.limit * pageInfo.index : 0; |   const offset = pageInfo?.limit !== undefined ? pageInfo.limit * pageInfo.index : 0; | ||||||
|  |  | ||||||
|   return useQuery( |   return useQuery( | ||||||
|     ['devices', 'all', { limit: pageInfo?.limit, offset }], |     ['devices', 'all', { limit: pageInfo?.limit, offset, platform }], | ||||||
|     () => getDevices(pageInfo?.limit || 0, offset), |     () => getDevices(pageInfo?.limit || 0, offset, platform), | ||||||
|     { |     { | ||||||
|       keepPreviousData: true, |       keepPreviousData: true, | ||||||
|       enabled: enabled && pageInfo !== undefined, |       enabled: enabled && pageInfo !== undefined, | ||||||
| @@ -123,22 +132,28 @@ export const useGetDevices = ({ | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const getAllDevices = async () => { | const getAllDevices = async (platform: DevicePlatform) => { | ||||||
|   let offset = 0; |   let offset = 0; | ||||||
|   let devices: DeviceWithStatus[] = []; |   let devices: DeviceWithStatus[] = []; | ||||||
|   let devicesResponse: { devicesWithStatus: DeviceWithStatus[] }; |   let devicesResponse: { devicesWithStatus: DeviceWithStatus[] }; | ||||||
|   do { |   do { | ||||||
|     // eslint-disable-next-line no-await-in-loop |     // eslint-disable-next-line no-await-in-loop | ||||||
|     devicesResponse = await getDevices(500, offset); |     devicesResponse = await getDevices(500, offset, platform); | ||||||
|     devices = devices.concat(devicesResponse.devicesWithStatus); |     devices = devices.concat(devicesResponse.devicesWithStatus); | ||||||
|     offset += 500; |     offset += 500; | ||||||
|   } while (devicesResponse.devicesWithStatus.length === 500); |   } while (devicesResponse.devicesWithStatus.length === 500); | ||||||
|   return devices; |   return devices; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const useGetAllDevicesWithStatus = ({ onError }: { onError?: (e: AxiosError) => void }) => { | export const useGetAllDevicesWithStatus = ({ | ||||||
|  |   onError, | ||||||
|  |   platform = 'all', | ||||||
|  | }: { | ||||||
|  |   onError?: (e: AxiosError) => void; | ||||||
|  |   platform?: DevicePlatform; | ||||||
|  | }) => { | ||||||
|   const { isReady } = useEndpointStatus('owgw'); |   const { isReady } = useEndpointStatus('owgw'); | ||||||
|   return useQuery(['devices', 'all', 'full'], getAllDevices, { |   return useQuery(['devices', 'all', 'full', { platform }], () => getAllDevices(platform), { | ||||||
|     enabled: isReady && false, |     enabled: isReady && false, | ||||||
|     onError, |     onError, | ||||||
|   }); |   }); | ||||||
| @@ -431,3 +446,45 @@ export const useUpdateDevice = ({ serialNumber }: { serialNumber: string }) => { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const deleteDeviceBatch = async (pattern: string) => { | ||||||
|  |   if (pattern.length < 6) throw new Error('Pattern must be at least 6 characters long'); | ||||||
|  |  | ||||||
|  |   axiosGw.delete(`devices?macPattern=${pattern}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const useDeleteDeviceBatch = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(deleteDeviceBatch, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['devices']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type PowerCyclePort = { | ||||||
|  |   /** Ex.: Ethernet0 */ | ||||||
|  |   name: string; | ||||||
|  |   /** Cycle length in MS. Default is 10 000 */ | ||||||
|  |   cycle?: number; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type PowerCycleRequest = { | ||||||
|  |   serial: string; | ||||||
|  |   when: number; | ||||||
|  |   ports: PowerCyclePort[]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const usePowerCycle = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |   return useMutation( | ||||||
|  |     (request: PowerCycleRequest) => | ||||||
|  |       axiosGw.post(`device/${request.serial}/powercycle`, request).then((res) => res.data as DeviceCommandHistory), | ||||||
|  |     { | ||||||
|  |       onSettled: () => { | ||||||
|  |         queryClient.invalidateQueries(['commands']); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -70,25 +70,66 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe | |||||||
|  |  | ||||||
|   return useMutation( |   return useMutation( | ||||||
|     ({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) => |     ({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) => | ||||||
|       axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, { |       axiosGw | ||||||
|         serialNumber, |         .post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, { | ||||||
|         when: 0, |           serialNumber, | ||||||
|         keepRedirector, |           when: 0, | ||||||
|         uri, |           keepRedirector, | ||||||
|         signature, |           uri, | ||||||
|       }), |           signature, | ||||||
|  |         }) | ||||||
|  |         .then( | ||||||
|  |           (response) => | ||||||
|  |             response as { | ||||||
|  |               data: { | ||||||
|  |                 errorCode: number; | ||||||
|  |                 errorText: string; | ||||||
|  |                 status: string; | ||||||
|  |                 results?: { | ||||||
|  |                   status?: { | ||||||
|  |                     error?: number; | ||||||
|  |                     resultCode?: number; | ||||||
|  |                     text?: string; | ||||||
|  |                   }; | ||||||
|  |                 }; | ||||||
|  |               }; | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|     { |     { | ||||||
|       onSuccess: () => { |       onSuccess: ({ data }) => { | ||||||
|         toast({ |         if (data.errorCode === 0) { | ||||||
|           id: `device-upgrade-success-${uuid()}`, |           toast({ | ||||||
|           title: t('common.success'), |             id: `device-upgrade-success-${uuid()}`, | ||||||
|           description: t('commands.firmware_upgrade_success'), |             title: t('common.success'), | ||||||
|           status: 'success', |             description: t('commands.firmware_upgrade_success'), | ||||||
|           duration: 5000, |             status: 'success', | ||||||
|           isClosable: true, |             duration: 5000, | ||||||
|           position: 'top-right', |             isClosable: true, | ||||||
|         }); |             position: 'top-right', | ||||||
|         onClose(); |           }); | ||||||
|  |           onClose(); | ||||||
|  |         } else if (data.errorCode === 1) { | ||||||
|  |           toast({ | ||||||
|  |             id: `device-upgrade-warning-${uuid()}`, | ||||||
|  |             title: 'Warning', | ||||||
|  |             description: `${data?.errorText ?? 'Unknown Warning'}`, | ||||||
|  |             status: 'warning', | ||||||
|  |             duration: 5000, | ||||||
|  |             isClosable: true, | ||||||
|  |             position: 'top-right', | ||||||
|  |           }); | ||||||
|  |           onClose(); | ||||||
|  |         } else { | ||||||
|  |           toast({ | ||||||
|  |             id: `device-upgrade-error-${uuid()}`, | ||||||
|  |             title: t('common.error'), | ||||||
|  |             description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`, | ||||||
|  |             status: 'error', | ||||||
|  |             duration: 5000, | ||||||
|  |             isClosable: true, | ||||||
|  |             position: 'top-right', | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|       }, |       }, | ||||||
|       onError: (e: AxiosError) => { |       onError: (e: AxiosError) => { | ||||||
|         toast({ |         toast({ | ||||||
|   | |||||||
							
								
								
									
										173
									
								
								src/hooks/Network/Simulations.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,173 @@ | |||||||
|  | import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||||
|  | import { axiosGw, axiosOwls } from 'constants/axiosInstances'; | ||||||
|  | import { AtLeast } from 'models/General'; | ||||||
|  |  | ||||||
|  | export type Simulation = { | ||||||
|  |   clientInterval: number; | ||||||
|  |   concurrentDeviceS: number; | ||||||
|  |   deviceType: string; | ||||||
|  |   devices: number; | ||||||
|  |   gateway: string; | ||||||
|  |   healthCheckInterval: number; | ||||||
|  |   id: string; | ||||||
|  |   keepAlive: number; | ||||||
|  |   key: string; | ||||||
|  |   macPrefix: string; | ||||||
|  |   minAssociations: number; | ||||||
|  |   maxAssociations: number; | ||||||
|  |   minClients: number; | ||||||
|  |   maxClients: number; | ||||||
|  |   name: string; | ||||||
|  |   reconnectionInterval: number; | ||||||
|  |   simulationLength: number; | ||||||
|  |   stateInterval: number; | ||||||
|  |   threads: number; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getSimulations = () => async () => | ||||||
|  |   axiosOwls.get(`simulation/*`).then((response) => response.data as { list: Simulation[] }); | ||||||
|  |  | ||||||
|  | export const useGetSimulations = () => | ||||||
|  |   useQuery(['simulations'], getSimulations(), { | ||||||
|  |     keepPreviousData: true, | ||||||
|  |     staleTime: 30000, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | const getSimulation = (id?: string) => async () => | ||||||
|  |   axiosOwls.get(`simulation/${id}`).then((response) => response.data as { list: Simulation[] }); | ||||||
|  | export const useGetSimulation = ({ id }: { id?: string }) => | ||||||
|  |   useQuery(['simulation', id], getSimulation(id), { | ||||||
|  |     keepPreviousData: true, | ||||||
|  |     enabled: id !== undefined, | ||||||
|  |     staleTime: 30000, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | const createSimulation = async (newSimulation: Partial<Simulation>) => axiosOwls.post(`simulation/0`, newSimulation); | ||||||
|  | export const useCreateSimulation = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(createSimulation, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['simulations']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateSimulation = async (newSimulation: AtLeast<Simulation, 'id'>) => | ||||||
|  |   axiosOwls.put(`simulation/${newSimulation.id}`, newSimulation).then((response) => response.data as Simulation); | ||||||
|  | export const useUpdateSimulation = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(updateSimulation, { | ||||||
|  |     onSuccess: (newSimulation) => { | ||||||
|  |       queryClient.setQueryData(['simulation'], newSimulation); | ||||||
|  |       queryClient.invalidateQueries(['simulations']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteSimulation = async ({ id }: { id: string }) => axiosOwls.delete(`simulation/${id}`); | ||||||
|  | export const useDeleteSimulation = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(deleteSimulation, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['simulations']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const startSimulation = async ({ id }: { id: string }) => axiosOwls.post(`operation/${id}?operation=start`); | ||||||
|  | export const useStartSimulation = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(startSimulation, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['simulations', 'status']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | const stopSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) => | ||||||
|  |   axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=stop`); | ||||||
|  | export const useStopSimulation = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(stopSimulation, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['simulations', 'status']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | const cancelSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) => | ||||||
|  |   axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=cancel`); | ||||||
|  | export const useCancelSimulation = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(cancelSimulation, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['simulations', 'status']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type SimulationStatus = { | ||||||
|  |   endTime: number; | ||||||
|  |   errorDevices: number; | ||||||
|  |   id: string; | ||||||
|  |   liveDevices: number; | ||||||
|  |   msgsRx: number; | ||||||
|  |   msgsTx: number; | ||||||
|  |   owner: string; | ||||||
|  |   rx: number; | ||||||
|  |   simulationId: string; | ||||||
|  |   startTime: number; | ||||||
|  |   state: 'running' | 'completed' | 'cancelled' | 'none'; | ||||||
|  |   timeToFullDevices: number; | ||||||
|  |   tx: number; | ||||||
|  | }; | ||||||
|  | const getSimulationsStatus = async () => | ||||||
|  |   axiosOwls.get(`status/*`).then((response) => response.data as SimulationStatus[]); | ||||||
|  | export const useGetSimulationsStatus = () => | ||||||
|  |   useQuery(['simulations', 'status'], getSimulationsStatus, { | ||||||
|  |     keepPreviousData: true, | ||||||
|  |     staleTime: Infinity, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | const getSimulationStatus = async (context: QueryFunctionContext<[string, string, string]>) => | ||||||
|  |   axiosOwls.get(`status/${context.queryKey[2]}`).then((response) => response.data as SimulationStatus); | ||||||
|  | export const useGetSimulationStatus = ({ id }: { id: string }) => | ||||||
|  |   useQuery(['simulations', 'status', id], getSimulationStatus, { | ||||||
|  |     keepPreviousData: true, | ||||||
|  |     staleTime: Infinity, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | const getSimulationHistory = async (context: QueryFunctionContext<[string, string, string]>) => | ||||||
|  |   axiosOwls.get(`results/${context.queryKey[2]}`).then((response) => response.data.list as SimulationStatus[]); | ||||||
|  | export const useGetSimulationHistory = ({ id }: { id: string }) => | ||||||
|  |   useQuery(['simulations', 'history', id], getSimulationHistory, { | ||||||
|  |     keepPreviousData: true, | ||||||
|  |     enabled: !!id, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | const deleteSimulationResult = async ({ id }: { id: string }) => axiosOwls.delete(`results/${id}`); | ||||||
|  | export const useDeleteSimulationResult = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(deleteSimulationResult, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['simulations', 'history']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const deleteSimulatedDevices = async () => axiosGw.delete('devices?simulatedDevices=true'); | ||||||
|  |  | ||||||
|  | export const useDeleteSimulatedDevices = () => { | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |  | ||||||
|  |   return useMutation(deleteSimulatedDevices, { | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries(['devices']); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @@ -2,7 +2,24 @@ 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'; | ||||||
|  |  | ||||||
| type DeviceInterfaceStatistics = { | export type DeviceLinkState = { | ||||||
|  |   carrier?: number; | ||||||
|  |   counters?: { | ||||||
|  |     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; | ||||||
|  |   }; | ||||||
|  |   duplex?: string; | ||||||
|  |   speed?: number; | ||||||
|  | }; | ||||||
|  | export type DeviceInterfaceStatistics = { | ||||||
|   clients: { |   clients: { | ||||||
|     ipv4_addresses?: string[]; |     ipv4_addresses?: string[]; | ||||||
|     ipv6_addresses?: string[]; |     ipv6_addresses?: string[]; | ||||||
| @@ -42,6 +59,7 @@ type DeviceInterfaceStatistics = { | |||||||
|       dynamic_vlan?: number; |       dynamic_vlan?: number; | ||||||
|       inactive: number; |       inactive: number; | ||||||
|       ipaddr_v4: string; |       ipaddr_v4: string; | ||||||
|  |       fingerprint?: object; | ||||||
|       rssi: number; |       rssi: number; | ||||||
|       rx_bytes: number; |       rx_bytes: number; | ||||||
|       rx_duration: number; |       rx_duration: number; | ||||||
| @@ -112,11 +130,21 @@ export type DeviceStatistics = { | |||||||
|     channel: number; |     channel: number; | ||||||
|     band?: string[]; |     band?: string[]; | ||||||
|     channel_width: string; |     channel_width: string; | ||||||
|     noise: number; |     noise?: number; | ||||||
|     phy: string; |     phy: string; | ||||||
|     receive_ms: number; |     receive_ms: number; | ||||||
|     transmit_ms: number; |     transmit_ms: number; | ||||||
|  |     temperature?: number; | ||||||
|     tx_power: number; |     tx_power: number; | ||||||
|  |     frequency?: number[]; | ||||||
|  |     survey?: { | ||||||
|  |       busy: number; | ||||||
|  |       frequency: number; | ||||||
|  |       noise: number; | ||||||
|  |       time: number; | ||||||
|  |       time_rx: number; | ||||||
|  |       time_tx: number; | ||||||
|  |     }[]; | ||||||
|   }[]; |   }[]; | ||||||
|   dynamic_vlans?: { |   dynamic_vlans?: { | ||||||
|     vid: number; |     vid: number; | ||||||
| @@ -138,18 +166,10 @@ export type DeviceStatistics = { | |||||||
|   }; |   }; | ||||||
|   'link-state'?: { |   'link-state'?: { | ||||||
|     downstream: { |     downstream: { | ||||||
|       eth1?: { |       [key: string]: DeviceLinkState; | ||||||
|         carrier?: number; |  | ||||||
|         duplex?: string; |  | ||||||
|         speed?: number; |  | ||||||
|       }; |  | ||||||
|     }; |     }; | ||||||
|     upstream: { |     upstream: { | ||||||
|       eth0?: { |       [key: string]: DeviceLinkState; | ||||||
|         carrier?: number; |  | ||||||
|         duplex?: string; |  | ||||||
|         speed?: number; |  | ||||||
|       }; |  | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|   'lldp-peers'?: { |   'lldp-peers'?: { | ||||||
| @@ -190,6 +210,7 @@ export const useGetDeviceLastStats = ({ | |||||||
|   useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), { |   useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), { | ||||||
|     enabled: serialNumber !== undefined && serialNumber !== '', |     enabled: serialNumber !== undefined && serialNumber !== '', | ||||||
|     staleTime: 1000 * 60, |     staleTime: 1000 * 60, | ||||||
|  |     refetchInterval: 1000 * 60, | ||||||
|     onError, |     onError, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										80
									
								
								src/hooks/useNotification.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,80 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { useToast } from '@chakra-ui/react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { v4 as uuid } from 'uuid'; | ||||||
|  | import { isApiError } from 'models/Axios'; | ||||||
|  |  | ||||||
|  | export type SuccessNotificationProps = { | ||||||
|  |   description: string; | ||||||
|  |   id?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type ApiErrorNotificationProps = { | ||||||
|  |   e: unknown; | ||||||
|  |   fallbackMessage?: string; | ||||||
|  |   id?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const useNotification = () => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const toast = useToast(); | ||||||
|  |  | ||||||
|  |   const successToast = ({ description, id }: SuccessNotificationProps) => { | ||||||
|  |     toast({ | ||||||
|  |       id: id ?? uuid(), | ||||||
|  |       title: t('common.success'), | ||||||
|  |       description, | ||||||
|  |       status: 'success', | ||||||
|  |       duration: 3000, | ||||||
|  |       isClosable: true, | ||||||
|  |       position: 'top-right', | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const apiErrorToast = ({ e, id, fallbackMessage }: ApiErrorNotificationProps) => { | ||||||
|  |     if (isApiError(e)) { | ||||||
|  |       toast({ | ||||||
|  |         id: id ?? uuid(), | ||||||
|  |         title: t('common.error'), | ||||||
|  |         description: e.response?.data.ErrorDescription, | ||||||
|  |         status: 'error', | ||||||
|  |         duration: 5000, | ||||||
|  |         isClosable: true, | ||||||
|  |         position: 'top-right', | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       toast({ | ||||||
|  |         id: id ?? uuid(), | ||||||
|  |         title: t('common.error'), | ||||||
|  |         description: fallbackMessage, | ||||||
|  |         status: 'error', | ||||||
|  |         duration: 5000, | ||||||
|  |         isClosable: true, | ||||||
|  |         position: 'top-right', | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const errorToast = ({ description, id }: SuccessNotificationProps) => { | ||||||
|  |     toast({ | ||||||
|  |       id: id ?? uuid(), | ||||||
|  |       title: t('common.error'), | ||||||
|  |       description, | ||||||
|  |       status: 'error', | ||||||
|  |       duration: 5000, | ||||||
|  |       isClosable: true, | ||||||
|  |       position: 'top-right', | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return React.useMemo( | ||||||
|  |     () => ({ | ||||||
|  |       successToast, | ||||||
|  |       errorToast, | ||||||
|  |       apiErrorToast, | ||||||
|  |     }), | ||||||
|  |     [t], | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type UseNotificationReturn = ReturnType<typeof useNotification>; | ||||||
| @@ -1,16 +1,6 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { | import { Flex, Heading, Icon, Text, Tooltip, VStack } from '@chakra-ui/react'; | ||||||
|   Box, | import { ArrowSquareDown, ArrowSquareUp } from '@phosphor-icons/react'; | ||||||
|   CircularProgress, |  | ||||||
|   CircularProgressLabel, |  | ||||||
|   Flex, |  | ||||||
|   Heading, |  | ||||||
|   Icon, |  | ||||||
|   Text, |  | ||||||
|   Tooltip, |  | ||||||
|   VStack, |  | ||||||
| } from '@chakra-ui/react'; |  | ||||||
| import { ArrowSquareDown, ArrowSquareUp, Clock } from '@phosphor-icons/react'; |  | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { Card } from 'components/Containers/Card'; | import { Card } from 'components/Containers/Card'; | ||||||
| import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting'; | import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting'; | ||||||
| @@ -20,74 +10,26 @@ import { useGetDevicesStats } from 'hooks/Network/Devices'; | |||||||
| const SidebarDevices = () => { | const SidebarDevices = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const getStats = useGetDevicesStats({}); |   const getStats = useGetDevicesStats({}); | ||||||
|   const [lastTime, setLastTime] = React.useState<Date | undefined>(); |  | ||||||
|   const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>(); |  | ||||||
|  |  | ||||||
|   const time = React.useMemo(() => { |  | ||||||
|     if (lastTime === undefined || lastUpdate === undefined) return null; |  | ||||||
|  |  | ||||||
|     const seconds = lastTime.getTime() - lastUpdate.getTime(); |  | ||||||
|  |  | ||||||
|     return Math.max(0, Math.floor(seconds / 1000)); |  | ||||||
|   }, [lastTime, lastUpdate]); |  | ||||||
|  |  | ||||||
|   const circleColor = () => { |  | ||||||
|     if (time === null) return 'gray.300'; |  | ||||||
|     if (time < 10) return 'green.300'; |  | ||||||
|     if (time < 30) return 'yellow.300'; |  | ||||||
|     return 'red.300'; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     setLastUpdate(new Date()); |  | ||||||
|   }, [getStats.data]); |  | ||||||
|  |  | ||||||
|   React.useEffect(() => { |  | ||||||
|     const interval = setInterval(() => { |  | ||||||
|       setLastTime(new Date()); |  | ||||||
|     }, 1000); |  | ||||||
|     return () => { |  | ||||||
|       clearInterval(interval); |  | ||||||
|     }; |  | ||||||
|   }, []); |  | ||||||
|  |  | ||||||
|   if (!getStats.data) return null; |   if (!getStats.data) return null; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card p={4}> |     <Card p={4}> | ||||||
|       <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}> |       <VStack mb={-1}> | ||||||
|         <Flex flexDir="column" textAlign="center"> |         <Flex flexDir="column" textAlign="center"> | ||||||
|           <Heading size="md">{getStats.data.connectedDevices}</Heading> |  | ||||||
|           <Heading size="xs" display="flex" justifyContent="center"> |           <Heading size="xs" display="flex" justifyContent="center"> | ||||||
|             <Text> |             <Text> | ||||||
|               {t('common.connected')} {t('devices.title')}{' '} |               {t('common.connected')} {t('devices.title')}{' '} | ||||||
|             </Text>{' '} |             </Text>{' '} | ||||||
|           </Heading> |           </Heading> | ||||||
|  |           <Heading size="md">{getStats.data.connectedDevices}</Heading> | ||||||
|  |           <Heading size="xs">{t('controller.devices.average_uptime')}</Heading> | ||||||
|           <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> |           <Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}> | ||||||
|             <Heading size="md" textAlign="center" mt={1}> |             <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> |  | ||||||
|           <Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}> |           <Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}> | ||||||
|             <Tooltip hasArrow label="Rx"> |             <Tooltip hasArrow label="Rx"> | ||||||
|               <Flex alignItems="center" mr={1}> |               <Flex alignItems="center" mr={1}> | ||||||
|   | |||||||
| @@ -123,6 +123,7 @@ export const Navbar = ({ | |||||||
|         top="15px" |         top="15px" | ||||||
|         border={scrolled ? '0.5px solid' : undefined} |         border={scrolled ? '0.5px solid' : undefined} | ||||||
|         w={isCompact ? '100%' : 'calc(100% - 254px)'} |         w={isCompact ? '100%' : 'calc(100% - 254px)'} | ||||||
|  |         zIndex={10} | ||||||
|       > |       > | ||||||
|         <Flex |         <Flex | ||||||
|           w="100%" |           w="100%" | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
| import { AxiosError as Err } from 'axios'; | import { AxiosError as Err, isAxiosError } from 'axios'; | ||||||
|  |  | ||||||
| export type AxiosError = Err<{ ErrorDescription: string; ErrorCode: number }>; | export type AxiosError = Err<{ ErrorDescription: string; ErrorCode: number }>; | ||||||
|  |  | ||||||
|  | export const isApiError = (e: unknown): e is AxiosError => | ||||||
|  |   isAxiosError(e) && (e as AxiosError).response?.data?.ErrorDescription !== undefined; | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ import { Note } from './Note'; | |||||||
|  |  | ||||||
| export interface GatewayDevice { | export interface GatewayDevice { | ||||||
|   UUID: number; |   UUID: number; | ||||||
|  |   blackListed?: boolean; | ||||||
|  |   certificateExpiryDate: number; | ||||||
|   compatible: string; |   compatible: string; | ||||||
|   configuration: unknown; |   configuration: unknown; | ||||||
|   createdTimestamp: number; |   createdTimestamp: number; | ||||||
| @@ -16,6 +18,7 @@ export interface GatewayDevice { | |||||||
|   lastConfigurationChange: number; |   lastConfigurationChange: number; | ||||||
|   lastConfigurationDownload: number; |   lastConfigurationDownload: number; | ||||||
|   lastFWUpdate: number; |   lastFWUpdate: number; | ||||||
|  |   lastRecordedContact: number; | ||||||
|   locale: string; |   locale: string; | ||||||
|   location: string; |   location: string; | ||||||
|   macAddress: string; |   macAddress: string; | ||||||
| @@ -112,12 +115,16 @@ interface BssidResult { | |||||||
|   bssid: string; |   bssid: string; | ||||||
|   capability: number; |   capability: number; | ||||||
|   channel: number; |   channel: number; | ||||||
|  |   /** Channel Utilization percentage (ex.: 28 -> 28% channel utilization) */ | ||||||
|  |   ch_util?: number; | ||||||
|   frequency: number; |   frequency: number; | ||||||
|   ht_oper: string; |   ht_oper: string; | ||||||
|   ies: { content: unknown; name: string; type: number }[]; |   ies: { content: unknown; name: string; type: number }[]; | ||||||
|   last_seen: number; |   last_seen: number; | ||||||
|   ssid: string; |   ssid: string; | ||||||
|   signal: number; |   signal: number; | ||||||
|  |   /** Station count */ | ||||||
|  |   sta_count?: number; | ||||||
|   tsf: number; |   tsf: number; | ||||||
|   meshid?: string; |   meshid?: string; | ||||||
|   vht_oper: string; |   vht_oper: string; | ||||||
| @@ -142,20 +149,8 @@ export interface WifiScanResult { | |||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface DeviceScanResult { | export type DeviceScanResult = BssidResult; | ||||||
|   bssid: string; |  | ||||||
|   capability: number; |  | ||||||
|   channel: number; |  | ||||||
|   frequency: number; |  | ||||||
|   ht_oper: string; |  | ||||||
|   ies: { content: unknown; name: string; type: number }[]; |  | ||||||
|   last_seen: number; |  | ||||||
|   ssid: string; |  | ||||||
|   signal: number | string; |  | ||||||
|   tsf: number; |  | ||||||
|   meshid?: string; |  | ||||||
|   vht_oper: string; |  | ||||||
| } |  | ||||||
| export interface ScanChannel { | export interface ScanChannel { | ||||||
|   channel: number; |   channel: number; | ||||||
|   devices: DeviceScanResult[]; |   devices: DeviceScanResult[]; | ||||||
|   | |||||||
							
								
								
									
										93
									
								
								src/pages/AdvancedSystemPage/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,93 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { | ||||||
|  |   Box, | ||||||
|  |   Button, | ||||||
|  |   Center, | ||||||
|  |   Heading, | ||||||
|  |   Popover, | ||||||
|  |   PopoverArrow, | ||||||
|  |   PopoverBody, | ||||||
|  |   PopoverCloseButton, | ||||||
|  |   PopoverContent, | ||||||
|  |   PopoverHeader, | ||||||
|  |   PopoverTrigger, | ||||||
|  |   Text, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  | import { Trash } from '@phosphor-icons/react'; | ||||||
|  | import { Card } from 'components/Containers/Card'; | ||||||
|  | import { CardHeader } from 'components/Containers/Card/CardHeader'; | ||||||
|  | import { CardBody } from 'components/Containers/Card/CardBody'; | ||||||
|  | import { DeleteButton } from 'components/Buttons/DeleteButton'; | ||||||
|  | import { useNotification } from 'hooks/useNotification'; | ||||||
|  | import { useDeleteSimulatedDevices } from 'hooks/Network/Simulations'; | ||||||
|  |  | ||||||
|  | const AdvancedSystemPage = () => { | ||||||
|  |   const { successToast, apiErrorToast } = useNotification(); | ||||||
|  |   const deleteSimulatedDevices = useDeleteSimulatedDevices(); | ||||||
|  |  | ||||||
|  |   const handleDeleteSimulatedDevices = async () => | ||||||
|  |     deleteSimulatedDevices.mutateAsync(undefined, { | ||||||
|  |       onSuccess: () => { | ||||||
|  |         successToast({ | ||||||
|  |           id: 'delete-simulated-devices', | ||||||
|  |           description: 'Simulated devices deleted!', | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |       onError: (e) => { | ||||||
|  |         apiErrorToast({ | ||||||
|  |           id: 'delete-simulated-devices', | ||||||
|  |           e, | ||||||
|  |           fallbackMessage: 'Error deleting simulated devices', | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Card> | ||||||
|  |       <CardHeader> | ||||||
|  |         <Heading size="md">Operations</Heading> | ||||||
|  |       </CardHeader> | ||||||
|  |       <CardBody> | ||||||
|  |         <Box> | ||||||
|  |           <Heading size="sm">Delete Simulated Devices</Heading> | ||||||
|  |           <Text fontStyle="italic">Delete all simulated devices from the database. This action cannot be undone.</Text> | ||||||
|  |           <Popover> | ||||||
|  |             {({ onClose }) => ( | ||||||
|  |               <> | ||||||
|  |                 <PopoverTrigger> | ||||||
|  |                   <Button colorScheme="red" rightIcon={<Trash size={20} />}> | ||||||
|  |                     Delete | ||||||
|  |                   </Button> | ||||||
|  |                 </PopoverTrigger> | ||||||
|  |                 <PopoverContent> | ||||||
|  |                   <PopoverArrow /> | ||||||
|  |                   <PopoverCloseButton /> | ||||||
|  |                   <PopoverHeader>Confirm</PopoverHeader> | ||||||
|  |                   <PopoverBody> | ||||||
|  |                     <Text>Are you sure you want to delete all simulated devices?</Text> | ||||||
|  |                     <Center mt={4}> | ||||||
|  |                       <Button onClick={onClose} mr={1}> | ||||||
|  |                         Cancel | ||||||
|  |                       </Button> | ||||||
|  |                       <DeleteButton | ||||||
|  |                         ml={1} | ||||||
|  |                         isLoading={deleteSimulatedDevices.isLoading} | ||||||
|  |                         onClick={async () => { | ||||||
|  |                           await handleDeleteSimulatedDevices(); | ||||||
|  |                           onClose(); | ||||||
|  |                         }} | ||||||
|  |                         isCompact={false} | ||||||
|  |                       /> | ||||||
|  |                     </Center> | ||||||
|  |                   </PopoverBody> | ||||||
|  |                 </PopoverContent> | ||||||
|  |               </> | ||||||
|  |             )} | ||||||
|  |           </Popover> | ||||||
|  |         </Box> | ||||||
|  |       </CardBody> | ||||||
|  |     </Card> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default AdvancedSystemPage; | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, SimpleGrid, useBoolean, useDisclosure, useToast } from '@chakra-ui/react'; | import { Box, Flex, useBoolean, useDisclosure, useToast } from '@chakra-ui/react'; | ||||||
| import { Formik, FormikProps } from 'formik'; | import { Formik, FormikProps } from 'formik'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| @@ -16,6 +16,7 @@ import { useGetDeviceTypes } from 'hooks/Network/Firmware'; | |||||||
| import { useFormModal } from 'hooks/useFormModal'; | import { useFormModal } from 'hooks/useFormModal'; | ||||||
| import { useFormRef } from 'hooks/useFormRef'; | import { useFormRef } from 'hooks/useFormRef'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
|  | import { SelectField } from 'components/Form/Fields/SelectField'; | ||||||
|  |  | ||||||
| const CreateDefaultConfigurationModal = () => { | const CreateDefaultConfigurationModal = () => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
| @@ -68,42 +69,63 @@ const CreateDefaultConfigurationModal = () => { | |||||||
|             key={formKey} |             key={formKey} | ||||||
|             validationSchema={DefaultConfigurationSchema(t)} |             validationSchema={DefaultConfigurationSchema(t)} | ||||||
|             onSubmit={(data, { setSubmitting, resetForm }) => { |             onSubmit={(data, { setSubmitting, resetForm }) => { | ||||||
|               createConfig.mutateAsync(data, { |               createConfig.mutateAsync( | ||||||
|                 onSuccess: () => { |                 { ...data, modelIds: data.modelIds.map((v) => v.value) }, | ||||||
|                   toast({ |                 { | ||||||
|                     id: `config-create-success`, |                   onSuccess: () => { | ||||||
|                     title: t('common.success'), |                     toast({ | ||||||
|                     description: t('controller.configurations.create_success'), |                       id: `config-create-success`, | ||||||
|                     status: 'success', |                       title: t('common.success'), | ||||||
|                     duration: 5000, |                       description: t('controller.configurations.create_success'), | ||||||
|                     isClosable: true, |                       status: 'success', | ||||||
|                     position: 'top-right', |                       duration: 5000, | ||||||
|                   }); |                       isClosable: true, | ||||||
|                   setSubmitting(false); |                       position: 'top-right', | ||||||
|                   resetForm(); |                     }); | ||||||
|                   modalProps.onClose(); |                     setSubmitting(false); | ||||||
|  |                     resetForm(); | ||||||
|  |                     modalProps.onClose(); | ||||||
|  |                   }, | ||||||
|  |                   onError: (error) => { | ||||||
|  |                     const e = error as AxiosError; | ||||||
|  |                     toast({ | ||||||
|  |                       id: `config-create-error`, | ||||||
|  |                       title: t('common.error'), | ||||||
|  |                       description: e?.response?.data?.ErrorDescription, | ||||||
|  |                       status: 'error', | ||||||
|  |                       duration: 5000, | ||||||
|  |                       isClosable: true, | ||||||
|  |                       position: 'top-right', | ||||||
|  |                     }); | ||||||
|  |                     setSubmitting(false); | ||||||
|  |                   }, | ||||||
|                 }, |                 }, | ||||||
|                 onError: (error) => { |               ); | ||||||
|                   const e = error as AxiosError; |  | ||||||
|                   toast({ |  | ||||||
|                     id: `config-create-error`, |  | ||||||
|                     title: t('common.error'), |  | ||||||
|                     description: e?.response?.data?.ErrorDescription, |  | ||||||
|                     status: 'error', |  | ||||||
|                     duration: 5000, |  | ||||||
|                     isClosable: true, |  | ||||||
|                     position: 'top-right', |  | ||||||
|                   }); |  | ||||||
|                   setSubmitting(false); |  | ||||||
|                 }, |  | ||||||
|               }); |  | ||||||
|             }} |             }} | ||||||
|           > |           > | ||||||
|             <Box> |             <Box> | ||||||
|               <SimpleGrid spacing={4} minChildWidth="200px"> |               <Flex mb={4}> | ||||||
|                 <StringField name="name" label={t('common.name')} isRequired isDisabled={isDisabled} /> |                 <StringField | ||||||
|                 <StringField name="description" label={t('common.description')} isDisabled={isDisabled} /> |                   name="name" | ||||||
|               </SimpleGrid> |                   label={t('common.name')} | ||||||
|  |                   isRequired | ||||||
|  |                   isDisabled={isDisabled} | ||||||
|  |                   maxW="340px" | ||||||
|  |                   mr={4} | ||||||
|  |                 /> | ||||||
|  |                 <SelectField | ||||||
|  |                   name="platform" | ||||||
|  |                   label="Platform" | ||||||
|  |                   options={[ | ||||||
|  |                     { label: 'AP', value: 'ap' }, | ||||||
|  |                     { label: 'Switch', value: 'switch' }, | ||||||
|  |                   ]} | ||||||
|  |                   isRequired | ||||||
|  |                   isDisabled={isDisabled} | ||||||
|  |                   w="max-content" | ||||||
|  |                 /> | ||||||
|  |               </Flex> | ||||||
|  |               <StringField name="description" label={t('common.description')} isDisabled={isDisabled} mb={4} /> | ||||||
|               <MultiSelectField |               <MultiSelectField | ||||||
|                 name="modelIds" |                 name="modelIds" | ||||||
|                 label={t('controller.dashboard.device_types')} |                 label={t('controller.dashboard.device_types')} | ||||||
| @@ -114,9 +136,10 @@ const CreateDefaultConfigurationModal = () => { | |||||||
|                     value: devType, |                     value: devType, | ||||||
|                   })) ?? [] |                   })) ?? [] | ||||||
|                 } |                 } | ||||||
|  |                 isCreatable | ||||||
|                 isRequired |                 isRequired | ||||||
|               /> |               /> | ||||||
|               <StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} /> |               <StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} mt={4} /> | ||||||
|             </Box> |             </Box> | ||||||
|           </Formik> |           </Formik> | ||||||
|         </Box> |         </Box> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, SimpleGrid, useBoolean, UseDisclosureReturn, useToast } from '@chakra-ui/react'; | import { Box, Flex, useBoolean, UseDisclosureReturn, useToast } from '@chakra-ui/react'; | ||||||
| import { Formik, FormikProps } from 'formik'; | import { Formik, FormikProps } from 'formik'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| @@ -15,6 +15,7 @@ import { useGetDeviceTypes } from 'hooks/Network/Firmware'; | |||||||
| import { useFormModal } from 'hooks/useFormModal'; | import { useFormModal } from 'hooks/useFormModal'; | ||||||
| import { useFormRef } from 'hooks/useFormRef'; | import { useFormRef } from 'hooks/useFormRef'; | ||||||
| import { AxiosError } from 'models/Axios'; | import { AxiosError } from 'models/Axios'; | ||||||
|  | import { SelectField } from 'components/Form/Fields/SelectField'; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   modalProps: UseDisclosureReturn; |   modalProps: UseDisclosureReturn; | ||||||
| @@ -69,47 +70,69 @@ const EditDefaultConfiguration = ({ modalProps, config }: Props) => { | |||||||
|               innerRef={formRef as React.Ref<FormikProps<DefaultConfigurationResponse>>} |               innerRef={formRef as React.Ref<FormikProps<DefaultConfigurationResponse>>} | ||||||
|               initialValues={{ |               initialValues={{ | ||||||
|                 ...config, |                 ...config, | ||||||
|  |                 modelIds: config.modelIds.map((v) => ({ label: v, value: v })), | ||||||
|                 configuration: JSON.stringify(config.configuration, null, 2), |                 configuration: JSON.stringify(config.configuration, null, 2), | ||||||
|               }} |               }} | ||||||
|               key={formKey} |               key={formKey} | ||||||
|               validationSchema={DefaultConfigurationSchema(t)} |               validationSchema={DefaultConfigurationSchema(t)} | ||||||
|               onSubmit={(data, { setSubmitting, resetForm }) => { |               onSubmit={(data, { setSubmitting, resetForm }) => { | ||||||
|                 updateConfig.mutateAsync(data, { |                 updateConfig.mutateAsync( | ||||||
|                   onSuccess: () => { |                   { ...data, modelIds: data.modelIds.map((v) => v.value) }, | ||||||
|                     toast({ |                   { | ||||||
|                       id: `config-edit-success`, |                     onSuccess: () => { | ||||||
|                       title: t('common.success'), |                       toast({ | ||||||
|                       description: t('controller.configurations.update_success'), |                         id: `config-edit-success`, | ||||||
|                       status: 'success', |                         title: t('common.success'), | ||||||
|                       duration: 5000, |                         description: t('controller.configurations.update_success'), | ||||||
|                       isClosable: true, |                         status: 'success', | ||||||
|                       position: 'top-right', |                         duration: 5000, | ||||||
|                     }); |                         isClosable: true, | ||||||
|                     setSubmitting(false); |                         position: 'top-right', | ||||||
|                     resetForm(); |                       }); | ||||||
|                     modalProps.onClose(); |                       setSubmitting(false); | ||||||
|  |                       resetForm(); | ||||||
|  |                       modalProps.onClose(); | ||||||
|  |                     }, | ||||||
|  |                     onError: (error) => { | ||||||
|  |                       const e = error as AxiosError; | ||||||
|  |                       toast({ | ||||||
|  |                         id: `config-edit-error`, | ||||||
|  |                         title: t('common.error'), | ||||||
|  |                         description: e?.response?.data?.ErrorDescription, | ||||||
|  |                         status: 'error', | ||||||
|  |                         duration: 5000, | ||||||
|  |                         isClosable: true, | ||||||
|  |                         position: 'top-right', | ||||||
|  |                       }); | ||||||
|  |                       setSubmitting(false); | ||||||
|  |                     }, | ||||||
|                   }, |                   }, | ||||||
|                   onError: (error) => { |                 ); | ||||||
|                     const e = error as AxiosError; |  | ||||||
|                     toast({ |  | ||||||
|                       id: `config-edit-error`, |  | ||||||
|                       title: t('common.error'), |  | ||||||
|                       description: e?.response?.data?.ErrorDescription, |  | ||||||
|                       status: 'error', |  | ||||||
|                       duration: 5000, |  | ||||||
|                       isClosable: true, |  | ||||||
|                       position: 'top-right', |  | ||||||
|                     }); |  | ||||||
|                     setSubmitting(false); |  | ||||||
|                   }, |  | ||||||
|                 }); |  | ||||||
|               }} |               }} | ||||||
|             > |             > | ||||||
|               <Box> |               <Box> | ||||||
|                 <SimpleGrid spacing={4} minChildWidth="200px"> |                 <Flex mb={4}> | ||||||
|                   <StringField name="name" label={t('common.name')} isRequired isDisabled={isDisabled} /> |                   <StringField | ||||||
|                   <StringField name="description" label={t('common.description')} isDisabled={isDisabled} /> |                     name="name" | ||||||
|                 </SimpleGrid> |                     label={t('common.name')} | ||||||
|  |                     isRequired | ||||||
|  |                     isDisabled={isDisabled} | ||||||
|  |                     maxW="340px" | ||||||
|  |                     mr={4} | ||||||
|  |                   /> | ||||||
|  |                   <SelectField | ||||||
|  |                     name="platform" | ||||||
|  |                     label="Platform" | ||||||
|  |                     options={[ | ||||||
|  |                       { label: 'AP', value: 'ap' }, | ||||||
|  |                       { label: 'Switch', value: 'switch' }, | ||||||
|  |                     ]} | ||||||
|  |                     isRequired | ||||||
|  |                     isDisabled | ||||||
|  |                     w="max-content" | ||||||
|  |                   /> | ||||||
|  |                 </Flex> | ||||||
|  |                 <StringField name="description" label={t('common.description')} isDisabled={isDisabled} mb={4} /> | ||||||
|                 <MultiSelectField |                 <MultiSelectField | ||||||
|                   name="modelIds" |                   name="modelIds" | ||||||
|                   label={t('controller.dashboard.device_types')} |                   label={t('controller.dashboard.device_types')} | ||||||
| @@ -120,9 +143,16 @@ const EditDefaultConfiguration = ({ modalProps, config }: Props) => { | |||||||
|                       value: devType, |                       value: devType, | ||||||
|                     })) ?? [] |                     })) ?? [] | ||||||
|                   } |                   } | ||||||
|  |                   isCreatable | ||||||
|                   isRequired |                   isRequired | ||||||
|                 /> |                 /> | ||||||
|                 <StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} /> |                 <StringField | ||||||
|  |                   name="configuration" | ||||||
|  |                   label={t('configurations.one')} | ||||||
|  |                   isArea | ||||||
|  |                   isDisabled={isDisabled} | ||||||
|  |                   mt={4} | ||||||
|  |                 /> | ||||||
|               </Box> |               </Box> | ||||||
|             </Formik> |             </Formik> | ||||||
|           )} |           )} | ||||||
|   | |||||||
| @@ -58,6 +58,14 @@ const DefaultConfigurationsList = () => { | |||||||
|         Cell: ({ cell }) => dateCell(cell.row.original.lastModified), |         Cell: ({ cell }) => dateCell(cell.row.original.lastModified), | ||||||
|         customWidth: '50px', |         customWidth: '50px', | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         id: 'platform', | ||||||
|  |         Header: 'Platform', | ||||||
|  |         Footer: '', | ||||||
|  |         accessor: 'platform', | ||||||
|  |         Cell: ({ cell }) => cell.row.original.platform.toUpperCase(), | ||||||
|  |         customWidth: '50px', | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         id: 'modelIds', |         id: 'modelIds', | ||||||
|         Header: t('controller.dashboard.device_types'), |         Header: t('controller.dashboard.device_types'), | ||||||
|   | |||||||
| @@ -6,7 +6,8 @@ export const DefaultConfigurationSchema = (t: (str: string) => string) => | |||||||
|     .shape({ |     .shape({ | ||||||
|       name: Yup.string().required(t('form.required')), |       name: Yup.string().required(t('form.required')), | ||||||
|       description: Yup.string(), |       description: Yup.string(), | ||||||
|       modelIds: Yup.array().of(Yup.string()).required(t('form.required')).min(1, t('form.required')), |       modelIds: Yup.array().of(Yup.object()).required(t('form.required')).min(1, t('form.required')), | ||||||
|  |       platform: Yup.string().oneOf(['ap', 'switch']).required(t('form.required')), | ||||||
|       configuration: Yup.string() |       configuration: Yup.string() | ||||||
|         .required(t('form.required')) |         .required(t('form.required')) | ||||||
|         .test('configuration', t('form.invalid_json'), (v) => testJson(v ?? '')), |         .test('configuration', t('form.invalid_json'), (v) => testJson(v ?? '')), | ||||||
| @@ -15,5 +16,6 @@ export const DefaultConfigurationSchema = (t: (str: string) => string) => | |||||||
|       name: '', |       name: '', | ||||||
|       description: '', |       description: '', | ||||||
|       modelIds: [], |       modelIds: [], | ||||||
|  |       platform: 'ap', | ||||||
|       configuration: '', |       configuration: '', | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -11,7 +11,6 @@ import { compactDate } from 'helpers/dateFormatting'; | |||||||
| import { useGetDevice } from 'hooks/Network/Devices'; | import { useGetDevice } from 'hooks/Network/Devices'; | ||||||
| import { useGetProvUi } from 'hooks/Network/Endpoints'; | import { useGetProvUi } from 'hooks/Network/Endpoints'; | ||||||
| import { useGetTag } from 'hooks/Network/Inventory'; | import { useGetTag } from 'hooks/Network/Inventory'; | ||||||
| import { DeviceConfiguration } from 'models/Device'; |  | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| @@ -60,7 +59,7 @@ const DeviceDetails = ({ serialNumber }: Props) => { | |||||||
|         <Heading size="md">{t('common.details')}</Heading> |         <Heading size="md">{t('common.details')}</Heading> | ||||||
|         <Spacer /> |         <Spacer /> | ||||||
|         <ViewCapabilitiesModal serialNumber={serialNumber} /> |         <ViewCapabilitiesModal serialNumber={serialNumber} /> | ||||||
|         <ViewConfigurationModal configuration={getDevice.data?.configuration as DeviceConfiguration} /> |         <ViewConfigurationModal serialNumber={serialNumber} /> | ||||||
|       </CardHeader> |       </CardHeader> | ||||||
|       <CardBody display="block"> |       <CardBody display="block"> | ||||||
|         <Grid templateColumns="repeat(2, 1fr)" gap={0} w="100%"> |         <Grid templateColumns="repeat(2, 1fr)" gap={0} w="100%"> | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ import { CardHeader } from 'components/Containers/Card/CardHeader'; | |||||||
| import FormattedDate from 'components/InformationDisplays/FormattedDate'; | import FormattedDate from 'components/InformationDisplays/FormattedDate'; | ||||||
| import COUNTRY_LIST from 'constants/countryList'; | import COUNTRY_LIST from 'constants/countryList'; | ||||||
| import { compactDate, compactSecondsToDetailed } from 'helpers/dateFormatting'; | import { compactDate, compactSecondsToDetailed } from 'helpers/dateFormatting'; | ||||||
| import { bytesString, getRevision } from 'helpers/stringHelper'; | import { bytesString, getRevision, uppercaseFirstLetter } from 'helpers/stringHelper'; | ||||||
| import { useGetDevice, useGetDeviceStatus } from 'hooks/Network/Devices'; | import { useGetDevice, useGetDeviceStatus } from 'hooks/Network/Devices'; | ||||||
| import { useGetDeviceLastStats } from 'hooks/Network/Statistics'; | import { useGetDeviceLastStats } from 'hooks/Network/Statistics'; | ||||||
|  |  | ||||||
| @@ -65,11 +65,7 @@ const DeviceSummary = ({ serialNumber }: Props) => { | |||||||
|   const getDeviceCompatible = () => { |   const getDeviceCompatible = () => { | ||||||
|     if (!getDevice.data?.compatible) return undefined; |     if (!getDevice.data?.compatible) return undefined; | ||||||
|  |  | ||||||
|     if (!getDevice.data?.compatible.includes('-')) return getDevice.data?.compatible; |     if (getDevice.data.compatible.includes(' ')) return getDevice.data.compatible.replaceAll(' ', '_'); | ||||||
|  |  | ||||||
|     const split = getDevice.data?.compatible.split('-'); |  | ||||||
|  |  | ||||||
|     if (split[split.length - 1]?.length === 2) return split[0]?.trim(); |  | ||||||
|  |  | ||||||
|     return getDevice.data?.compatible; |     return getDevice.data?.compatible; | ||||||
|   }; |   }; | ||||||
| @@ -129,9 +125,7 @@ const DeviceSummary = ({ serialNumber }: Props) => { | |||||||
|               <Heading size="sm">{t('controller.stats.load')}:</Heading> |               <Heading size="sm">{t('controller.stats.load')}:</Heading> | ||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1}> |             <GridItem colSpan={1}> | ||||||
|               {getStats.data?.unit?.load |               {getStats.data?.unit?.load ? getStats.data?.unit.load.map((l) => `${l.toFixed(2)}`).join(' | ') : ''} | ||||||
|                 ? getStats.data?.unit.load.map((l) => `${(l * 100).toFixed(2)}%`).join(' | ') |  | ||||||
|                 : ''} |  | ||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1} alignContent="center" alignItems="center"> |             <GridItem colSpan={1} alignContent="center" alignItems="center"> | ||||||
|               <Heading size="sm">{t('controller.devices.localtime')}:</Heading> |               <Heading size="sm">{t('controller.devices.localtime')}:</Heading> | ||||||
| @@ -157,7 +151,11 @@ const DeviceSummary = ({ serialNumber }: Props) => { | |||||||
|               <Heading size="sm">{t('analytics.last_contact')}:</Heading> |               <Heading size="sm">{t('analytics.last_contact')}:</Heading> | ||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1}> |             <GridItem colSpan={1}> | ||||||
|               {getStatus?.data?.lastContact ? <FormattedDate date={getStatus.data.lastContact} /> : ''} |               {getStatus?.data?.lastContact && getStatus?.data.lastContact !== 0 ? ( | ||||||
|  |                 <FormattedDate date={getStatus.data.lastContact} /> | ||||||
|  |               ) : ( | ||||||
|  |                 <FormattedDate date={getDevice.data?.lastRecordedContact} /> | ||||||
|  |               )} | ||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1} alignContent="center" alignItems="center"> |             <GridItem colSpan={1} alignContent="center" alignItems="center"> | ||||||
|               <Heading size="sm">{t('analytics.memory')}:</Heading> |               <Heading size="sm">{t('analytics.memory')}:</Heading> | ||||||
| @@ -167,8 +165,10 @@ const DeviceSummary = ({ serialNumber }: Props) => { | |||||||
|               <Heading size="sm">{t('devices.certificate_expires_in')}:</Heading> |               <Heading size="sm">{t('devices.certificate_expires_in')}:</Heading> | ||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1}> |             <GridItem colSpan={1}> | ||||||
|               {getStatus.data?.certificateExpiryDate && ( |               {getDevice.data?.certificateExpiryDate ? ( | ||||||
|                 <FormattedDate date={getStatus.data?.certificateExpiryDate} hidePrefix /> |                 <FormattedDate date={getDevice.data?.certificateExpiryDate} hidePrefix /> | ||||||
|  |               ) : ( | ||||||
|  |                 '-' | ||||||
|               )} |               )} | ||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1} alignContent="center" alignItems="center"> |             <GridItem colSpan={1} alignContent="center" alignItems="center"> | ||||||
| @@ -176,7 +176,7 @@ const DeviceSummary = ({ serialNumber }: Props) => { | |||||||
|             </GridItem> |             </GridItem> | ||||||
|             <GridItem colSpan={1}> |             <GridItem colSpan={1}> | ||||||
|               {getStatus.data?.connectReason && getStatus.data?.connectReason.length > 0 |               {getStatus.data?.connectReason && getStatus.data?.connectReason.length > 0 | ||||||
|                 ? getStatus.data?.connectReason |                 ? uppercaseFirstLetter(getStatus.data?.connectReason) | ||||||
|                 : '-'} |                 : '-'} | ||||||
|             </GridItem> |             </GridItem> | ||||||
|           </Grid> |           </Grid> | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								src/pages/Device/SwitchPortExamination/Actions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { IconButton, Tooltip, useToast } from '@chakra-ui/react'; | ||||||
|  | import { Power } from '@phosphor-icons/react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { usePowerCycle } from 'hooks/Network/Devices'; | ||||||
|  | import { useNotification } from 'hooks/useNotification'; | ||||||
|  | import { DeviceLinkState } from 'hooks/Network/Statistics'; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   state: DeviceLinkState & { name: string }; | ||||||
|  |   deviceSerialNumber: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const LinkStateTableActions = ({ state, deviceSerialNumber }: Props) => { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const powerCycle = usePowerCycle(); | ||||||
|  |   const toast = useToast(); | ||||||
|  |   const { successToast, apiErrorToast } = useNotification(); | ||||||
|  |  | ||||||
|  |   const onPowerCycle = () => { | ||||||
|  |     powerCycle.mutate( | ||||||
|  |       { serial: deviceSerialNumber, when: 0, ports: [{ name: state.name, cycle: 10 * 1000 }] }, | ||||||
|  |       { | ||||||
|  |         onSuccess: (data) => { | ||||||
|  |           if (data.errorCode === 0) { | ||||||
|  |             successToast({ | ||||||
|  |               description: `Power cycle started for port ${state.name} for 10s`, | ||||||
|  |             }); | ||||||
|  |           } else if (data.errorCode === 1) { | ||||||
|  |             toast({ | ||||||
|  |               id: `powercycle-warning-${deviceSerialNumber}`, | ||||||
|  |               title: 'Warning', | ||||||
|  |               description: `${data?.errorText ?? 'Unknown Warning'}`, | ||||||
|  |               status: 'warning', | ||||||
|  |               duration: 5000, | ||||||
|  |               isClosable: true, | ||||||
|  |               position: 'top-right', | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             toast({ | ||||||
|  |               id: `powercycle-error-${deviceSerialNumber}`, | ||||||
|  |               title: t('common.error'), | ||||||
|  |               description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`, | ||||||
|  |               status: 'error', | ||||||
|  |               duration: 5000, | ||||||
|  |               isClosable: true, | ||||||
|  |               position: 'top-right', | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         onError: (e) => apiErrorToast({ e }), | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Tooltip label="Power Cycle" placement="auto-start"> | ||||||
|  |       <IconButton | ||||||
|  |         aria-label="Power Cycle" | ||||||
|  |         icon={<Power size={20} />} | ||||||
|  |         colorScheme="green" | ||||||
|  |         onClick={onPowerCycle} | ||||||
|  |         isLoading={powerCycle.isLoading} | ||||||
|  |         size="xs" | ||||||
|  |       /> | ||||||
|  |     </Tooltip> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default LinkStateTableActions; | ||||||
							
								
								
									
										197
									
								
								src/pages/Device/SwitchPortExamination/LinkStateTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,197 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Alert, AlertDescription, AlertIcon, Center } from '@chakra-ui/react'; | ||||||
|  | import { DeviceLinkState } from 'hooks/Network/Statistics'; | ||||||
|  | import DataCell from 'components/TableCells/DataCell'; | ||||||
|  | import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; | ||||||
|  | import { DataGrid } from 'components/DataTables/DataGrid'; | ||||||
|  | import { uppercaseFirstLetter } from 'helpers/stringHelper'; | ||||||
|  | import LinkStateTableActions from './Actions'; | ||||||
|  |  | ||||||
|  | type Row = DeviceLinkState & { name: string }; | ||||||
|  | const dataCell = (v: number) => <DataCell bytes={v} />; | ||||||
|  | const actionCell = (row: Row, serialNumber: string) => ( | ||||||
|  |   <LinkStateTableActions state={row} deviceSerialNumber={serialNumber} /> | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   statistics?: Row[]; | ||||||
|  |   refetch: () => void; | ||||||
|  |   isFetching: boolean; | ||||||
|  |   type: 'upstream' | 'downstream'; | ||||||
|  |   serialNumber: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber }: Props) => { | ||||||
|  |   const tableController = useDataGrid({ | ||||||
|  |     tableSettingsId: 'switch.link-state.table', | ||||||
|  |     defaultOrder: [ | ||||||
|  |       'carrier', | ||||||
|  |       'name', | ||||||
|  |       'duplex', | ||||||
|  |       'speed', | ||||||
|  |       'rx_bytes', | ||||||
|  |       'rx_dropped', | ||||||
|  |       'rx_error', | ||||||
|  |       'rx_packets', | ||||||
|  |       'tx_bytes', | ||||||
|  |       'tx_dropped', | ||||||
|  |       'tx_error', | ||||||
|  |       'tx_packets', | ||||||
|  |       'actions', | ||||||
|  |     ], | ||||||
|  |     defaultSortBy: [{ id: 'name', desc: false }], | ||||||
|  |     showAllRows: true, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const columns: DataGridColumn<Row>[] = React.useMemo( | ||||||
|  |     (): DataGridColumn<Row>[] => [ | ||||||
|  |       { | ||||||
|  |         id: 'carrier', | ||||||
|  |         header: '', | ||||||
|  |         accessorKey: '', | ||||||
|  |         sortingFn: 'alphanumericCaseSensitive', | ||||||
|  |         cell: ({ cell }) => (cell.row.original.carrier ? '🟢' : '🔴'), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'name', | ||||||
|  |         header: 'Name', | ||||||
|  |         accessorKey: 'name', | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'duplex', | ||||||
|  |         header: 'Duplex', | ||||||
|  |         accessorKey: 'duplex', | ||||||
|  |         cell: ({ cell }) => (cell.row.original.duplex ? uppercaseFirstLetter(cell.row.original.duplex) : '-'), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'speed', | ||||||
|  |         header: 'Speed', | ||||||
|  |         accessorKey: 'speed', | ||||||
|  |         cell: ({ cell }) => `${(cell.row.original.speed ?? 0) / 1000} Gbps`, | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_bytes', | ||||||
|  |         header: 'Rx', | ||||||
|  |         accessorKey: 'rx_bytes', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_bytes ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_dropped', | ||||||
|  |         header: 'Rx Dropped', | ||||||
|  |         accessorKey: 'rx_dropped', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_dropped ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_error', | ||||||
|  |         header: 'Rx Errors', | ||||||
|  |         accessorKey: 'rx_error', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_errors ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_packets', | ||||||
|  |         header: 'Rx Packets', | ||||||
|  |         accessorKey: 'counters.rx_packets', | ||||||
|  |         cell: ({ cell }) => (cell.row.original.counters?.rx_packets ?? 0).toLocaleString(), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_bytes', | ||||||
|  |         header: 'Tx', | ||||||
|  |         accessorKey: 'tx_bytes', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_bytes ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_dropped', | ||||||
|  |         header: 'Tx Dropped', | ||||||
|  |         accessorKey: 'tx_dropped', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_dropped ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_error', | ||||||
|  |         header: 'Tx Errors', | ||||||
|  |         accessorKey: 'tx_error', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_errors ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_packets', | ||||||
|  |         header: 'Tx Packets', | ||||||
|  |         accessorKey: 'counters.tx_packets', | ||||||
|  |         cell: ({ cell }) => (cell.row.original.counters?.tx_packets ?? 0).toLocaleString(), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'actions', | ||||||
|  |         header: '', | ||||||
|  |         accessorKey: '', | ||||||
|  |         cell: ({ cell }) => actionCell(cell.row.original, serialNumber), | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     [], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (!statistics || statistics?.length === 0) { | ||||||
|  |     return ( | ||||||
|  |       <Center> | ||||||
|  |         <Alert status="info"> | ||||||
|  |           <AlertIcon /> | ||||||
|  |           <AlertDescription> | ||||||
|  |             There are currently no {type} link-states provided in this devices statistics | ||||||
|  |           </AlertDescription> | ||||||
|  |         </Alert> | ||||||
|  |       </Center> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <DataGrid<Row> | ||||||
|  |       controller={tableController} | ||||||
|  |       header={{ | ||||||
|  |         title: '', | ||||||
|  |         objectListed: 'Statistics', | ||||||
|  |       }} | ||||||
|  |       columns={columns} | ||||||
|  |       isLoading={isFetching} | ||||||
|  |       data={statistics ?? []} | ||||||
|  |       options={{ | ||||||
|  |         refetch, | ||||||
|  |         isHidingControls: true, | ||||||
|  |       }} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default LinkStateTable; | ||||||
							
								
								
									
										172
									
								
								src/pages/Device/SwitchPortExamination/SwitchInterfaceTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,172 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Alert, AlertDescription, AlertIcon, Center } from '@chakra-ui/react'; | ||||||
|  | import { DeviceInterfaceStatistics, DeviceStatistics } from 'hooks/Network/Statistics'; | ||||||
|  | import DataCell from 'components/TableCells/DataCell'; | ||||||
|  | import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; | ||||||
|  | import DurationCell from 'components/TableCells/DurationCell'; | ||||||
|  | import { DataGrid } from 'components/DataTables/DataGrid'; | ||||||
|  |  | ||||||
|  | const dataCell = (v: number) => <DataCell bytes={v} />; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   statistics: DeviceStatistics; | ||||||
|  |   refetch: () => void; | ||||||
|  |   isFetching: boolean; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const SwitchInterfaceTable = ({ statistics, refetch, isFetching }: Props) => { | ||||||
|  |   const tableController = useDataGrid({ | ||||||
|  |     tableSettingsId: 'switch.interfaces.table', | ||||||
|  |     defaultOrder: [ | ||||||
|  |       'name', | ||||||
|  |       'uptime', | ||||||
|  |       'clients', | ||||||
|  |       'rx_bytes', | ||||||
|  |       'rx_dropped', | ||||||
|  |       'rx_error', | ||||||
|  |       'rx_packets', | ||||||
|  |       'tx_bytes', | ||||||
|  |       'tx_dropped', | ||||||
|  |       'tx_error', | ||||||
|  |     ], | ||||||
|  |     defaultSortBy: [{ id: 'name', desc: false }], | ||||||
|  |     showAllRows: true, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const columns: DataGridColumn<DeviceInterfaceStatistics>[] = React.useMemo( | ||||||
|  |     (): DataGridColumn<DeviceInterfaceStatistics>[] => [ | ||||||
|  |       { | ||||||
|  |         id: 'name', | ||||||
|  |         header: 'Name', | ||||||
|  |         accessorKey: 'name', | ||||||
|  |         sortingFn: 'alphanumericCaseSensitive', | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'uptime', | ||||||
|  |         header: 'Uptime', | ||||||
|  |  | ||||||
|  |         accessorKey: 'uptime', | ||||||
|  |         cell: ({ cell }) => <DurationCell seconds={cell.row.original.uptime} />, | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'clients', | ||||||
|  |         header: 'Clients', | ||||||
|  |  | ||||||
|  |         accessorKey: 'clients', | ||||||
|  |         cell: ({ cell }) => cell.row.original.clients?.length ?? 0, | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_bytes', | ||||||
|  |         header: 'Rx', | ||||||
|  |         accessorKey: 'rx_bytes', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_bytes ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_dropped', | ||||||
|  |         header: 'Rx Dropped', | ||||||
|  |         accessorKey: 'rx_dropped', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_dropped ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_error', | ||||||
|  |         header: 'Rx Errors', | ||||||
|  |         accessorKey: 'rx_error', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_errors ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'rx_packets', | ||||||
|  |         header: 'Rx Packets', | ||||||
|  |         accessorKey: 'counters.rx_packets', | ||||||
|  |         cell: ({ cell }) => (cell.row.original.counters?.rx_packets ?? 0).toLocaleString(), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_bytes', | ||||||
|  |         header: 'Tx', | ||||||
|  |         accessorKey: 'tx_bytes', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_bytes ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_dropped', | ||||||
|  |         header: 'Tx Dropped', | ||||||
|  |         accessorKey: 'tx_dropped', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_dropped ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_error', | ||||||
|  |         header: 'Tx Errors', | ||||||
|  |         accessorKey: 'tx_error', | ||||||
|  |         cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_errors ?? 0), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx_packets', | ||||||
|  |         header: 'Tx Packets', | ||||||
|  |         accessorKey: 'counters.tx_packets', | ||||||
|  |         cell: ({ cell }) => (cell.row.original.counters?.tx_packets ?? 0).toLocaleString(), | ||||||
|  |         meta: { | ||||||
|  |           customWidth: '35px', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     [], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (!statistics.interfaces) { | ||||||
|  |     return ( | ||||||
|  |       <Center> | ||||||
|  |         <Alert status="info"> | ||||||
|  |           <AlertIcon /> | ||||||
|  |           <AlertDescription>There are currently no interfaces provided in this devices statistics</AlertDescription> | ||||||
|  |         </Alert> | ||||||
|  |       </Center> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <DataGrid<DeviceInterfaceStatistics> | ||||||
|  |       controller={tableController} | ||||||
|  |       header={{ | ||||||
|  |         title: '', | ||||||
|  |         objectListed: 'Statistics', | ||||||
|  |       }} | ||||||
|  |       columns={columns} | ||||||
|  |       isLoading={isFetching} | ||||||
|  |       data={statistics.interfaces ?? []} | ||||||
|  |       options={{ | ||||||
|  |         refetch, | ||||||
|  |         isHidingControls: true, | ||||||
|  |       }} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default SwitchInterfaceTable; | ||||||
							
								
								
									
										98
									
								
								src/pages/Device/SwitchPortExamination/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,98 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; | ||||||
|  | import LinkStateTable from './LinkStateTable'; | ||||||
|  | import SwitchInterfaceTable from './SwitchInterfaceTable'; | ||||||
|  | import { DeviceLinkState, useGetDeviceLastStats } from 'hooks/Network/Statistics'; | ||||||
|  | import { Card } from 'components/Containers/Card'; | ||||||
|  | import { CardBody } from 'components/Containers/Card/CardBody'; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   serialNumber: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const SwitchPortExamination = ({ serialNumber }: Props) => { | ||||||
|  |   const [tabIndex, setTabIndex] = React.useState(0); | ||||||
|  |  | ||||||
|  |   const handleTabsChange = React.useCallback((index: number) => { | ||||||
|  |     setTabIndex(index); | ||||||
|  |   }, []); | ||||||
|  |   const getStats = useGetDeviceLastStats({ serialNumber }); | ||||||
|  |  | ||||||
|  |   const upLinkStates: (DeviceLinkState & { name: string })[] = React.useMemo(() => { | ||||||
|  |     if (!getStats.data || !getStats.data['link-state']?.upstream) return []; | ||||||
|  |  | ||||||
|  |     return Object.entries(getStats.data['link-state']?.upstream).map(([name, value]) => ({ | ||||||
|  |       ...value, | ||||||
|  |       name, | ||||||
|  |     })); | ||||||
|  |   }, [getStats.data]); | ||||||
|  |   const downLinkStates: (DeviceLinkState & { name: string })[] = React.useMemo(() => { | ||||||
|  |     if (!getStats.data || !getStats.data['link-state']?.downstream) return []; | ||||||
|  |  | ||||||
|  |     return Object.entries(getStats.data['link-state']?.downstream).map(([name, value]) => ({ | ||||||
|  |       ...value, | ||||||
|  |       name, | ||||||
|  |     })); | ||||||
|  |   }, [getStats.data]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Card p={0} mb={4}> | ||||||
|  |       <CardBody p={0} display="block"> | ||||||
|  |         <Tabs index={tabIndex} onChange={handleTabsChange} variant="enclosed" w="100%"> | ||||||
|  |           <TabList> | ||||||
|  |             <Tab fontSize="lg" fontWeight="bold"> | ||||||
|  |               Interfaces | ||||||
|  |             </Tab> | ||||||
|  |             <Tab fontSize="lg" fontWeight="bold"> | ||||||
|  |               Link-State (Up) | ||||||
|  |             </Tab> | ||||||
|  |             <Tab fontSize="lg" fontWeight="bold"> | ||||||
|  |               Link-State (Down) | ||||||
|  |             </Tab> | ||||||
|  |           </TabList> | ||||||
|  |           <TabPanels> | ||||||
|  |             <TabPanel> | ||||||
|  |               {getStats.data ? ( | ||||||
|  |                 <SwitchInterfaceTable | ||||||
|  |                   statistics={getStats.data} | ||||||
|  |                   refetch={getStats.refetch} | ||||||
|  |                   isFetching={getStats.isFetching} | ||||||
|  |                 /> | ||||||
|  |               ) : ( | ||||||
|  |                 <Spinner size="xl" /> | ||||||
|  |               )} | ||||||
|  |             </TabPanel> | ||||||
|  |             <TabPanel> | ||||||
|  |               {getStats.data ? ( | ||||||
|  |                 <LinkStateTable | ||||||
|  |                   statistics={upLinkStates} | ||||||
|  |                   refetch={getStats.refetch} | ||||||
|  |                   isFetching={getStats.isFetching} | ||||||
|  |                   type="upstream" | ||||||
|  |                   serialNumber={serialNumber} | ||||||
|  |                 /> | ||||||
|  |               ) : ( | ||||||
|  |                 <Spinner size="xl" /> | ||||||
|  |               )} | ||||||
|  |             </TabPanel> | ||||||
|  |             <TabPanel> | ||||||
|  |               {getStats.data ? ( | ||||||
|  |                 <LinkStateTable | ||||||
|  |                   statistics={downLinkStates} | ||||||
|  |                   refetch={getStats.refetch} | ||||||
|  |                   isFetching={getStats.isFetching} | ||||||
|  |                   type="downstream" | ||||||
|  |                   serialNumber={serialNumber} | ||||||
|  |                 /> | ||||||
|  |               ) : ( | ||||||
|  |                 <Spinner size="xl" /> | ||||||
|  |               )} | ||||||
|  |             </TabPanel> | ||||||
|  |           </TabPanels> | ||||||
|  |         </Tabs> | ||||||
|  |       </CardBody> | ||||||
|  |     </Card> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default SwitchPortExamination; | ||||||
| @@ -7,7 +7,9 @@ import { | |||||||
|   AccordionPanel, |   AccordionPanel, | ||||||
|   Box, |   Box, | ||||||
|   Button, |   Button, | ||||||
|  |   Center, | ||||||
|   IconButton, |   IconButton, | ||||||
|  |   Spinner, | ||||||
|   Tooltip, |   Tooltip, | ||||||
|   useClipboard, |   useClipboard, | ||||||
|   useColorMode, |   useColorMode, | ||||||
| @@ -17,19 +19,26 @@ import { JsonViewer } from '@textea/json-viewer'; | |||||||
| import { Barcode } from '@phosphor-icons/react'; | import { Barcode } from '@phosphor-icons/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 { useGetDevice } from 'hooks/Network/Devices'; | ||||||
|  | import { RefreshButton } from 'components/Buttons/RefreshButton'; | ||||||
|  |  | ||||||
| const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfiguration }) => { | const ViewConfigurationModal = ({ serialNumber }: { serialNumber: string }) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|  |   const getDevice = useGetDevice({ serialNumber }); | ||||||
|   const { isOpen, onOpen, onClose } = useDisclosure(); |   const { isOpen, onOpen, onClose } = useDisclosure(); | ||||||
|   const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(configuration ?? {}, null, 2)); |   const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(getDevice.data?.configuration ?? {}, null, 2)); | ||||||
|   const { colorMode } = useColorMode(); |   const { colorMode } = useColorMode(); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     if (configuration) { |     if (getDevice.data) { | ||||||
|       setValue(JSON.stringify(configuration, null, 2)); |       setValue(JSON.stringify(getDevice.data.configuration, null, 2)); | ||||||
|     } |     } | ||||||
|   }, [configuration]); |   }, [getDevice.data?.configuration]); | ||||||
|  |  | ||||||
|  |   const handleOpenClick = () => { | ||||||
|  |     getDevice.refetch(); | ||||||
|  |     onOpen(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| @@ -37,7 +46,7 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi | |||||||
|         <IconButton |         <IconButton | ||||||
|           aria-label={t('configurations.one')} |           aria-label={t('configurations.one')} | ||||||
|           icon={<Barcode size={20} />} |           icon={<Barcode size={20} />} | ||||||
|           onClick={onOpen} |           onClick={handleOpenClick} | ||||||
|           colorScheme="purple" |           colorScheme="purple" | ||||||
|         /> |         /> | ||||||
|       </Tooltip> |       </Tooltip> | ||||||
| @@ -45,14 +54,17 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi | |||||||
|         isOpen={isOpen} |         isOpen={isOpen} | ||||||
|         title={t('configurations.one')} |         title={t('configurations.one')} | ||||||
|         topRightButtons={ |         topRightButtons={ | ||||||
|           <Button onClick={onCopy} size="md" colorScheme="teal"> |           <> | ||||||
|             {hasCopied ? `${t('common.copied')}!` : t('common.copy')} |             <Button onClick={onCopy} size="md" colorScheme="teal"> | ||||||
|           </Button> |               {hasCopied ? `${t('common.copied')}!` : t('common.copy')} | ||||||
|  |             </Button> | ||||||
|  |             <RefreshButton onClick={getDevice.refetch} isFetching={getDevice.isFetching} /> | ||||||
|  |           </> | ||||||
|         } |         } | ||||||
|         onClose={onClose} |         onClose={onClose} | ||||||
|       > |       > | ||||||
|         <Box display="inline-block" w="100%"> |         <Box display="inline-block" w="100%"> | ||||||
|           {configuration && ( |           {getDevice.data && !getDevice.isFetching ? ( | ||||||
|             <Box maxH="calc(100vh - 250px)" minH="300px" overflowY="auto"> |             <Box maxH="calc(100vh - 250px)" minH="300px" overflowY="auto"> | ||||||
|               <Accordion defaultIndex={0} allowToggle> |               <Accordion defaultIndex={0} allowToggle> | ||||||
|                 <AccordionItem> |                 <AccordionItem> | ||||||
| @@ -71,7 +83,7 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi | |||||||
|                       enableClipboard={false} |                       enableClipboard={false} | ||||||
|                       theme={colorMode === 'light' ? undefined : 'dark'} |                       theme={colorMode === 'light' ? undefined : 'dark'} | ||||||
|                       defaultInspectDepth={1} |                       defaultInspectDepth={1} | ||||||
|                       value={configuration as object} |                       value={getDevice.data.configuration as object} | ||||||
|                       style={{ background: 'unset', display: 'unset' }} |                       style={{ background: 'unset', display: 'unset' }} | ||||||
|                     /> |                     /> | ||||||
|                   </AccordionPanel> |                   </AccordionPanel> | ||||||
| @@ -86,11 +98,15 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi | |||||||
|                     </AccordionButton> |                     </AccordionButton> | ||||||
|                   </h2> |                   </h2> | ||||||
|                   <AccordionPanel pb={4} overflowX="auto"> |                   <AccordionPanel pb={4} overflowX="auto"> | ||||||
|                     <pre>{JSON.stringify(configuration, null, 2)}</pre> |                     <pre>{JSON.stringify(getDevice.data.configuration, null, 2)}</pre> | ||||||
|                   </AccordionPanel> |                   </AccordionPanel> | ||||||
|                 </AccordionItem> |                 </AccordionItem> | ||||||
|               </Accordion> |               </Accordion> | ||||||
|             </Box> |             </Box> | ||||||
|  |           ) : ( | ||||||
|  |             <Center my={12}> | ||||||
|  |               <Spinner size="xl" /> | ||||||
|  |             </Center> | ||||||
|           )} |           )} | ||||||
|         </Box> |         </Box> | ||||||
|       </Modal> |       </Modal> | ||||||
| @@ -98,4 +114,4 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default ViewConfigurationModal; | export default React.memo(ViewConfigurationModal); | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import { ColumnPicker } from 'components/DataTables/ColumnPicker'; | |||||||
| import { DataTable } from 'components/DataTables/DataTable'; | import { DataTable } from 'components/DataTables/DataTable'; | ||||||
| import DataCell from 'components/TableCells/DataCell'; | import DataCell from 'components/TableCells/DataCell'; | ||||||
| import { Column } from 'models/Table'; | import { Column } from 'models/Table'; | ||||||
|  | import IpCell from './IpCell'; | ||||||
|  |  | ||||||
| export type ParsedAssociation = { | export type ParsedAssociation = { | ||||||
|   radio?: ParsedRadio; |   radio?: ParsedRadio; | ||||||
| @@ -27,6 +28,7 @@ export type ParsedAssociation = { | |||||||
|   txNss: number | string; |   txNss: number | string; | ||||||
|   recorded: number; |   recorded: number; | ||||||
|   dynamicVlan?: number; |   dynamicVlan?: number; | ||||||
|  |   fingerprint?: object; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
| @@ -35,7 +37,7 @@ type Props = { | |||||||
|   isSingle?: boolean; |   isSingle?: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => { | const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); |   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); | ||||||
|  |  | ||||||
| @@ -50,6 +52,10 @@ const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => { | |||||||
|   ); |   ); | ||||||
|   const dataCell = React.useCallback((v: number) => <DataCell bytes={v} />, []); |   const dataCell = React.useCallback((v: number) => <DataCell bytes={v} />, []); | ||||||
|   const indexCell = React.useCallback((assoc: ParsedAssociation) => assoc.radio?.band ?? assoc.radio?.deductedBand, []); |   const indexCell = React.useCallback((assoc: ParsedAssociation) => assoc.radio?.band ?? assoc.radio?.deductedBand, []); | ||||||
|  |   const ipCell = React.useCallback( | ||||||
|  |     (assoc: ParsedAssociation) => <IpCell ipv4={assoc.ips.ipv4} ipv6={assoc.ips.ipv6} />, | ||||||
|  |     [], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   const columns: Column<ParsedAssociation>[] = React.useMemo( |   const columns: Column<ParsedAssociation>[] = React.useMemo( | ||||||
|     (): Column<ParsedAssociation>[] => [ |     (): Column<ParsedAssociation>[] => [ | ||||||
| @@ -72,6 +78,28 @@ const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => { | |||||||
|         isMonospace: true, |         isMonospace: true, | ||||||
|         alwaysShow: true, |         alwaysShow: true, | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         id: 'ssid', | ||||||
|  |         Header: 'SSID', | ||||||
|  |         Footer: '', | ||||||
|  |         accessor: 'ssid', | ||||||
|  |         customWidth: '35px', | ||||||
|  |         alwaysShow: true, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'ips', | ||||||
|  |         Header: 'IPs', | ||||||
|  |         Footer: '', | ||||||
|  |         Cell: (v) => ipCell(v.cell.row.original), | ||||||
|  |         disableSortBy: true, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'fingerprint', | ||||||
|  |         Header: 'Fingerprint', | ||||||
|  |         Footer: '', | ||||||
|  |         Cell: (v) => Object.values(v.cell.row.original.fingerprint ?? {}).join(', '), | ||||||
|  |         disableSortBy: true, | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         id: 'vendor', |         id: 'vendor', | ||||||
|         Header: t('controller.wifi.vendor'), |         Header: t('controller.wifi.vendor'), | ||||||
| @@ -154,7 +182,7 @@ const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => { | |||||||
|         customWidth: '35px', |         customWidth: '35px', | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|     [t], |     [t, ouis], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -195,4 +223,4 @@ const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default WifiAnalysisAssocationsTable; | export default WifiAnalysisAssociationsTable; | ||||||
|   | |||||||
							
								
								
									
										90
									
								
								src/pages/Device/WifiAnalysis/IpCell.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,90 @@ | |||||||
|  | import * as React from 'react'; | ||||||
|  | import { CopyIcon } from '@chakra-ui/icons'; | ||||||
|  | import { | ||||||
|  |   Button, | ||||||
|  |   Flex, | ||||||
|  |   Heading, | ||||||
|  |   IconButton, | ||||||
|  |   ListItem, | ||||||
|  |   Popover, | ||||||
|  |   PopoverArrow, | ||||||
|  |   PopoverBody, | ||||||
|  |   PopoverCloseButton, | ||||||
|  |   PopoverContent, | ||||||
|  |   PopoverHeader, | ||||||
|  |   PopoverTrigger, | ||||||
|  |   Text, | ||||||
|  |   Tooltip, | ||||||
|  |   UnorderedList, | ||||||
|  |   useBoolean, | ||||||
|  |   useClipboard, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
|  |  | ||||||
|  | const CopyString = ({ str }: { str: string }) => { | ||||||
|  |   const [isHovered, setHovered] = useBoolean(false); | ||||||
|  |   const copy = useClipboard(str); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Flex alignItems="center" onMouseEnter={setHovered.on} onMouseLeave={setHovered.off}> | ||||||
|  |       <Text>{str}</Text> | ||||||
|  |       <Tooltip label={copy.hasCopied ? 'Copied!' : 'Copy'} placement="top"> | ||||||
|  |         <IconButton | ||||||
|  |           aria-label={copy.hasCopied ? 'Copied!' : 'Copy'} | ||||||
|  |           size="sm" | ||||||
|  |           onClick={copy.onCopy} | ||||||
|  |           icon={<CopyIcon />} | ||||||
|  |           variant="transparent" | ||||||
|  |           opacity={!isHovered ? 0 : 1} | ||||||
|  |         /> | ||||||
|  |       </Tooltip> | ||||||
|  |     </Flex> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type Props = { | ||||||
|  |   ipv4: string[]; | ||||||
|  |   ipv6: string[]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const IpCell = ({ ipv4, ipv6 }: Props) => { | ||||||
|  |   const length = ipv4.length + ipv6.length; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Popover> | ||||||
|  |       <PopoverTrigger> | ||||||
|  |         <Button colorScheme="teal" size="sm"> | ||||||
|  |           {length} | ||||||
|  |         </Button> | ||||||
|  |       </PopoverTrigger> | ||||||
|  |       <PopoverContent> | ||||||
|  |         <PopoverArrow /> | ||||||
|  |         <PopoverCloseButton /> | ||||||
|  |         <PopoverHeader> | ||||||
|  |           <Heading size="sm"> | ||||||
|  |             {length} {length === 1 ? 'IP' : 'IPs'} | ||||||
|  |           </Heading> | ||||||
|  |         </PopoverHeader> | ||||||
|  |         <PopoverBody> | ||||||
|  |           <Heading size="sm">IpV4 ({ipv4.length})</Heading> | ||||||
|  |           <UnorderedList> | ||||||
|  |             {ipv4.map((ip) => ( | ||||||
|  |               <ListItem key={ip}> | ||||||
|  |                 <CopyString str={ip} /> | ||||||
|  |               </ListItem> | ||||||
|  |             ))} | ||||||
|  |           </UnorderedList> | ||||||
|  |           <Heading size="sm">IpV6 ({ipv6.length})</Heading> | ||||||
|  |           <UnorderedList> | ||||||
|  |             {ipv6.map((ip) => ( | ||||||
|  |               <ListItem key={ip}> | ||||||
|  |                 <CopyString str={ip} /> | ||||||
|  |               </ListItem> | ||||||
|  |             ))} | ||||||
|  |           </UnorderedList> | ||||||
|  |         </PopoverBody> | ||||||
|  |       </PopoverContent> | ||||||
|  |     </Popover> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default IpCell; | ||||||
| @@ -17,14 +17,18 @@ export type ParsedRadio = { | |||||||
|   activeMs: string; |   activeMs: string; | ||||||
|   busyMs: string; |   busyMs: string; | ||||||
|   receiveMs: string; |   receiveMs: string; | ||||||
|  |   sendMs: string; | ||||||
|   phy: string; |   phy: string; | ||||||
|  |   frequency: string; | ||||||
|  |   temperature: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   data?: ParsedRadio[]; |   data?: ParsedRadio[]; | ||||||
|  |   isSingle?: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const WifiAnalysisRadioTable = ({ data }: Props) => { | const WifiAnalysisRadioTable = ({ data, isSingle }: Props) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); |   const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]); | ||||||
|  |  | ||||||
| @@ -44,19 +48,27 @@ const WifiAnalysisRadioTable = ({ data }: Props) => { | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         id: 'channel', |         id: 'channel', | ||||||
|         Header: 'Ch', |         Header: 'Ch.', | ||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'channel', |         accessor: 'channel', | ||||||
|         customWidth: '35px', |         customWidth: '35px', | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         id: 'channelWidth', |         id: 'channelWidth', | ||||||
|         Header: t('controller.wifi.channel_width'), |         Header: 'Ch. W', | ||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'channelWidth', |         accessor: 'channelWidth', | ||||||
|         customWidth: '35px', |         customWidth: '35px', | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         id: 'tx-power', | ||||||
|  |         Header: 'Tx Pow.', | ||||||
|  |         Footer: '', | ||||||
|  |         accessor: 'txPower', | ||||||
|  |         customWidth: '35px', | ||||||
|  |         disableSortBy: true, | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         id: 'noise', |         id: 'noise', | ||||||
|         Header: t('controller.wifi.noise'), |         Header: t('controller.wifi.noise'), | ||||||
| @@ -67,25 +79,49 @@ const WifiAnalysisRadioTable = ({ data }: Props) => { | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         id: 'activeMs', |         id: 'activeMs', | ||||||
|         Header: t('controller.wifi.active_ms'), |         Header: 'Active (ms)', | ||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'activeMs', |         accessor: 'activeMs', | ||||||
|         customWidth: '35px', |         customWidth: '105px', | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         id: 'busyMs', |         id: 'busyMs', | ||||||
|         Header: t('controller.wifi.busy_ms'), |         Header: 'Busy (ms)', | ||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'busyMs', |         accessor: 'busyMs', | ||||||
|         customWidth: '35px', |         customWidth: '105px', | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         id: 'receiveMs', |         id: 'receiveMs', | ||||||
|         Header: t('controller.wifi.receive_ms'), |         Header: 'Receive (ms)', | ||||||
|         Footer: '', |         Footer: '', | ||||||
|         accessor: 'receiveMs', |         accessor: 'receiveMs', | ||||||
|  |         customWidth: '105px', | ||||||
|  |         disableSortBy: true, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'sendMs', | ||||||
|  |         Header: 'Send (ms)', | ||||||
|  |         Footer: '', | ||||||
|  |         accessor: 'sendMs', | ||||||
|  |         customWidth: '105px', | ||||||
|  |         disableSortBy: true, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'temperature', | ||||||
|  |         Header: 'Temp.', | ||||||
|  |         Footer: '', | ||||||
|  |         accessor: 'temperature', | ||||||
|  |         customWidth: '35px', | ||||||
|  |         disableSortBy: true, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'frequency', | ||||||
|  |         Header: 'Frequency', | ||||||
|  |         Footer: '', | ||||||
|  |         accessor: 'frequency', | ||||||
|         customWidth: '35px', |         customWidth: '35px', | ||||||
|         disableSortBy: true, |         disableSortBy: true, | ||||||
|       }, |       }, | ||||||
| @@ -97,7 +133,7 @@ const WifiAnalysisRadioTable = ({ data }: Props) => { | |||||||
|     <> |     <> | ||||||
|       <Flex> |       <Flex> | ||||||
|         <Heading size="sm" mt={2} my="auto"> |         <Heading size="sm" mt={2} my="auto"> | ||||||
|           {t('configurations.radios')} ({data?.length}) |           {isSingle ? 'Radio' : `${t('configurations.radios')} (${data?.length})`} | ||||||
|         </Heading> |         </Heading> | ||||||
|         <Spacer /> |         <Spacer /> | ||||||
|         <ColumnPicker |         <ColumnPicker | ||||||
|   | |||||||
| @@ -16,11 +16,29 @@ type Props = { | |||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const parseRadios = (t: (str: string) => string, data: { data: DeviceStatistics; recorded: number }) => { | const parseRadios = (_: (str: string) => string, data: { data: DeviceStatistics; recorded: number }) => { | ||||||
|   const radios: ParsedRadio[] = []; |   const radios: ParsedRadio[] = []; | ||||||
|   if (data.data.radios) { |   if (data.data.radios) { | ||||||
|     for (let i = 0; i < data.data.radios.length; i += 1) { |     for (let i = 0; i < data.data.radios.length; i += 1) { | ||||||
|       const radio = data.data.radios[i]; |       const radio = data.data.radios[i]; | ||||||
|  |       let temperature = radio?.temperature; | ||||||
|  |       if (temperature) temperature = temperature > 1000 ? Math.round(temperature / 1000) : temperature; | ||||||
|  |  | ||||||
|  |       const tempNoise = radio?.noise ?? radio?.survey?.[0]?.noise; | ||||||
|  |       const noise = tempNoise ? parseDbm(tempNoise) : '-'; | ||||||
|  |  | ||||||
|  |       const tempActiveMs = radio?.survey?.[0]?.time ?? radio?.active_ms; | ||||||
|  |       const activeMs = tempActiveMs?.toLocaleString() ?? '-'; | ||||||
|  |  | ||||||
|  |       const tempBusyMs = radio?.survey?.[0]?.busy ?? radio?.busy_ms; | ||||||
|  |       const busyMs = tempBusyMs?.toLocaleString() ?? '-'; | ||||||
|  |  | ||||||
|  |       const tempReceiveMs = radio?.survey?.[0]?.time_rx ?? radio?.receive_ms; | ||||||
|  |       const receiveMs = tempReceiveMs?.toLocaleString() ?? '-'; | ||||||
|  |  | ||||||
|  |       const tempSendMs = radio?.survey?.[0]?.time_tx; | ||||||
|  |       const sendMs = tempSendMs?.toLocaleString() ?? '-'; | ||||||
|  |  | ||||||
|       if (radio) { |       if (radio) { | ||||||
|         radios.push({ |         radios.push({ | ||||||
|           recorded: data.recorded, |           recorded: data.recorded, | ||||||
| @@ -29,12 +47,15 @@ const parseRadios = (t: (str: string) => string, data: { data: DeviceStatistics; | |||||||
|           deductedBand: radio.channel && radio.channel > 16 ? '5G' : '2G', |           deductedBand: radio.channel && radio.channel > 16 ? '5G' : '2G', | ||||||
|           channel: radio.channel, |           channel: radio.channel, | ||||||
|           channelWidth: radio.channel_width, |           channelWidth: radio.channel_width, | ||||||
|           noise: radio.noise ? parseDbm(radio.noise) : '-', |           noise, | ||||||
|           txPower: radio.tx_power ?? '-', |           txPower: radio.tx_power ?? '-', | ||||||
|           activeMs: compactSecondsToDetailed(radio?.active_ms ? Math.floor(radio.active_ms / 1000) : 0, t), |           activeMs, | ||||||
|           busyMs: compactSecondsToDetailed(radio?.busy_ms ? Math.floor(radio.busy_ms / 1000) : 0, t), |           busyMs, | ||||||
|           receiveMs: compactSecondsToDetailed(radio?.receive_ms ? Math.floor(radio.receive_ms / 1000) : 0, t), |           receiveMs, | ||||||
|  |           sendMs, | ||||||
|           phy: radio.phy, |           phy: radio.phy, | ||||||
|  |           temperature: temperature ? temperature.toString() : '-', | ||||||
|  |           frequency: radio.frequency?.join(', ') ?? '-', | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -84,6 +105,7 @@ const parseAssociations = (data: { data: DeviceStatistics; recorded: number }, r | |||||||
|           txNss: association.tx_rate.nss ?? '-', |           txNss: association.tx_rate.nss ?? '-', | ||||||
|           recorded: data.recorded, |           recorded: data.recorded, | ||||||
|           dynamicVlan: association.dynamic_vlan, |           dynamicVlan: association.dynamic_vlan, | ||||||
|  |           fingerprint: association.fingerprint, | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -162,7 +184,7 @@ const WifiAnalysisCard = ({ serialNumber }: Props) => { | |||||||
|             <SliderTrack> |             <SliderTrack> | ||||||
|               <SliderFilledTrack /> |               <SliderFilledTrack /> | ||||||
|             </SliderTrack> |             </SliderTrack> | ||||||
|             <SliderThumb /> |             <SliderThumb zIndex={0} /> | ||||||
|           </Slider> |           </Slider> | ||||||
|         )} |         )} | ||||||
|         <Box /> |         <Box /> | ||||||
|   | |||||||
| @@ -42,10 +42,13 @@ 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 { RebootModal } from 'components/Modals/RebootModal'; | ||||||
| import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; | import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; | ||||||
|  | import ethernetConnected from './ethernetIconConnected.svg?react'; | ||||||
|  | import ethernetDisconnected from './ethernetIconDisconnected.svg?react'; | ||||||
| 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 { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; | import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; | ||||||
|  | import SwitchPortExamination from './SwitchPortExamination'; | ||||||
|  |  | ||||||
| type Props = { | type Props = { | ||||||
|   serialNumber: string; |   serialNumber: string; | ||||||
| @@ -77,19 +80,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|   const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; |   const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; | ||||||
|   const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); |   const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); | ||||||
|  |  | ||||||
|   const handleDeleteClick = () => |   const handleDeleteClick = () => { | ||||||
|     deleteDevice(serialNumber, { |     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) => { |       onError: (e) => { | ||||||
|         if (axios.isAxiosError(e)) { |         if (axios.isAxiosError(e)) { | ||||||
|           toast({ |           toast({ | ||||||
| @@ -104,18 +96,43 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|  |     toast({ | ||||||
|  |       id: `delete-device-success-${serialNumber}`, | ||||||
|  |       title: t('common.success'), | ||||||
|  |       status: 'success', | ||||||
|  |       duration: 5000, | ||||||
|  |       isClosable: true, | ||||||
|  |       position: 'top-right', | ||||||
|  |     }); | ||||||
|  |     navigate('/'); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const connectedTag = React.useMemo(() => { |   const connectedTag = React.useMemo(() => { | ||||||
|     if (!getStatus.data) return null; |     if (!getStatus.data) return null; | ||||||
|  |  | ||||||
|  |     if (getDevice.data?.blackListed) { | ||||||
|  |       return ( | ||||||
|  |         <ResponsiveTag | ||||||
|  |           label="Blacklisted" | ||||||
|  |           tooltip="This device is blacklisted, it will not be able to connect to the network. Please visit the Blacklist page if you wish to remove it from the blacklist." | ||||||
|  |           colorScheme="red" | ||||||
|  |           icon={LockSimple} | ||||||
|  |         /> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let icon = getStatus.data.connected ? WifiHigh : WifiSlash; | ||||||
|  |     if (getDevice.data?.deviceType === 'switch') | ||||||
|  |       icon = getStatus.data.connected ? ethernetConnected : ethernetDisconnected; | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <ResponsiveTag |       <ResponsiveTag | ||||||
|         label={getStatus?.data?.connected ? t('common.connected') : t('common.disconnected')} |         label={getStatus?.data?.connected ? t('common.connected') : t('common.disconnected')} | ||||||
|         colorScheme={getStatus?.data?.connected ? 'green' : 'red'} |         colorScheme={getStatus?.data?.connected ? 'green' : 'red'} | ||||||
|         icon={getStatus.data.connected ? WifiHigh : WifiSlash} |         icon={icon} | ||||||
|       /> |       /> | ||||||
|     ); |     ); | ||||||
|   }, [getStatus.data]); |   }, [getStatus.data, getDevice.data]); | ||||||
|  |  | ||||||
|   const healthTag = React.useMemo(() => { |   const healthTag = React.useMemo(() => { | ||||||
|     if (!getStatus.data || !getStatus.data.connected || !getHealth.data || getHealth.data?.values?.length === 0) |     if (!getStatus.data || !getStatus.data.connected || !getHealth.data || getHealth.data?.values?.length === 0) | ||||||
| @@ -307,7 +324,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { | |||||||
|           <DeviceSummary serialNumber={serialNumber} /> |           <DeviceSummary serialNumber={serialNumber} /> | ||||||
|           <DeviceDetails serialNumber={serialNumber} /> |           <DeviceDetails serialNumber={serialNumber} /> | ||||||
|           <DeviceStatisticsCard serialNumber={serialNumber} /> |           <DeviceStatisticsCard serialNumber={serialNumber} /> | ||||||
|           <WifiAnalysisCard serialNumber={serialNumber} /> |           {getDevice.data?.deviceType === 'ap' ? <WifiAnalysisCard serialNumber={serialNumber} /> : null} | ||||||
|  |           {getDevice.data?.deviceType === 'switch' ? <SwitchPortExamination serialNumber={serialNumber} /> : null} | ||||||
|           <DeviceLogsCard serialNumber={serialNumber} /> |           <DeviceLogsCard serialNumber={serialNumber} /> | ||||||
|           {getDevice.data && getDevice.data?.hasRADIUSSessions > 0 ? ( |           {getDevice.data && getDevice.data?.hasRADIUSSessions > 0 ? ( | ||||||
|             <RadiusClientsCard serialNumber={serialNumber} /> |             <RadiusClientsCard serialNumber={serialNumber} /> | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								src/pages/Device/ethernetIconConnected.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <svg viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#48BB78;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><rect class="cls-1" x="1.5" y="1.5" width="21" height="21" rx="1.91"/><polygon class="cls-1" points="5.32 6.27 5.32 13.91 7.23 13.91 7.23 15.82 10.09 15.82 10.09 17.73 13.91 17.73 13.91 15.82 16.77 15.82 16.77 13.91 18.68 13.91 18.68 6.27 5.32 6.27"/><line class="cls-1" x1="8.18" y1="9.14" x2="8.18" y2="6.27"/><line class="cls-1" x1="12" y1="9.14" x2="12" y2="6.27"/><line class="cls-1" x1="15.82" y1="9.14" x2="15.82" y2="6.27"/></svg> | ||||||
| After Width: | Height: | Size: 673 B | 
							
								
								
									
										2
									
								
								src/pages/Device/ethernetIconDisconnected.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <svg viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#FC8181;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><rect class="cls-1" x="1.5" y="1.5" width="21" height="21" rx="1.91"/><polygon class="cls-1" points="5.32 6.27 5.32 13.91 7.23 13.91 7.23 15.82 10.09 15.82 10.09 17.73 13.91 17.73 13.91 15.82 16.77 15.82 16.77 13.91 18.68 13.91 18.68 6.27 5.32 6.27"/><line class="cls-1" x1="8.18" y1="9.14" x2="8.18" y2="6.27"/><line class="cls-1" x1="12" y1="9.14" x2="12" y2="6.27"/><line class="cls-1" x1="15.82" y1="9.14" x2="15.82" y2="6.27"/></svg> | ||||||
| After Width: | Height: | Size: 673 B | 
| Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.1 KiB | 
| @@ -1,5 +1,16 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { Box, Center, Image, Link, Tag, TagLabel, TagRightIcon, Tooltip, useDisclosure } from '@chakra-ui/react'; | import { | ||||||
|  |   Box, | ||||||
|  |   Center, | ||||||
|  |   Image, | ||||||
|  |   Link, | ||||||
|  |   Select, | ||||||
|  |   Tag, | ||||||
|  |   TagLabel, | ||||||
|  |   TagRightIcon, | ||||||
|  |   Tooltip, | ||||||
|  |   useDisclosure, | ||||||
|  | } from '@chakra-ui/react'; | ||||||
| import { | import { | ||||||
|   CheckCircle, |   CheckCircle, | ||||||
|   Heart, |   Heart, | ||||||
| @@ -8,6 +19,7 @@ import { | |||||||
|   ThermometerCold, |   ThermometerCold, | ||||||
|   ThermometerHot, |   ThermometerHot, | ||||||
|   WarningCircle, |   WarningCircle, | ||||||
|  |   XCircle, | ||||||
| } from '@phosphor-icons/react'; | } from '@phosphor-icons/react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||||
| @@ -37,7 +49,7 @@ import { TraceModal } from 'components/Modals/TraceModal'; | |||||||
| import { WifiScanModal } from 'components/Modals/WifiScanModal'; | import { WifiScanModal } from 'components/Modals/WifiScanModal'; | ||||||
| import DataCell from 'components/TableCells/DataCell'; | import DataCell from 'components/TableCells/DataCell'; | ||||||
| import NumberCell from 'components/TableCells/NumberCell'; | import NumberCell from 'components/TableCells/NumberCell'; | ||||||
| import { DeviceWithStatus, useGetDeviceCount, useGetDevices } from 'hooks/Network/Devices'; | import { DevicePlatform, DeviceWithStatus, useGetDeviceCount, useGetDevices } from 'hooks/Network/Devices'; | ||||||
| import { FirmwareAgeResponse, useGetFirmwareAges } from 'hooks/Network/Firmware'; | import { FirmwareAgeResponse, useGetFirmwareAges } from 'hooks/Network/Firmware'; | ||||||
|  |  | ||||||
| const fourDigitNumber = (v?: number) => { | const fourDigitNumber = (v?: number) => { | ||||||
| @@ -63,6 +75,7 @@ const BADGE_COLORS: Record<string, string> = { | |||||||
|   NO_CERTIFICATE: 'red', |   NO_CERTIFICATE: 'red', | ||||||
|   MISMATCH_SERIAL: 'yellow', |   MISMATCH_SERIAL: 'yellow', | ||||||
|   VERIFIED: 'green', |   VERIFIED: 'green', | ||||||
|  |   BLACKLISTED: 'white', | ||||||
|   SIMULATED: 'purple', |   SIMULATED: 'purple', | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -70,6 +83,7 @@ const DeviceListCard = () => { | |||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const [serialNumber, setSerialNumber] = React.useState<string>(''); |   const [serialNumber, setSerialNumber] = React.useState<string>(''); | ||||||
|  |   const [platform, setPlatform] = React.useState<DevicePlatform>('ALL'); | ||||||
|   const scanModalProps = useDisclosure(); |   const scanModalProps = useDisclosure(); | ||||||
|   const resetModalProps = useDisclosure(); |   const resetModalProps = useDisclosure(); | ||||||
|   const upgradeModalProps = useDisclosure(); |   const upgradeModalProps = useDisclosure(); | ||||||
| @@ -108,13 +122,14 @@ const DeviceListCard = () => { | |||||||
|       'actions', |       'actions', | ||||||
|     ], |     ], | ||||||
|   }); |   }); | ||||||
|   const getCount = useGetDeviceCount({ enabled: true }); |   const getCount = useGetDeviceCount({ enabled: true, platform }); | ||||||
|   const getDevices = useGetDevices({ |   const getDevices = useGetDevices({ | ||||||
|     pageInfo: { |     pageInfo: { | ||||||
|       limit: tableController.pageInfo.pageSize, |       limit: tableController.pageInfo.pageSize, | ||||||
|       index: tableController.pageInfo.pageIndex, |       index: tableController.pageInfo.pageIndex, | ||||||
|     }, |     }, | ||||||
|     enabled: true, |     enabled: true, | ||||||
|  |     platform, | ||||||
|   }); |   }); | ||||||
|   const getAges = useGetFirmwareAges({ |   const getAges = useGetFirmwareAges({ | ||||||
|     serialNumbers: getDevices.data?.devicesWithStatus.map((device) => device.serialNumber), |     serialNumbers: getDevices.data?.devicesWithStatus.map((device) => device.serialNumber), | ||||||
| @@ -159,12 +174,32 @@ const DeviceListCard = () => { | |||||||
|         h="35px" |         h="35px" | ||||||
|         w="35px" |         w="35px" | ||||||
|         borderRadius="50em" |         borderRadius="50em" | ||||||
|         bgColor={BADGE_COLORS[device.simulated ? 'SIMULATED' : device.verifiedCertificate] ?? 'red'} |         bgColor={ | ||||||
|  |           BADGE_COLORS[ | ||||||
|  |             device.simulated ? 'SIMULATED' : device.blackListed ? 'BLACKLISTED' : device.verifiedCertificate | ||||||
|  |           ] ?? 'red' | ||||||
|  |         } | ||||||
|         alignItems="center" |         alignItems="center" | ||||||
|         display="inline-flex" |         display="inline-flex" | ||||||
|         justifyContent="center" |         justifyContent="center" | ||||||
|         position="relative" |         position="relative" | ||||||
|       > |       > | ||||||
|  |         {device.blackListed ? ( | ||||||
|  |           <Tooltip label="This device is blacklisted. If this was done by mistake, please visit the Blacklist page to correct."> | ||||||
|  |             <XCircle | ||||||
|  |               size={44} | ||||||
|  |               color="#ff2600" | ||||||
|  |               weight="duotone" | ||||||
|  |               style={{ | ||||||
|  |                 position: 'absolute', | ||||||
|  |                 // Center vertically and horizontally | ||||||
|  |                 top: '50%', | ||||||
|  |                 left: '50%', | ||||||
|  |                 transform: 'translate(-50%, -50%)', | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |           </Tooltip> | ||||||
|  |         ) : null} | ||||||
|         <Tooltip |         <Tooltip | ||||||
|           label={`${device.simulated ? 'SIMULATED' : device.verifiedCertificate} - ${ |           label={`${device.simulated ? 'SIMULATED' : device.verifiedCertificate} - ${ | ||||||
|             device.connected ? t('common.connected') : t('common.disconnected') |             device.connected ? t('common.connected') : t('common.disconnected') | ||||||
| @@ -182,6 +217,7 @@ const DeviceListCard = () => { | |||||||
|           bottom={0} |           bottom={0} | ||||||
|           borderColor="gray.200" |           borderColor="gray.200" | ||||||
|           borderWidth={1} |           borderWidth={1} | ||||||
|  |           hidden={device.blackListed} | ||||||
|         /> |         /> | ||||||
|         {device.restrictedDevice && ( |         {device.restrictedDevice && ( | ||||||
|           <Box |           <Box | ||||||
| @@ -533,12 +569,7 @@ const DeviceListCard = () => { | |||||||
|         header: t('analytics.last_connected'), |         header: t('analytics.last_connected'), | ||||||
|         footer: '', |         footer: '', | ||||||
|         accessorKey: 'lastRecordedContact', |         accessorKey: 'lastRecordedContact', | ||||||
|         cell: (v) => |         cell: (v) => dateCell(v.cell.row.original.lastRecordedContact), | ||||||
|           dateCell( |  | ||||||
|             v.cell.row.original.lastContact !== 0 |  | ||||||
|               ? v.cell.row.original.lastContact |  | ||||||
|               : v.cell.row.original.lastRecordedContact, |  | ||||||
|           ), |  | ||||||
|         enableSorting: false, |         enableSorting: false, | ||||||
|         meta: { |         meta: { | ||||||
|           headerOptions: { |           headerOptions: { | ||||||
| @@ -694,9 +725,23 @@ const DeviceListCard = () => { | |||||||
|       <DataGrid<DeviceWithStatus> |       <DataGrid<DeviceWithStatus> | ||||||
|         controller={tableController} |         controller={tableController} | ||||||
|         header={{ |         header={{ | ||||||
|           title: `${getCount.data?.count} ${t('devices.title')}`, |           title: `${getCount.data?.count ?? 0} ${t('devices.title')}`, | ||||||
|           objectListed: t('devices.title'), |           objectListed: t('devices.title'), | ||||||
|           leftContent: <GlobalSearchBar />, |           leftContent: ( | ||||||
|  |             <> | ||||||
|  |               <GlobalSearchBar /> | ||||||
|  |               <Select | ||||||
|  |                 value={platform} | ||||||
|  |                 onChange={(e) => setPlatform(e.target.value as DevicePlatform)} | ||||||
|  |                 w="max-content" | ||||||
|  |                 ml={2} | ||||||
|  |               > | ||||||
|  |                 <option value="ALL">All</option> | ||||||
|  |                 <option value="ap">APs</option> | ||||||
|  |                 <option value="switch">Switches</option> | ||||||
|  |               </Select> | ||||||
|  |             </> | ||||||
|  |           ), | ||||||
|           otherButtons: ( |           otherButtons: ( | ||||||
|             <ExportDevicesTableButton currentPageSerialNumbers={data.map((device) => device.serialNumber)} /> |             <ExportDevicesTableButton currentPageSerialNumbers={data.map((device) => device.serialNumber)} /> | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -138,6 +138,7 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => { | |||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     if (firmware) { |     if (firmware) { | ||||||
|  |       copy.setValue(firmware?.uri ?? ''); | ||||||
|       setNewDescription(firmware?.description); |       setNewDescription(firmware?.description); | ||||||
|     } |     } | ||||||
|   }, [firmware]); |   }, [firmware]); | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import React from 'react'; | |||||||
| import { Barcode, FloppyDisk, Info, ListBullets, TerminalWindow, UsersThree, WifiHigh } from '@phosphor-icons/react'; | import { Barcode, FloppyDisk, Info, ListBullets, TerminalWindow, UsersThree, WifiHigh } from '@phosphor-icons/react'; | ||||||
| import { Route } from 'models/Routes'; | import { Route } from 'models/Routes'; | ||||||
|  |  | ||||||
|  | const AdvancedSystemPage = React.lazy(() => import('pages/AdvancedSystemPage')); | ||||||
| const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations')); | const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations')); | ||||||
| const DefaultFirmwarePage = React.lazy(() => import('pages/DefaultFirmware')); | const DefaultFirmwarePage = React.lazy(() => import('pages/DefaultFirmware')); | ||||||
| const DevicePage = React.lazy(() => import('pages/Device')); | const DevicePage = React.lazy(() => import('pages/Device')); | ||||||
| @@ -178,6 +179,13 @@ const routes: Route[] = [ | |||||||
|     name: 'system.title', |     name: 'system.title', | ||||||
|     icon: () => <Info size={28} weight="bold" />, |     icon: () => <Info size={28} weight="bold" />, | ||||||
|     children: [ |     children: [ | ||||||
|  |       { | ||||||
|  |         id: 'system-advanced', | ||||||
|  |         authorized: ['root', 'partner', 'admin', 'csr', 'system'], | ||||||
|  |         path: '/systemAdvanced', | ||||||
|  |         name: 'system.advanced', | ||||||
|  |         component: AdvancedSystemPage, | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         id: 'system-configuration', |         id: 'system-configuration', | ||||||
|         authorized: ['root', 'partner', 'admin', 'csr', 'system'], |         authorized: ['root', 'partner', 'admin', 'csr', 'system'], | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { defineConfig } from 'vite'; | |||||||
| import tsconfigPaths from 'vite-tsconfig-paths'; | import tsconfigPaths from 'vite-tsconfig-paths'; | ||||||
| import react from '@vitejs/plugin-react'; | import react from '@vitejs/plugin-react'; | ||||||
| import { VitePWA } from 'vite-plugin-pwa'; | import { VitePWA } from 'vite-plugin-pwa'; | ||||||
|  | import svgr from 'vite-plugin-svgr'; | ||||||
|  |  | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   plugins: [ |   plugins: [ | ||||||
| @@ -44,6 +45,7 @@ export default defineConfig({ | |||||||
|         ], |         ], | ||||||
|       }, |       }, | ||||||
|     }), |     }), | ||||||
|  |     svgr(), | ||||||
|   ], |   ], | ||||||
|   build: { |   build: { | ||||||
|     outDir: './build', |     outDir: './build', | ||||||
|   | |||||||