mirror of
				https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
				synced 2025-11-03 20:27:59 +00:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			release/v2
			...
			v3.0.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9e3df09fb2 | ||
| 
						 | 
					2445935627 | ||
| 
						 | 
					e9f1e4d8da | ||
| 
						 | 
					f3a995f68f | ||
| 
						 | 
					a967163d28 | ||
| 
						 | 
					d3514213ca | ||
| 
						 | 
					a55341f406 | ||
| 
						 | 
					1c9a5bfa18 | ||
| 
						 | 
					179900fab0 | ||
| 
						 | 
					9011e30521 | ||
| 
						 | 
					418f4ce576 | ||
| 
						 | 
					9eb65237f9 | ||
| 
						 | 
					89a667569b | ||
| 
						 | 
					b87091a33a | ||
| 
						 | 
					d9a659acbc | ||
| 
						 | 
					ec8347fd7d | ||
| 
						 | 
					b161729c46 | ||
| 
						 | 
					2194a7fc23 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -18,3 +18,4 @@
 | 
			
		||||
.env.production.local
 | 
			
		||||
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
.vscode/settings.json
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ fullnameOverride: ""
 | 
			
		||||
images:
 | 
			
		||||
  owgwui:
 | 
			
		||||
    repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
 | 
			
		||||
    tag: main
 | 
			
		||||
    tag: v3.0.0
 | 
			
		||||
    pullPolicy: Always
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										154
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										154
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ucentral-client",
 | 
			
		||||
  "version": "2.11.0(7)",
 | 
			
		||||
  "version": "3.0.0(1)",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "ucentral-client",
 | 
			
		||||
      "version": "2.11.0(7)",
 | 
			
		||||
      "version": "3.0.0(1)",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@chakra-ui/anatomy": "^2.1.1",
 | 
			
		||||
@@ -104,11 +104,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/code-frame": {
 | 
			
		||||
      "version": "7.21.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
 | 
			
		||||
      "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==",
 | 
			
		||||
      "version": "7.22.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
 | 
			
		||||
      "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/highlight": "^7.18.6"
 | 
			
		||||
        "@babel/highlight": "^7.22.13",
 | 
			
		||||
        "chalk": "^2.4.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
@@ -154,12 +155,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/generator": {
 | 
			
		||||
      "version": "7.21.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz",
 | 
			
		||||
      "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==",
 | 
			
		||||
      "version": "7.23.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
 | 
			
		||||
      "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/types": "^7.21.5",
 | 
			
		||||
        "@babel/types": "^7.23.0",
 | 
			
		||||
        "@jridgewell/gen-mapping": "^0.3.2",
 | 
			
		||||
        "@jridgewell/trace-mapping": "^0.3.17",
 | 
			
		||||
        "jsesc": "^2.5.1"
 | 
			
		||||
