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