@@ -297,33 +298,34 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/helper-environment-visitor": {
 | 
			
		||||
      "version": "7.21.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz",
 | 
			
		||||
      "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==",
 | 
			
		||||
      "version": "7.22.20",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
 | 
			
		||||
      "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/helper-function-name": {
 | 
			
		||||
      "version": "7.21.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz",
 | 
			
		||||
      "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==",
 | 
			
		||||
      "version": "7.23.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
 | 
			
		||||
      "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/template": "^7.20.7",
 | 
			
		||||
        "@babel/types": "^7.21.0"
 | 
			
		||||
        "@babel/template": "^7.22.15",
 | 
			
		||||
        "@babel/types": "^7.23.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "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,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/types": "^7.18.6"
 | 
			
		||||
        "@babel/types": "^7.22.5"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
@@ -452,27 +454,29 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "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,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/types": "^7.18.6"
 | 
			
		||||
        "@babel/types": "^7.22.5"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/helper-string-parser": {
 | 
			
		||||
      "version": "7.21.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz",
 | 
			
		||||
      "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==",
 | 
			
		||||
      "version": "7.22.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
 | 
			
		||||
      "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/helper-validator-identifier": {
 | 
			
		||||
      "version": "7.19.1",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "7.22.20",
 | 
			
		||||
      "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": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -516,11 +520,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/highlight": {
 | 
			
		||||
      "version": "7.18.6",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "7.22.20",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
 | 
			
		||||
      "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/helper-validator-identifier": "^7.18.6",
 | 
			
		||||
        "chalk": "^2.0.0",
 | 
			
		||||
        "@babel/helper-validator-identifier": "^7.22.20",
 | 
			
		||||
        "chalk": "^2.4.2",
 | 
			
		||||
        "js-tokens": "^4.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
@@ -528,9 +533,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/parser": {
 | 
			
		||||
      "version": "7.21.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz",
 | 
			
		||||
      "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==",
 | 
			
		||||
      "version": "7.23.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
 | 
			
		||||
      "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "parser": "bin/babel-parser.js"
 | 
			
		||||
@@ -1684,33 +1689,33 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/template": {
 | 
			
		||||
      "version": "7.20.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
 | 
			
		||||
      "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
 | 
			
		||||
      "version": "7.22.15",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
 | 
			
		||||
      "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/code-frame": "^7.18.6",
 | 
			
		||||
        "@babel/parser": "^7.20.7",
 | 
			
		||||
        "@babel/types": "^7.20.7"
 | 
			
		||||
        "@babel/code-frame": "^7.22.13",
 | 
			
		||||
        "@babel/parser": "^7.22.15",
 | 
			
		||||
        "@babel/types": "^7.22.15"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.9.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/traverse": {
 | 
			
		||||
      "version": "7.21.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz",
 | 
			
		||||
      "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==",
 | 
			
		||||
      "version": "7.23.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
 | 
			
		||||
      "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/code-frame": "^7.21.4",
 | 
			
		||||
        "@babel/generator": "^7.21.5",
 | 
			
		||||
        "@babel/helper-environment-visitor": "^7.21.5",
 | 
			
		||||
        "@babel/helper-function-name": "^7.21.0",
 | 
			
		||||
        "@babel/helper-hoist-variables": "^7.18.6",
 | 
			
		||||
        "@babel/helper-split-export-declaration": "^7.18.6",
 | 
			
		||||
        "@babel/parser": "^7.21.5",
 | 
			
		||||
        "@babel/types": "^7.21.5",
 | 
			
		||||
        "@babel/code-frame": "^7.22.13",
 | 
			
		||||
        "@babel/generator": "^7.23.0",
 | 
			
		||||
        "@babel/helper-environment-visitor": "^7.22.20",
 | 
			
		||||
        "@babel/helper-function-name": "^7.23.0",
 | 
			
		||||
        "@babel/helper-hoist-variables": "^7.22.5",
 | 
			
		||||
        "@babel/helper-split-export-declaration": "^7.22.6",
 | 
			
		||||
        "@babel/parser": "^7.23.0",
 | 
			
		||||
        "@babel/types": "^7.23.0",
 | 
			
		||||
        "debug": "^4.1.0",
 | 
			
		||||
        "globals": "^11.1.0"
 | 
			
		||||
      },
 | 
			
		||||
@@ -1719,12 +1724,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/types": {
 | 
			
		||||
      "version": "7.21.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz",
 | 
			
		||||
      "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==",
 | 
			
		||||
      "version": "7.23.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
 | 
			
		||||
      "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/helper-string-parser": "^7.21.5",
 | 
			
		||||
        "@babel/helper-validator-identifier": "^7.19.1",
 | 
			
		||||
        "@babel/helper-string-parser": "^7.22.5",
 | 
			
		||||
        "@babel/helper-validator-identifier": "^7.22.20",
 | 
			
		||||
        "to-fast-properties": "^2.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
@@ -4579,7 +4584,8 @@
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ansi-styles": {
 | 
			
		||||
      "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": {
 | 
			
		||||
        "color-convert": "^1.9.0"
 | 
			
		||||
      },
 | 
			
		||||
@@ -4754,9 +4760,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/axios": {
 | 
			
		||||
      "version": "1.3.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz",
 | 
			
		||||
      "integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==",
 | 
			
		||||
      "version": "1.6.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
 | 
			
		||||
      "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "follow-redirects": "^1.15.0",
 | 
			
		||||
        "form-data": "^4.0.0",
 | 
			
		||||
@@ -5005,7 +5011,8 @@
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/chalk": {
 | 
			
		||||
      "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": {
 | 
			
		||||
        "ansi-styles": "^3.2.1",
 | 
			
		||||
        "escape-string-regexp": "^1.0.5",
 | 
			
		||||
@@ -5017,7 +5024,8 @@
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/chalk/node_modules/escape-string-regexp": {
 | 
			
		||||
      "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": {
 | 
			
		||||
        "node": ">=0.8.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -5108,14 +5116,16 @@
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/color-convert": {
 | 
			
		||||
      "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": {
 | 
			
		||||
        "color-name": "1.1.3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/color-name": {
 | 
			
		||||
      "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": {
 | 
			
		||||
      "version": "2.0.20",
 | 
			
		||||
@@ -6769,7 +6779,8 @@
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/has-flag": {
 | 
			
		||||
      "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": {
 | 
			
		||||
        "node": ">=4"
 | 
			
		||||
      }
 | 
			
		||||
@@ -8438,9 +8449,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/postcss": {
 | 
			
		||||
      "version": "8.4.28",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
 | 
			
		||||
      "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==",
 | 
			
		||||
      "version": "8.4.31",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
 | 
			
		||||
      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "opencollective",
 | 
			
		||||
@@ -9665,7 +9676,8 @@
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/supports-color": {
 | 
			
		||||
      "version": "5.5.0",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "has-flag": "^3.0.0"
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ucentral-client",
 | 
			
		||||
  "version": "2.11.0(7)",
 | 
			
		||||
  "version": "3.0.0(1)",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "main": "index.tsx",
 | 
			
		||||
 
 | 
			
		||||
@@ -269,6 +269,7 @@
 | 
			
		||||
		"map": "Karte",
 | 
			
		||||
		"max": "Max",
 | 
			
		||||
		"min": "MINDEST",
 | 
			
		||||
		"miscellaneous": "Verschiedenes",
 | 
			
		||||
		"mode": "Modus",
 | 
			
		||||
		"model": "Modell",
 | 
			
		||||
		"modified": "Geändert",
 | 
			
		||||
@@ -737,6 +738,7 @@
 | 
			
		||||
	"form": {
 | 
			
		||||
		"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.",
 | 
			
		||||
		"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_email": "Ungültige E-Mail",
 | 
			
		||||
		"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_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.",
 | 
			
		||||
		"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_value": "Mindestwert von {{min}}",
 | 
			
		||||
		"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_phone_to_notify": "Neues Telefon zu benachrichtigen",
 | 
			
		||||
@@ -905,6 +911,11 @@
 | 
			
		||||
		"one": "Benachrichtigung",
 | 
			
		||||
		"other": "Benachrichtigungen"
 | 
			
		||||
	},
 | 
			
		||||
	"openroaming": {
 | 
			
		||||
		"pool_strategy": "Pool-Strategie",
 | 
			
		||||
		"radius_endpoint_one": "Radiusendpunkt",
 | 
			
		||||
		"radius_endpoint_other": "Radiusendpunkte"
 | 
			
		||||
	},
 | 
			
		||||
	"operator": {
 | 
			
		||||
		"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar",
 | 
			
		||||
		"delete_operator": "Betreiber löschen",
 | 
			
		||||
@@ -970,6 +981,27 @@
 | 
			
		||||
		"title": "Beschränkungen",
 | 
			
		||||
		"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": {
 | 
			
		||||
		"algorithm": "Algorithmus",
 | 
			
		||||
		"algorithm_other": "Algorithmen",
 | 
			
		||||
@@ -1091,6 +1123,7 @@
 | 
			
		||||
		"title": "Abonnenten"
 | 
			
		||||
	},
 | 
			
		||||
	"system": {
 | 
			
		||||
		"advanced": "Erweitert",
 | 
			
		||||
		"backend_logs": "Back-End-Protokolle",
 | 
			
		||||
		"configuration": "Aufbau",
 | 
			
		||||
		"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
 | 
			
		||||
 
 | 
			
		||||
@@ -269,6 +269,7 @@
 | 
			
		||||
		"map": "Map",
 | 
			
		||||
		"max": "Max",
 | 
			
		||||
		"min": "Min",
 | 
			
		||||
		"miscellaneous": "Miscellaneous",
 | 
			
		||||
		"mode": "Mode",
 | 
			
		||||
		"model": "Model",
 | 
			
		||||
		"modified": "Modified",
 | 
			
		||||
@@ -737,6 +738,7 @@
 | 
			
		||||
	"form": {
 | 
			
		||||
		"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-----\"",
 | 
			
		||||
		"invalid_alphanumeric_with_dash": "Accepted chars. are only alphanumeric (letters & numbers)",
 | 
			
		||||
		"invalid_cidr": "Invalid CIDR IPv4 address. Example: 192.168.0.1/12",
 | 
			
		||||
		"invalid_email": "Invalid Email",
 | 
			
		||||
		"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_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-----\"",
 | 
			
		||||
		"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_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",
 | 
			
		||||
		"new_email_to_notify": "New email to notify",
 | 
			
		||||
		"new_phone_to_notify": "New phone to notify",
 | 
			
		||||
@@ -905,6 +911,11 @@
 | 
			
		||||
		"one": "Notification",
 | 
			
		||||
		"other": "Notifications"
 | 
			
		||||
	},
 | 
			
		||||
	"openroaming": {
 | 
			
		||||
		"pool_strategy": "Pool Strategy",
 | 
			
		||||
		"radius_endpoint_one": "Radius Endpoint",
 | 
			
		||||
		"radius_endpoint_other": "Radius Endpoints"
 | 
			
		||||
	},
 | 
			
		||||
	"operator": {
 | 
			
		||||
		"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible",
 | 
			
		||||
		"delete_operator": "Delete Operator",
 | 
			
		||||
@@ -970,6 +981,27 @@
 | 
			
		||||
		"title": "Restrictions",
 | 
			
		||||
		"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": {
 | 
			
		||||
		"algorithm": "Algorithm",
 | 
			
		||||
		"algorithm_other": "Algorithms",
 | 
			
		||||
@@ -1091,6 +1123,7 @@
 | 
			
		||||
		"title": "Subscribers"
 | 
			
		||||
	},
 | 
			
		||||
	"system": {
 | 
			
		||||
		"advanced": "Advanced",
 | 
			
		||||
		"backend_logs": "Back-End Logs",
 | 
			
		||||
		"configuration": "Configuration",
 | 
			
		||||
		"could_not_retrieve": "Error: could not retrieve {{name}} system information",
 | 
			
		||||
 
 | 
			
		||||
@@ -269,6 +269,7 @@
 | 
			
		||||
		"map": "Mapa",
 | 
			
		||||
		"max": "Max",
 | 
			
		||||
		"min": "Min",
 | 
			
		||||
		"miscellaneous": "Diverso",
 | 
			
		||||
		"mode": "Modo",
 | 
			
		||||
		"model": "Modelo",
 | 
			
		||||
		"modified": "Modificado",
 | 
			
		||||
@@ -737,6 +738,7 @@
 | 
			
		||||
	"form": {
 | 
			
		||||
		"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-----\"",
 | 
			
		||||
		"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_email": "Email invá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_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-----\"",
 | 
			
		||||
		"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_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",
 | 
			
		||||
		"new_email_to_notify": "Nuevo correo electrónico para notificar",
 | 
			
		||||
		"new_phone_to_notify": "Nuevo teléfono para avisar",
 | 
			
		||||
@@ -905,6 +911,11 @@
 | 
			
		||||
		"one": "Notificación",
 | 
			
		||||
		"other": "Notificaciones"
 | 
			
		||||
	},
 | 
			
		||||
	"openroaming": {
 | 
			
		||||
		"pool_strategy": "Estrategia de piscina",
 | 
			
		||||
		"radius_endpoint_one": "Punto final del radio",
 | 
			
		||||
		"radius_endpoint_other": "Puntos finales de radio"
 | 
			
		||||
	},
 | 
			
		||||
	"operator": {
 | 
			
		||||
		"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.",
 | 
			
		||||
		"delete_operator": "Eliminar operador",
 | 
			
		||||
@@ -970,6 +981,27 @@
 | 
			
		||||
		"title": "Las restricciones",
 | 
			
		||||
		"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": {
 | 
			
		||||
		"algorithm": "Algoritmo",
 | 
			
		||||
		"algorithm_other": "Algoritmos",
 | 
			
		||||
@@ -1091,6 +1123,7 @@
 | 
			
		||||
		"title": "Suscriptores"
 | 
			
		||||
	},
 | 
			
		||||
	"system": {
 | 
			
		||||
		"advanced": "Avanzado",
 | 
			
		||||
		"backend_logs": "Registros de back-end",
 | 
			
		||||
		"configuration": "Configuración",
 | 
			
		||||
		"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
 | 
			
		||||
 
 | 
			
		||||
@@ -269,6 +269,7 @@
 | 
			
		||||
		"map": "Carte",
 | 
			
		||||
		"max": "Max",
 | 
			
		||||
		"min": "Min",
 | 
			
		||||
		"miscellaneous": "Divers",
 | 
			
		||||
		"mode": "Mode",
 | 
			
		||||
		"model": "Modèle",
 | 
			
		||||
		"modified": "Modifié",
 | 
			
		||||
@@ -737,6 +738,7 @@
 | 
			
		||||
	"form": {
 | 
			
		||||
		"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-----\"",
 | 
			
		||||
		"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_email": "Email Invalide",
 | 
			
		||||
		"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_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-----\"",
 | 
			
		||||
		"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_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",
 | 
			
		||||
		"new_email_to_notify": "Nouvel e-mail à notifier",
 | 
			
		||||
		"new_phone_to_notify": "Nouveau téléphone à notifier",
 | 
			
		||||
@@ -905,6 +911,11 @@
 | 
			
		||||
		"one": "Notification",
 | 
			
		||||
		"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": {
 | 
			
		||||
		"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible",
 | 
			
		||||
		"delete_operator": "Supprimer l'opérateur",
 | 
			
		||||
@@ -970,6 +981,27 @@
 | 
			
		||||
		"title": "Restrictions",
 | 
			
		||||
		"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": {
 | 
			
		||||
		"algorithm": "Algorithme",
 | 
			
		||||
		"algorithm_other": "Algorithmes",
 | 
			
		||||
@@ -1091,6 +1123,7 @@
 | 
			
		||||
		"title": "Les abonnés"
 | 
			
		||||
	},
 | 
			
		||||
	"system": {
 | 
			
		||||
		"advanced": "Avancée",
 | 
			
		||||
		"backend_logs": "Journaux principaux",
 | 
			
		||||
		"configuration": "Configuration",
 | 
			
		||||
		"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
 | 
			
		||||
 
 | 
			
		||||
@@ -269,6 +269,7 @@
 | 
			
		||||
		"map": "Mapa",
 | 
			
		||||
		"max": "máximo",
 | 
			
		||||
		"min": "minuto",
 | 
			
		||||
		"miscellaneous": "Diversos",
 | 
			
		||||
		"mode": "Modo",
 | 
			
		||||
		"model": "Modelo",
 | 
			
		||||
		"modified": "Modificado",
 | 
			
		||||
@@ -737,6 +738,7 @@
 | 
			
		||||
	"form": {
 | 
			
		||||
		"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-----\"",
 | 
			
		||||
		"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_email": "E-mail invá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_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-----\"",
 | 
			
		||||
		"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_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",
 | 
			
		||||
		"new_email_to_notify": "Novo e-mail para notificar",
 | 
			
		||||
		"new_phone_to_notify": "Novo telefone para notificar",
 | 
			
		||||
@@ -905,6 +911,11 @@
 | 
			
		||||
		"one": "Notificação",
 | 
			
		||||
		"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": {
 | 
			
		||||
		"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível",
 | 
			
		||||
		"delete_operator": "Excluir operador",
 | 
			
		||||
@@ -970,6 +981,27 @@
 | 
			
		||||
		"title": "RESTRIÇÕES",
 | 
			
		||||
		"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": {
 | 
			
		||||
		"algorithm": "Algoritmo",
 | 
			
		||||
		"algorithm_other": "Algoritmos",
 | 
			
		||||
@@ -1091,6 +1123,7 @@
 | 
			
		||||
		"title": "Inscritos"
 | 
			
		||||
	},
 | 
			
		||||
	"system": {
 | 
			
		||||
		"advanced": "Avançado",
 | 
			
		||||
		"backend_logs": "Registros de back-end",
 | 
			
		||||
		"configuration": "Configuração",
 | 
			
		||||
		"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								src/@tanstack.react-table.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								src/@tanstack.react-table.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -4,18 +4,22 @@ import '@tanstack/react-table';
 | 
			
		||||
 | 
			
		||||
declare module '@tanstack/table-core' {
 | 
			
		||||
  interface ColumnMeta<TData extends RowData, TValue> {
 | 
			
		||||
    ref?: React.MutableRefObject<HTMLTableCellElement | null>;
 | 
			
		||||
    customMinWidth?: string;
 | 
			
		||||
    anchored?: boolean;
 | 
			
		||||
    stopPropagation?: boolean;
 | 
			
		||||
    alwaysShow?: boolean;
 | 
			
		||||
    anchored?: boolean;
 | 
			
		||||
    hasPopover?: boolean;
 | 
			
		||||
    customMaxWidth?: string;
 | 
			
		||||
    customMinWidth?: string;
 | 
			
		||||
    customWidth?: string;
 | 
			
		||||
    isMonospace?: boolean;
 | 
			
		||||
    isCentered?: boolean;
 | 
			
		||||
    columnSelectorOptions?: {
 | 
			
		||||
      label?: string;
 | 
			
		||||
    };
 | 
			
		||||
    rowContentOptions?: {
 | 
			
		||||
      style?: React.CSSProperties;
 | 
			
		||||
    };
 | 
			
		||||
    headerOptions?: {
 | 
			
		||||
      tooltip?: string;
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,6 @@ export const DataGridCellRow = <TValue extends object>({
 | 
			
		||||
        backgroundColor: hoveredRowBg,
 | 
			
		||||
      }}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
      borderRight="1px solid gray"
 | 
			
		||||
    >
 | 
			
		||||
      {row.getVisibleCells().map((cell) => (
 | 
			
		||||
        <Td
 | 
			
		||||
@@ -55,6 +54,7 @@ export const DataGridCellRow = <TValue extends object>({
 | 
			
		||||
              : undefined
 | 
			
		||||
          }
 | 
			
		||||
          border="0.5px solid gray"
 | 
			
		||||
          style={cell.column.columnDef.meta?.rowContentOptions?.style}
 | 
			
		||||
        >
 | 
			
		||||
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
 | 
			
		||||
        </Td>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export type DataGridHeaderRowProps<TValue extends object> = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
 | 
			
		||||
  <Tr p={0} borderRight="1px solid gray">
 | 
			
		||||
  <Tr p={0}>
 | 
			
		||||
    {headerGroup.headers.map((header) => (
 | 
			
		||||
      <Th
 | 
			
		||||
        color="gray.400"
 | 
			
		||||
 
 | 
			
		||||
@@ -40,13 +40,16 @@ export type DataGridOptions<TValue extends object> = {
 | 
			
		||||
  onRowClick?: (row: TValue) => (() => void) | undefined;
 | 
			
		||||
  refetch?: () => void;
 | 
			
		||||
  showAsCard?: boolean;
 | 
			
		||||
  hideTablePreferences?: boolean;
 | 
			
		||||
  hideTableTitleRow?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DataGridProps<TValue extends object> = {
 | 
			
		||||
  innerTableKey?: string | number;
 | 
			
		||||
  controller: UseDataGridReturn;
 | 
			
		||||
  columns: DataGridColumn<TValue>[];
 | 
			
		||||
  header: {
 | 
			
		||||
    title: string;
 | 
			
		||||
    title: string | React.ReactNode;
 | 
			
		||||
    objectListed: string;
 | 
			
		||||
    leftContent?: React.ReactNode;
 | 
			
		||||
    addButton?: React.ReactNode;
 | 
			
		||||
@@ -58,6 +61,7 @@ export type DataGridProps<TValue extends object> = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DataGrid = <TValue extends object>({
 | 
			
		||||
  innerTableKey,
 | 
			
		||||
  controller,
 | 
			
		||||
  columns,
 | 
			
		||||
  header,
 | 
			
		||||
@@ -149,6 +153,20 @@ export const DataGrid = <TValue extends object>({
 | 
			
		||||
    ...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) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Center>
 | 
			
		||||
@@ -160,25 +178,29 @@ export const DataGrid = <TValue extends object>({
 | 
			
		||||
  return options.showAsCard ? (
 | 
			
		||||
    <Card>
 | 
			
		||||
      <CardHeader>
 | 
			
		||||
        <Heading size="md" my="auto" mr={2}>
 | 
			
		||||
          {header.title}
 | 
			
		||||
        </Heading>
 | 
			
		||||
        {typeof header.title === 'string' ? (
 | 
			
		||||
          <Heading size="md" my="auto" mr={2}>
 | 
			
		||||
            {header.title}
 | 
			
		||||
          </Heading>
 | 
			
		||||
        ) : (
 | 
			
		||||
          header.title
 | 
			
		||||
        )}
 | 
			
		||||
        {header.leftContent}
 | 
			
		||||
        <Spacer />
 | 
			
		||||
        <HStack spacing={2}>
 | 
			
		||||
          {header.otherButtons}
 | 
			
		||||
          {header.addButton}
 | 
			
		||||
          {
 | 
			
		||||
          {options.hideTablePreferences ? null : (
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            <TableSettingsModal<TValue> controller={controller} columns={columns} />
 | 
			
		||||
          }
 | 
			
		||||
          )}
 | 
			
		||||
          {options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
 | 
			
		||||
        </HStack>
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardBody display="flex" flexDirection="column">
 | 
			
		||||
        <LoadingOverlay isLoading={isLoading}>
 | 
			
		||||
          <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>
 | 
			
		||||
                {table.getHeaderGroups().map((headerGroup) => (
 | 
			
		||||
                  <DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
 | 
			
		||||
@@ -206,7 +228,7 @@ export const DataGrid = <TValue extends object>({
 | 
			
		||||
    </Card>
 | 
			
		||||
  ) : (
 | 
			
		||||
    <Box w="100%">
 | 
			
		||||
      <Flex mb={2}>
 | 
			
		||||
      <Flex mb={2} hidden={options.hideTableTitleRow}>
 | 
			
		||||
        <Heading size="md" my="auto" mr={2}>
 | 
			
		||||
          {header.title}
 | 
			
		||||
        </Heading>
 | 
			
		||||
@@ -215,16 +237,16 @@ export const DataGrid = <TValue extends object>({
 | 
			
		||||
        <HStack spacing={2}>
 | 
			
		||||
          {header.otherButtons}
 | 
			
		||||
          {header.addButton}
 | 
			
		||||
          {
 | 
			
		||||
          {options.hideTablePreferences ? null : (
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            <TableSettingsModal<TValue> controller={controller} columns={columns} />
 | 
			
		||||
          }
 | 
			
		||||
          )}
 | 
			
		||||
          {options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
 | 
			
		||||
        </HStack>
 | 
			
		||||
      </Flex>
 | 
			
		||||
      <LoadingOverlay isLoading={isLoading}>
 | 
			
		||||
        <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>
 | 
			
		||||
              {table.getHeaderGroups().map((headerGroup) => (
 | 
			
		||||
                <DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
 | 
			
		||||
 
 | 
			
		||||
@@ -104,18 +104,43 @@ const GlobalSearchBar = () => {
 | 
			
		||||
          .then(() => callback([]));
 | 
			
		||||
      }
 | 
			
		||||
      if (v.match('^[a-fA-F0-9-*]+$')) {
 | 
			
		||||
        let result: { label: string; value: string; type: 'serial' }[] = [];
 | 
			
		||||
        let tryAgain = true;
 | 
			
		||||
 | 
			
		||||
        await store
 | 
			
		||||
          .searchSerialNumber(v)
 | 
			
		||||
          .then((res) => {
 | 
			
		||||
            callback(
 | 
			
		||||
              res.map((r) => ({
 | 
			
		||||
            result = 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,
 | 
			
		||||
                value: r,
 | 
			
		||||
                type: 'serial',
 | 
			
		||||
              })),
 | 
			
		||||
            );
 | 
			
		||||
          })
 | 
			
		||||
          .catch(() => []);
 | 
			
		||||
              }));
 | 
			
		||||
              tryAgain = false;
 | 
			
		||||
            })
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
              result = [];
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        callback(result);
 | 
			
		||||
      }
 | 
			
		||||
      return callback([]);
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ export type ConfigureModalProps = {
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps) => {
 | 
			
		||||
const _ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const toast = useToast();
 | 
			
		||||
  const configure = useConfigureDevice({ serialNumber });
 | 
			
		||||
@@ -45,6 +45,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
 | 
			
		||||
  const onImportConfiguration = () => {
 | 
			
		||||
    setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const isValid = React.useMemo(() => {
 | 
			
		||||
    try {
 | 
			
		||||
      JSON.parse(newConfig);
 | 
			
		||||
@@ -71,9 +72,19 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
 | 
			
		||||
          modalProps.onClose();
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {}
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // do nothing
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (modalProps.isOpen) {
 | 
			
		||||
      getDevice.refetch();
 | 
			
		||||
    } else {
 | 
			
		||||
      setNewConfig('');
 | 
			
		||||
    }
 | 
			
		||||
  }, [modalProps.isOpen]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      {...modalProps}
 | 
			
		||||
@@ -124,3 +135,5 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ConfigureModal = React.memo(_ConfigureModal);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,5 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Heading,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Spacer,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tbody,
 | 
			
		||||
  Td,
 | 
			
		||||
  Th,
 | 
			
		||||
  Thead,
 | 
			
		||||
  Tr,
 | 
			
		||||
  useColorMode,
 | 
			
		||||
} from '@chakra-ui/react';
 | 
			
		||||
import { Box, Button, Center, Heading, IconButton, Spacer, useColorMode } from '@chakra-ui/react';
 | 
			
		||||
import { JsonViewer } from '@textea/json-viewer';
 | 
			
		||||
import { ArrowLeft } from '@phosphor-icons/react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
@@ -20,21 +7,124 @@ import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { Card } from 'components/Containers/Card';
 | 
			
		||||
import { CardBody } from 'components/Containers/Card/CardBody';
 | 
			
		||||
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 {
 | 
			
		||||
  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 { colorMode } = useColorMode();
 | 
			
		||||
  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 (
 | 
			
		||||
    <Card variant="widget">
 | 
			
		||||
    <Card>
 | 
			
		||||
      <CardHeader display="flex">
 | 
			
		||||
        <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>
 | 
			
		||||
        <Spacer />
 | 
			
		||||
        {ies && (
 | 
			
		||||
@@ -49,52 +139,43 @@ const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => {
 | 
			
		||||
        )}
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardBody>
 | 
			
		||||
        <Box h="400px" w="100%" overflowY="auto" overflowX="auto" px={0}>
 | 
			
		||||
          {ies ? (
 | 
			
		||||
            <Box w="800px">
 | 
			
		||||
              {ies.map(({ content, name, type }) => (
 | 
			
		||||
                <Box key={uuid()} my={2}>
 | 
			
		||||
                  <Heading size="sm" mb={2} textDecor="underline">
 | 
			
		||||
                    {name} ({type})
 | 
			
		||||
                  </Heading>
 | 
			
		||||
                  <JsonViewer
 | 
			
		||||
                    rootName={false}
 | 
			
		||||
                    displayDataTypes={false}
 | 
			
		||||
                    enableClipboard
 | 
			
		||||
                    theme={colorMode === 'light' ? undefined : 'dark'}
 | 
			
		||||
                    value={content as object}
 | 
			
		||||
                    style={{ background: 'unset', display: 'unset' }}
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
              ))}
 | 
			
		||||
            </Box>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Table variant="simple" px={0}>
 | 
			
		||||
              <Thead>
 | 
			
		||||
                <Tr>
 | 
			
		||||
                  <Th>SSID</Th>
 | 
			
		||||
                  <Th width="110px" isNumeric>
 | 
			
		||||
                    {t('commands.signal')}
 | 
			
		||||
                  </Th>
 | 
			
		||||
                  <Th w="10px">IEs</Th>
 | 
			
		||||
                </Tr>
 | 
			
		||||
              </Thead>
 | 
			
		||||
              <Tbody>
 | 
			
		||||
                {devices.map((dev) => (
 | 
			
		||||
                  <Tr key={uuid()}>
 | 
			
		||||
                    <Td>{dev.ssid}</Td>
 | 
			
		||||
                    <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>
 | 
			
		||||
        {ies ? (
 | 
			
		||||
          <Box w="800px">
 | 
			
		||||
            {ies.map(({ content, name, type }) => (
 | 
			
		||||
              <Box key={uuid()} my={2}>
 | 
			
		||||
                <Heading size="sm" mb={2} textDecor="underline">
 | 
			
		||||
                  {name} ({type})
 | 
			
		||||
                </Heading>
 | 
			
		||||
                <JsonViewer
 | 
			
		||||
                  rootName={false}
 | 
			
		||||
                  displayDataTypes={false}
 | 
			
		||||
                  enableClipboard
 | 
			
		||||
                  theme={colorMode === 'light' ? undefined : 'dark'}
 | 
			
		||||
                  value={content as object}
 | 
			
		||||
                  style={{ background: 'unset', display: 'unset' }}
 | 
			
		||||
                />
 | 
			
		||||
              </Box>
 | 
			
		||||
            ))}
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <DataGrid<DeviceScanResult>
 | 
			
		||||
            controller={tableController}
 | 
			
		||||
            header={{
 | 
			
		||||
              title: '',
 | 
			
		||||
              objectListed: t('devices.title'),
 | 
			
		||||
            }}
 | 
			
		||||
            columns={columns}
 | 
			
		||||
            data={devices}
 | 
			
		||||
            options={{
 | 
			
		||||
              count: devices.length,
 | 
			
		||||
              onRowClick: (device) => () => setIes(device.ies ?? []),
 | 
			
		||||
              hideTablePreferences: true,
 | 
			
		||||
              isHidingControls: true,
 | 
			
		||||
              minimumHeight: '0px',
 | 
			
		||||
              hideTableTitleRow: true,
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </CardBody>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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 { v4 as uuid } from 'uuid';
 | 
			
		||||
import ResultCard from './ResultCard';
 | 
			
		||||
@@ -11,7 +11,7 @@ interface Props {
 | 
			
		||||
  setCsvData: (data: DeviceScanResult[]) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
 | 
			
		||||
const WifiScanResultDisplay = ({ results, setCsvData }: Props) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const scanResults = useMemo(() => {
 | 
			
		||||
@@ -54,18 +54,18 @@ const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {results.errorCode === 1 && (
 | 
			
		||||
        <Heading size="sm">
 | 
			
		||||
        <Heading size="md">
 | 
			
		||||
          <Alert colorScheme="red">{t('commands.wifiscan_error_1')}</Alert>
 | 
			
		||||
        </Heading>
 | 
			
		||||
      )}
 | 
			
		||||
      <Heading size="sm">
 | 
			
		||||
      <Heading size="md" mb={2}>
 | 
			
		||||
        {t('commands.execution_time')}: {Math.floor(results.executionTime / 1000)}s
 | 
			
		||||
      </Heading>
 | 
			
		||||
      <SimpleGrid minChildWidth="360px" spacing={2}>
 | 
			
		||||
      <VStack spacing={4} align="stretch">
 | 
			
		||||
        {scanResults?.scanList.map((channel) => (
 | 
			
		||||
          <ResultCard key={uuid()} channelInfo={channel} />
 | 
			
		||||
        ))}
 | 
			
		||||
      </SimpleGrid>
 | 
			
		||||
      </VStack>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -187,6 +187,8 @@ export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) =
 | 
			
		||||
  return useMutation(configureDevice(serialNumber), {
 | 
			
		||||
    onSuccess: () => {
 | 
			
		||||
      queryClient.invalidateQueries(['commands', serialNumber]);
 | 
			
		||||
      queryClient.invalidateQueries(['device', serialNumber]);
 | 
			
		||||
      queryClient.invalidateQueries(['devices']);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
@@ -248,27 +250,14 @@ const startScript = (data: { serialNumber: string; timeout?: number; [k: string]
 | 
			
		||||
    })
 | 
			
		||||
    .then((response: { data: DeviceCommandHistory }) => response.data);
 | 
			
		||||
export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const toast = useToast();
 | 
			
		||||
  const queryClient = useQueryClient();
 | 
			
		||||
 | 
			
		||||
  return useMutation(startScript, {
 | 
			
		||||
    onSuccess: () => {
 | 
			
		||||
      queryClient.invalidateQueries(['commands', serialNumber]);
 | 
			
		||||
    },
 | 
			
		||||
    onError: (e) => {
 | 
			
		||||
    onError: () => {
 | 
			
		||||
      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',
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										173
									
								
								src/hooks/Network/Simulations.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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']);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										80
									
								
								src/hooks/useNotification.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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>;
 | 
			
		||||
@@ -123,6 +123,7 @@ export const Navbar = ({
 | 
			
		||||
        top="15px"
 | 
			
		||||
        border={scrolled ? '0.5px solid' : undefined}
 | 
			
		||||
        w={isCompact ? '100%' : 'calc(100% - 254px)'}
 | 
			
		||||
        zIndex={10}
 | 
			
		||||
      >
 | 
			
		||||
        <Flex
 | 
			
		||||
          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 const isApiError = (e: unknown): e is AxiosError =>
 | 
			
		||||
  isAxiosError(e) && (e as AxiosError).response?.data?.ErrorDescription !== undefined;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { Note } from './Note';
 | 
			
		||||
 | 
			
		||||
export interface GatewayDevice {
 | 
			
		||||
  UUID: number;
 | 
			
		||||
  certificateExpiryDate: number;
 | 
			
		||||
  compatible: string;
 | 
			
		||||
  configuration: unknown;
 | 
			
		||||
  createdTimestamp: number;
 | 
			
		||||
@@ -16,6 +17,7 @@ export interface GatewayDevice {
 | 
			
		||||
  lastConfigurationChange: number;
 | 
			
		||||
  lastConfigurationDownload: number;
 | 
			
		||||
  lastFWUpdate: number;
 | 
			
		||||
  lastRecordedContact: number;
 | 
			
		||||
  locale: string;
 | 
			
		||||
  location: string;
 | 
			
		||||
  macAddress: string;
 | 
			
		||||
@@ -112,12 +114,16 @@ interface BssidResult {
 | 
			
		||||
  bssid: string;
 | 
			
		||||
  capability: number;
 | 
			
		||||
  channel: number;
 | 
			
		||||
  /** Channel Utilization percentage (ex.: 28 -> 28% channel utilization) */
 | 
			
		||||
  ch_util?: number;
 | 
			
		||||
  frequency: number;
 | 
			
		||||
  ht_oper: string;
 | 
			
		||||
  ies: { content: unknown; name: string; type: number }[];
 | 
			
		||||
  last_seen: number;
 | 
			
		||||
  ssid: string;
 | 
			
		||||
  signal: number;
 | 
			
		||||
  /** Station count */
 | 
			
		||||
  sta_count?: number;
 | 
			
		||||
  tsf: number;
 | 
			
		||||
  meshid?: string;
 | 
			
		||||
  vht_oper: string;
 | 
			
		||||
@@ -142,20 +148,8 @@ export interface WifiScanResult {
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DeviceScanResult {
 | 
			
		||||
  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 type DeviceScanResult = BssidResult;
 | 
			
		||||
 | 
			
		||||
export interface ScanChannel {
 | 
			
		||||
  channel: number;
 | 
			
		||||
  devices: DeviceScanResult[];
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										93
									
								
								src/pages/AdvancedSystemPage/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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;
 | 
			
		||||
@@ -11,7 +11,6 @@ import { compactDate } from 'helpers/dateFormatting';
 | 
			
		||||
import { useGetDevice } from 'hooks/Network/Devices';
 | 
			
		||||
import { useGetProvUi } from 'hooks/Network/Endpoints';
 | 
			
		||||
import { useGetTag } from 'hooks/Network/Inventory';
 | 
			
		||||
import { DeviceConfiguration } from 'models/Device';
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  serialNumber: string;
 | 
			
		||||
@@ -60,7 +59,7 @@ const DeviceDetails = ({ serialNumber }: Props) => {
 | 
			
		||||
        <Heading size="md">{t('common.details')}</Heading>
 | 
			
		||||
        <Spacer />
 | 
			
		||||
        <ViewCapabilitiesModal serialNumber={serialNumber} />
 | 
			
		||||
        <ViewConfigurationModal configuration={getDevice.data?.configuration as DeviceConfiguration} />
 | 
			
		||||
        <ViewConfigurationModal serialNumber={serialNumber} />
 | 
			
		||||
      </CardHeader>
 | 
			
		||||
      <CardBody display="block">
 | 
			
		||||
        <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 COUNTRY_LIST from 'constants/countryList';
 | 
			
		||||
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 { useGetDeviceLastStats } from 'hooks/Network/Statistics';
 | 
			
		||||
 | 
			
		||||
@@ -157,7 +157,11 @@ const DeviceSummary = ({ serialNumber }: Props) => {
 | 
			
		||||
              <Heading size="sm">{t('analytics.last_contact')}:</Heading>
 | 
			
		||||
            </GridItem>
 | 
			
		||||
            <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 colSpan={1} alignContent="center" alignItems="center">
 | 
			
		||||
              <Heading size="sm">{t('analytics.memory')}:</Heading>
 | 
			
		||||
@@ -167,8 +171,10 @@ const DeviceSummary = ({ serialNumber }: Props) => {
 | 
			
		||||
              <Heading size="sm">{t('devices.certificate_expires_in')}:</Heading>
 | 
			
		||||
            </GridItem>
 | 
			
		||||
            <GridItem colSpan={1}>
 | 
			
		||||
              {getStatus.data?.certificateExpiryDate && (
 | 
			
		||||
                <FormattedDate date={getStatus.data?.certificateExpiryDate} hidePrefix />
 | 
			
		||||
              {getDevice.data?.certificateExpiryDate ? (
 | 
			
		||||
                <FormattedDate date={getDevice.data?.certificateExpiryDate} hidePrefix />
 | 
			
		||||
              ) : (
 | 
			
		||||
                '-'
 | 
			
		||||
              )}
 | 
			
		||||
            </GridItem>
 | 
			
		||||
            <GridItem colSpan={1} alignContent="center" alignItems="center">
 | 
			
		||||
@@ -176,7 +182,7 @@ const DeviceSummary = ({ serialNumber }: Props) => {
 | 
			
		||||
            </GridItem>
 | 
			
		||||
            <GridItem colSpan={1}>
 | 
			
		||||
              {getStatus.data?.connectReason && getStatus.data?.connectReason.length > 0
 | 
			
		||||
                ? getStatus.data?.connectReason
 | 
			
		||||
                ? uppercaseFirstLetter(getStatus.data.connectReason)
 | 
			
		||||
                : '-'}
 | 
			
		||||
            </GridItem>
 | 
			
		||||
          </Grid>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,9 @@ import {
 | 
			
		||||
  AccordionPanel,
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Center,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Spinner,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  useClipboard,
 | 
			
		||||
  useColorMode,
 | 
			
		||||
@@ -17,19 +19,26 @@ import { JsonViewer } from '@textea/json-viewer';
 | 
			
		||||
import { Barcode } from '@phosphor-icons/react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
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 getDevice = useGetDevice({ serialNumber });
 | 
			
		||||
  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();
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (configuration) {
 | 
			
		||||
      setValue(JSON.stringify(configuration, null, 2));
 | 
			
		||||
    if (getDevice.data) {
 | 
			
		||||
      setValue(JSON.stringify(getDevice.data.configuration, null, 2));
 | 
			
		||||
    }
 | 
			
		||||
  }, [configuration]);
 | 
			
		||||
  }, [getDevice.data?.configuration]);
 | 
			
		||||
 | 
			
		||||
  const handleOpenClick = () => {
 | 
			
		||||
    getDevice.refetch();
 | 
			
		||||
    onOpen();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
@@ -37,7 +46,7 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
 | 
			
		||||
        <IconButton
 | 
			
		||||
          aria-label={t('configurations.one')}
 | 
			
		||||
          icon={<Barcode size={20} />}
 | 
			
		||||
          onClick={onOpen}
 | 
			
		||||
          onClick={handleOpenClick}
 | 
			
		||||
          colorScheme="purple"
 | 
			
		||||
        />
 | 
			
		||||
      </Tooltip>
 | 
			
		||||
@@ -45,14 +54,17 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
 | 
			
		||||
        isOpen={isOpen}
 | 
			
		||||
        title={t('configurations.one')}
 | 
			
		||||
        topRightButtons={
 | 
			
		||||
          <Button onClick={onCopy} size="md" colorScheme="teal">
 | 
			
		||||
            {hasCopied ? `${t('common.copied')}!` : t('common.copy')}
 | 
			
		||||
          </Button>
 | 
			
		||||
          <>
 | 
			
		||||
            <Button onClick={onCopy} size="md" colorScheme="teal">
 | 
			
		||||
              {hasCopied ? `${t('common.copied')}!` : t('common.copy')}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <RefreshButton onClick={getDevice.refetch} isFetching={getDevice.isFetching} />
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
        onClose={onClose}
 | 
			
		||||
      >
 | 
			
		||||
        <Box display="inline-block" w="100%">
 | 
			
		||||
          {configuration && (
 | 
			
		||||
          {getDevice.data && !getDevice.isFetching ? (
 | 
			
		||||
            <Box maxH="calc(100vh - 250px)" minH="300px" overflowY="auto">
 | 
			
		||||
              <Accordion defaultIndex={0} allowToggle>
 | 
			
		||||
                <AccordionItem>
 | 
			
		||||
@@ -71,7 +83,7 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
 | 
			
		||||
                      enableClipboard={false}
 | 
			
		||||
                      theme={colorMode === 'light' ? undefined : 'dark'}
 | 
			
		||||
                      defaultInspectDepth={1}
 | 
			
		||||
                      value={configuration as object}
 | 
			
		||||
                      value={getDevice.data.configuration as object}
 | 
			
		||||
                      style={{ background: 'unset', display: 'unset' }}
 | 
			
		||||
                    />
 | 
			
		||||
                  </AccordionPanel>
 | 
			
		||||
@@ -86,11 +98,15 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
 | 
			
		||||
                    </AccordionButton>
 | 
			
		||||
                  </h2>
 | 
			
		||||
                  <AccordionPanel pb={4} overflowX="auto">
 | 
			
		||||
                    <pre>{JSON.stringify(configuration, null, 2)}</pre>
 | 
			
		||||
                    <pre>{JSON.stringify(getDevice.data.configuration, null, 2)}</pre>
 | 
			
		||||
                  </AccordionPanel>
 | 
			
		||||
                </AccordionItem>
 | 
			
		||||
              </Accordion>
 | 
			
		||||
            </Box>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Center my={12}>
 | 
			
		||||
              <Spinner size="xl" />
 | 
			
		||||
            </Center>
 | 
			
		||||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
      </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 DataCell from 'components/TableCells/DataCell';
 | 
			
		||||
import { Column } from 'models/Table';
 | 
			
		||||
import IpCell from './IpCell';
 | 
			
		||||
 | 
			
		||||
export type ParsedAssociation = {
 | 
			
		||||
  radio?: ParsedRadio;
 | 
			
		||||
@@ -35,7 +36,7 @@ type Props = {
 | 
			
		||||
  isSingle?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => {
 | 
			
		||||
const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
 | 
			
		||||
 | 
			
		||||
@@ -50,6 +51,10 @@ const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => {
 | 
			
		||||
  );
 | 
			
		||||
  const dataCell = React.useCallback((v: number) => <DataCell bytes={v} />, []);
 | 
			
		||||
  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(
 | 
			
		||||
    (): Column<ParsedAssociation>[] => [
 | 
			
		||||
@@ -72,6 +77,13 @@ const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => {
 | 
			
		||||
        isMonospace: true,
 | 
			
		||||
        alwaysShow: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: 'ips',
 | 
			
		||||
        Header: 'IPs',
 | 
			
		||||
        Footer: '',
 | 
			
		||||
        Cell: (v) => ipCell(v.cell.row.original),
 | 
			
		||||
        disableSortBy: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: 'vendor',
 | 
			
		||||
        Header: t('controller.wifi.vendor'),
 | 
			
		||||
@@ -195,4 +207,4 @@ const WifiAnalysisAssocationsTable = ({ data, ouis, isSingle }: Props) => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default WifiAnalysisAssocationsTable;
 | 
			
		||||
export default WifiAnalysisAssociationsTable;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										90
									
								
								src/pages/Device/WifiAnalysis/IpCell.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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;
 | 
			
		||||
@@ -162,7 +162,7 @@ const WifiAnalysisCard = ({ serialNumber }: Props) => {
 | 
			
		||||
            <SliderTrack>
 | 
			
		||||
              <SliderFilledTrack />
 | 
			
		||||
            </SliderTrack>
 | 
			
		||||
            <SliderThumb />
 | 
			
		||||
            <SliderThumb zIndex={0} />
 | 
			
		||||
          </Slider>
 | 
			
		||||
        )}
 | 
			
		||||
        <Box />
 | 
			
		||||
 
 | 
			
		||||
@@ -694,7 +694,7 @@ const DeviceListCard = () => {
 | 
			
		||||
      <DataGrid<DeviceWithStatus>
 | 
			
		||||
        controller={tableController}
 | 
			
		||||
        header={{
 | 
			
		||||
          title: `${getCount.data?.count} ${t('devices.title')}`,
 | 
			
		||||
          title: `${getCount.data?.count ?? 0} ${t('devices.title')}`,
 | 
			
		||||
          objectListed: t('devices.title'),
 | 
			
		||||
          leftContent: <GlobalSearchBar />,
 | 
			
		||||
          otherButtons: (
 | 
			
		||||
 
 | 
			
		||||
@@ -138,6 +138,7 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (firmware) {
 | 
			
		||||
      copy.setValue(firmware?.uri ?? '');
 | 
			
		||||
      setNewDescription(firmware?.description);
 | 
			
		||||
    }
 | 
			
		||||
  }, [firmware]);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import React from 'react';
 | 
			
		||||
import { Barcode, FloppyDisk, Info, ListBullets, TerminalWindow, UsersThree, WifiHigh } from '@phosphor-icons/react';
 | 
			
		||||
import { Route } from 'models/Routes';
 | 
			
		||||
 | 
			
		||||
const AdvancedSystemPage = React.lazy(() => import('pages/AdvancedSystemPage'));
 | 
			
		||||
const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations'));
 | 
			
		||||
const DefaultFirmwarePage = React.lazy(() => import('pages/DefaultFirmware'));
 | 
			
		||||
const DevicePage = React.lazy(() => import('pages/Device'));
 | 
			
		||||
@@ -178,6 +179,13 @@ const routes: Route[] = [
 | 
			
		||||
    name: 'system.title',
 | 
			
		||||
    icon: () => <Info size={28} weight="bold" />,
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        id: 'system-advanced',
 | 
			
		||||
        authorized: ['root', 'partner', 'admin', 'csr', 'system'],
 | 
			
		||||
        path: '/systemAdvanced',
 | 
			
		||||
        name: 'system.advanced',
 | 
			
		||||
        component: AdvancedSystemPage,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: 'system-configuration',
 | 
			
		||||
        authorized: ['root', 'partner', 'admin', 'csr', 'system'],
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user