mirror of
				https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
				synced 2025-10-30 18:27:53 +00:00 
			
		
		
		
	Compare commits
	
		
			38 Commits
		
	
	
		
			v2.6.0
			...
			v2.7.0-RC2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c158f0aef8 | ||
|   | 4e5c6a9426 | ||
|   | 7ad184cb48 | ||
|   | 41a7d5d0a8 | ||
|   | 78c48e004c | ||
|   | 7106d61881 | ||
|   | 8ead4c4708 | ||
|   | 52ca7d3503 | ||
|   | 7d504da0a8 | ||
|   | c6dee2252b | ||
|   | 680c4a9ec4 | ||
|   | 3887d57fa4 | ||
|   | d733daed9d | ||
|   | de8651ab52 | ||
|   | 0ce641d10b | ||
|   | 316224b424 | ||
|   | cf9bbce284 | ||
|   | 6eae6c046e | ||
|   | 837a430228 | ||
|   | 71431f8fb5 | ||
|   | 0c7cd1f299 | ||
|   | 674682e919 | ||
|   | a5ca8115af | ||
|   | d4338fce42 | ||
|   | 14e8135f81 | ||
|   | e925f07505 | ||
|   | b792b51bd0 | ||
|   | fb64813b2a | ||
|   | b16e0e33ab | ||
|   | 818921e4a2 | ||
|   | 6c437459ca | ||
|   | b276901874 | ||
|   | 53a3de1ebc | ||
|   | 2d35747e75 | ||
|   | 71feebea6d | ||
|   | c8c75e7a70 | ||
|   | 7b2263e9a5 | ||
|   | 9cd216bbba | 
							
								
								
									
										8
									
								
								.github/workflows/cleanup.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/cleanup.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,4 +17,10 @@ jobs: | ||||
|     steps: | ||||
|       - run: | | ||||
|           export PR_BRANCH_TAG=$(echo ${GITHUB_HEAD_REF#refs/heads/} | tr '/' '-') | ||||
|           curl -uucentral:${{ secrets.DOCKER_REGISTRY_PASSWORD }} -X DELETE "https://tip.jfrog.io/artifactory/tip-wlan-cloud-ucentral/owgw-ui/$PR_BRANCH_TAG" | ||||
|  | ||||
|           if [[ ! $PR_BRANCH_TAG =~ (main|master|release-*) ]]; then | ||||
|             echo "PR branch is $PR_BRANCH_TAG, deleting Docker image" | ||||
|             curl -s -uucentral:${{ secrets.DOCKER_REGISTRY_PASSWORD }} -X DELETE "https://tip.jfrog.io/artifactory/tip-wlan-cloud-ucentral/owgw-ui/$PR_BRANCH_TAG" | ||||
|           else | ||||
|             echo "PR branch is $PR_BRANCH_TAG, not deleting Docker image" | ||||
|           fi | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| FROM node:14-alpine3.11 AS build | ||||
| FROM node:18.7.0-alpine3.15 AS build | ||||
|  | ||||
| COPY package.json package-lock.json / | ||||
|  | ||||
| @@ -8,7 +8,7 @@ COPY . . | ||||
|  | ||||
| RUN npm run build | ||||
|  | ||||
| FROM nginx:1.20.1-alpine AS runtime | ||||
| FROM nginx:1.22.0-alpine AS runtime | ||||
|  | ||||
| COPY --from=build /build/ /usr/share/nginx/html/ | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ fullnameOverride: "" | ||||
| images: | ||||
|   owgwui: | ||||
|     repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui | ||||
|     tag: main | ||||
|     tag: v2.7.0-RC2 | ||||
|     pullPolicy: Always | ||||
|  | ||||
| services: | ||||
|   | ||||
							
								
								
									
										347
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										347
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,12 +1,12 @@ | ||||
| { | ||||
|   "name": "ucentral-client", | ||||
|   "version": "2.6.29", | ||||
|   "version": "2.7.0(8)", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "ucentral-client", | ||||
|       "version": "2.6.29", | ||||
|       "version": "2.7.0(8)", | ||||
|       "dependencies": { | ||||
|         "@coreui/coreui": "^3.4.0", | ||||
|         "@coreui/icons": "^2.0.1", | ||||
| @@ -2069,6 +2069,64 @@ | ||||
|       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@jridgewell/gen-mapping": { | ||||
|       "version": "0.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", | ||||
|       "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@jridgewell/set-array": "^1.0.1", | ||||
|         "@jridgewell/sourcemap-codec": "^1.4.10", | ||||
|         "@jridgewell/trace-mapping": "^0.3.9" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=6.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@jridgewell/resolve-uri": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", | ||||
|       "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", | ||||
|       "dev": true, | ||||
|       "engines": { | ||||
|         "node": ">=6.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@jridgewell/set-array": { | ||||
|       "version": "1.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", | ||||
|       "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", | ||||
|       "dev": true, | ||||
|       "engines": { | ||||
|         "node": ">=6.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@jridgewell/source-map": { | ||||
|       "version": "0.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", | ||||
|       "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@jridgewell/gen-mapping": "^0.3.0", | ||||
|         "@jridgewell/trace-mapping": "^0.3.9" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@jridgewell/sourcemap-codec": { | ||||
|       "version": "1.4.14", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", | ||||
|       "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@jridgewell/trace-mapping": { | ||||
|       "version": "0.3.14", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", | ||||
|       "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@jridgewell/resolve-uri": "^3.0.3", | ||||
|         "@jridgewell/sourcemap-codec": "^1.4.10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@nodelib/fs.scandir": { | ||||
|       "version": "2.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||||
| @@ -7320,60 +7378,6 @@ | ||||
|         "node": ">=12" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/html-minifier-terser/node_modules/acorn": { | ||||
|       "version": "8.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", | ||||
|       "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", | ||||
|       "dev": true, | ||||
|       "optional": true, | ||||
|       "peer": true, | ||||
|       "bin": { | ||||
|         "acorn": "bin/acorn" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/html-minifier-terser/node_modules/source-map": { | ||||
|       "version": "0.7.3", | ||||
|       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", | ||||
|       "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", | ||||
|       "dev": true, | ||||
|       "engines": { | ||||
|         "node": ">= 8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/html-minifier-terser/node_modules/terser": { | ||||
|       "version": "5.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", | ||||
|       "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "commander": "^2.20.0", | ||||
|         "source-map": "~0.7.2", | ||||
|         "source-map-support": "~0.5.20" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "terser": "bin/terser" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "acorn": "^8.5.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "acorn": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": { | ||||
|       "version": "2.20.3", | ||||
|       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", | ||||
|       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/html-parse-stringify": { | ||||
|       "version": "3.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", | ||||
| @@ -9201,9 +9205,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/moment": { | ||||
|       "version": "2.29.3", | ||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", | ||||
|       "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", | ||||
|       "version": "2.29.4", | ||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", | ||||
|       "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", | ||||
|       "engines": { | ||||
|         "node": "*" | ||||
|       } | ||||
| @@ -13073,6 +13077,24 @@ | ||||
|         "node": ">=6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/terser": { | ||||
|       "version": "5.14.2", | ||||
|       "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", | ||||
|       "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "@jridgewell/source-map": "^0.3.2", | ||||
|         "acorn": "^8.5.0", | ||||
|         "commander": "^2.20.0", | ||||
|         "source-map-support": "~0.5.20" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "terser": "bin/terser" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/terser-webpack-plugin": { | ||||
|       "version": "5.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz", | ||||
| @@ -13107,26 +13129,6 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/terser-webpack-plugin/node_modules/acorn": { | ||||
|       "version": "8.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", | ||||
|       "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", | ||||
|       "dev": true, | ||||
|       "optional": true, | ||||
|       "peer": true, | ||||
|       "bin": { | ||||
|         "acorn": "bin/acorn" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/terser-webpack-plugin/node_modules/commander": { | ||||
|       "version": "2.20.3", | ||||
|       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", | ||||
|       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/terser-webpack-plugin/node_modules/has-flag": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", | ||||
| @@ -13192,39 +13194,23 @@ | ||||
|         "url": "https://github.com/chalk/supports-color?sponsor=1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/terser-webpack-plugin/node_modules/terser": { | ||||
|       "version": "5.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", | ||||
|       "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", | ||||
|     "node_modules/terser/node_modules/acorn": { | ||||
|       "version": "8.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", | ||||
|       "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "commander": "^2.20.0", | ||||
|         "source-map": "~0.7.2", | ||||
|         "source-map-support": "~0.5.20" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "terser": "bin/terser" | ||||
|         "acorn": "bin/acorn" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "acorn": "^8.5.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "acorn": { | ||||
|           "optional": true | ||||
|         } | ||||
|         "node": ">=0.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/terser-webpack-plugin/node_modules/terser/node_modules/source-map": { | ||||
|       "version": "0.7.3", | ||||
|       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", | ||||
|       "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", | ||||
|       "dev": true, | ||||
|       "engines": { | ||||
|         "node": ">= 8" | ||||
|       } | ||||
|     "node_modules/terser/node_modules/commander": { | ||||
|       "version": "2.20.3", | ||||
|       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", | ||||
|       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/text-table": { | ||||
|       "version": "0.2.0", | ||||
| @@ -16414,6 +16400,55 @@ | ||||
|       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@jridgewell/gen-mapping": { | ||||
|       "version": "0.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", | ||||
|       "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@jridgewell/set-array": "^1.0.1", | ||||
|         "@jridgewell/sourcemap-codec": "^1.4.10", | ||||
|         "@jridgewell/trace-mapping": "^0.3.9" | ||||
|       } | ||||
|     }, | ||||
|     "@jridgewell/resolve-uri": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", | ||||
|       "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@jridgewell/set-array": { | ||||
|       "version": "1.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", | ||||
|       "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@jridgewell/source-map": { | ||||
|       "version": "0.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", | ||||
|       "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@jridgewell/gen-mapping": "^0.3.0", | ||||
|         "@jridgewell/trace-mapping": "^0.3.9" | ||||
|       } | ||||
|     }, | ||||
|     "@jridgewell/sourcemap-codec": { | ||||
|       "version": "1.4.14", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", | ||||
|       "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@jridgewell/trace-mapping": { | ||||
|       "version": "0.3.14", | ||||
|       "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", | ||||
|       "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@jridgewell/resolve-uri": "^3.0.3", | ||||
|         "@jridgewell/sourcemap-codec": "^1.4.10" | ||||
|       } | ||||
|     }, | ||||
|     "@nodelib/fs.scandir": { | ||||
|       "version": "2.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||||
| @@ -20424,41 +20459,6 @@ | ||||
|         "param-case": "^3.0.4", | ||||
|         "relateurl": "^0.2.7", | ||||
|         "terser": "^5.10.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "acorn": { | ||||
|           "version": "8.7.0", | ||||
|           "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", | ||||
|           "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", | ||||
|           "dev": true, | ||||
|           "optional": true, | ||||
|           "peer": true | ||||
|         }, | ||||
|         "source-map": { | ||||
|           "version": "0.7.3", | ||||
|           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", | ||||
|           "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "terser": { | ||||
|           "version": "5.10.0", | ||||
|           "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", | ||||
|           "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "commander": "^2.20.0", | ||||
|             "source-map": "~0.7.2", | ||||
|             "source-map-support": "~0.5.20" | ||||
|           }, | ||||
|           "dependencies": { | ||||
|             "commander": { | ||||
|               "version": "2.20.3", | ||||
|               "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", | ||||
|               "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", | ||||
|               "dev": true | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "html-parse-stringify": { | ||||
| @@ -21815,9 +21815,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "moment": { | ||||
|       "version": "2.29.3", | ||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", | ||||
|       "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" | ||||
|       "version": "2.29.4", | ||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", | ||||
|       "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" | ||||
|     }, | ||||
|     "mrmime": { | ||||
|       "version": "1.0.0", | ||||
| @@ -24722,6 +24722,32 @@ | ||||
|       "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "terser": { | ||||
|       "version": "5.14.2", | ||||
|       "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", | ||||
|       "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "@jridgewell/source-map": "^0.3.2", | ||||
|         "acorn": "^8.5.0", | ||||
|         "commander": "^2.20.0", | ||||
|         "source-map-support": "~0.5.20" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "acorn": { | ||||
|           "version": "8.8.0", | ||||
|           "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", | ||||
|           "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "commander": { | ||||
|           "version": "2.20.3", | ||||
|           "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", | ||||
|           "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", | ||||
|           "dev": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "terser-webpack-plugin": { | ||||
|       "version": "5.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz", | ||||
| @@ -24735,20 +24761,6 @@ | ||||
|         "terser": "^5.7.2" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "acorn": { | ||||
|           "version": "8.7.0", | ||||
|           "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", | ||||
|           "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", | ||||
|           "dev": true, | ||||
|           "optional": true, | ||||
|           "peer": true | ||||
|         }, | ||||
|         "commander": { | ||||
|           "version": "2.20.3", | ||||
|           "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", | ||||
|           "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "has-flag": { | ||||
|           "version": "4.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", | ||||
| @@ -24791,25 +24803,6 @@ | ||||
|           "requires": { | ||||
|             "has-flag": "^4.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "terser": { | ||||
|           "version": "5.10.0", | ||||
|           "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", | ||||
|           "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "commander": "^2.20.0", | ||||
|             "source-map": "~0.7.2", | ||||
|             "source-map-support": "~0.5.20" | ||||
|           }, | ||||
|           "dependencies": { | ||||
|             "source-map": { | ||||
|               "version": "0.7.3", | ||||
|               "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", | ||||
|               "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", | ||||
|               "dev": true | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "ucentral-client", | ||||
|   "version": "2.6.29", | ||||
|   "version": "2.7.0(8)", | ||||
|   "dependencies": { | ||||
|     "@coreui/coreui": "^3.4.0", | ||||
|     "@coreui/icons": "^2.0.1", | ||||
|   | ||||
| @@ -326,6 +326,7 @@ | ||||
| 	"device": { | ||||
| 		"add_to_blacklist": "Gerät zur Blacklist hinzufügen", | ||||
| 		"all_devices": "Alle Geräte", | ||||
| 		"already_running_command": "Gerät führt bereits einen Befehl aus, bitte versuchen Sie es später erneut", | ||||
| 		"blacklisted_on": "Datum", | ||||
| 		"capabilities": "Fähigkeiten", | ||||
| 		"certificate_explanation": "Zertifikate der angeschlossenen Geräte", | ||||
| @@ -337,6 +338,7 @@ | ||||
| 		"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}", | ||||
| 		"firmware_count_explanation": "Dies ist die Gesamtzahl der Geräte, die diesem Firmware-Server hinzugefügt wurden, einschließlich der Geräte, die derzeit nicht auf den zugehörigen Gateway-Server verweisen.", | ||||
| 		"health_explanation": "Zustand der verbundenen Geräte ((Geräte = 100 % * 100 + Geräte > 90 % * 95 + Geräte > 60 % * 75 + Geräte < 60 % * 35) / Verbundene Geräte)", | ||||
| 		"mac_not_found": "Seriennummer nicht gefunden, Sie werden zur Seite „Geräte“ weitergeleitet", | ||||
| 		"memory_explanation": "Anzahl verbundener Geräte mit entsprechendem belegtem Speicher %", | ||||
| 		"remove_from_blacklist": "Von der schwarzen Liste entfernen", | ||||
| 		"success_added_blacklist": "Gerät erfolgreich zur Blacklist hinzugefügt!", | ||||
|   | ||||
| @@ -326,6 +326,7 @@ | ||||
| 	"device": { | ||||
| 		"add_to_blacklist": "Add Device To Blacklist", | ||||
| 		"all_devices": "All Devices", | ||||
| 		"already_running_command": "Device is already executing a command, please try later", | ||||
| 		"blacklisted_on": "Date", | ||||
| 		"capabilities": "Capabilities", | ||||
| 		"certificate_explanation": "Certificates of connected devices", | ||||
| @@ -337,6 +338,7 @@ | ||||
| 		"error_fetching_devices": "Error while fetching devices: {{error}}", | ||||
| 		"firmware_count_explanation": "This is the total amount of devices that were added to this firmware server, including devices not currently pointing at the related gateway server.", | ||||
| 		"health_explanation": "Health of connected devices ((Devices=100% * 100 + Devices>90% * 95 + Devices>60% * 75 + Devices<60% * 35) / ConnectedDevices)", | ||||
| 		"mac_not_found": "Serial number not found, redirecting you to the Devices page", | ||||
| 		"memory_explanation": "Amount of connected devices with corresponding memory used percentage", | ||||
| 		"remove_from_blacklist": "Remove from blacklist", | ||||
| 		"success_added_blacklist": "Device successfully added to blacklist!", | ||||
|   | ||||
| @@ -326,6 +326,7 @@ | ||||
| 	"device": { | ||||
| 		"add_to_blacklist": "Agregar dispositivo a la lista negra", | ||||
| 		"all_devices": "Todos los dispositivos", | ||||
| 		"already_running_command": "El dispositivo ya está ejecutando un comando, intente más tarde", | ||||
| 		"blacklisted_on": "Fecha", | ||||
| 		"capabilities": "capacidades", | ||||
| 		"certificate_explanation": "Certificados de dispositivos conectados", | ||||
| @@ -337,6 +338,7 @@ | ||||
| 		"error_fetching_devices": "Error al recuperar dispositivos: {{error}}", | ||||
| 		"firmware_count_explanation": "Esta es la cantidad total de dispositivos que se agregaron a este servidor de firmware, incluidos los dispositivos que actualmente no apuntan al servidor de puerta de enlace relacionado.", | ||||
| 		"health_explanation": "Estado de los dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos conectados)", | ||||
| 		"mac_not_found": "Número de serie no encontrado, lo redirige a la página Dispositivos", | ||||
| 		"memory_explanation": "Cantidad de dispositivos conectados con la memoria correspondiente utilizada%", | ||||
| 		"remove_from_blacklist": "ELIMINAR DE LA LISTA NEGRA", | ||||
| 		"success_added_blacklist": "¡Dispositivo agregado exitosamente a la lista negra!", | ||||
|   | ||||
| @@ -326,6 +326,7 @@ | ||||
| 	"device": { | ||||
| 		"add_to_blacklist": "Ajouter un appareil à la liste noire", | ||||
| 		"all_devices": "Tous les dispositifs", | ||||
| 		"already_running_command": "L'appareil exécute déjà une commande, veuillez réessayer plus tard", | ||||
| 		"blacklisted_on": "Rendez-vous amoureux", | ||||
| 		"capabilities": "Capacités", | ||||
| 		"certificate_explanation": "Certificats des appareils connectés", | ||||
| @@ -337,6 +338,7 @@ | ||||
| 		"error_fetching_devices": "Erreur lors de la récupération des appareils : {{error}}", | ||||
| 		"firmware_count_explanation": "Il s'agit du nombre total d'appareils qui ont été ajoutés à ce serveur de micrologiciel, y compris les appareils qui ne pointent pas actuellement vers le serveur de passerelle associé.", | ||||
| 		"health_explanation": "Santé des appareils connectés ((Appareils = 100 % * 100 + Appareils> 90 % * 95 + Appareils> 60 % * 75 + Appareils < 60 % * 35) / Appareils connectés)", | ||||
| 		"mac_not_found": "Numéro de série introuvable, vous redirigeant vers la page Appareils", | ||||
| 		"memory_explanation": "Nombre d'appareils connectés avec la mémoire correspondante utilisée %", | ||||
| 		"remove_from_blacklist": "Supprimer de la liste noire", | ||||
| 		"success_added_blacklist": "Appareil ajouté avec succès à la liste noire !", | ||||
|   | ||||
| @@ -326,6 +326,7 @@ | ||||
| 	"device": { | ||||
| 		"add_to_blacklist": "Adicionar dispositivo à lista negra", | ||||
| 		"all_devices": "Todos os dispositivos", | ||||
| 		"already_running_command": "O dispositivo já está executando um comando, tente mais tarde", | ||||
| 		"blacklisted_on": "Encontro", | ||||
| 		"capabilities": "Recursos", | ||||
| 		"certificate_explanation": "Certificados de dispositivos conectados", | ||||
| @@ -337,6 +338,7 @@ | ||||
| 		"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}", | ||||
| 		"firmware_count_explanation": "Esta é a quantidade total de dispositivos que foram adicionados a este servidor de firmware, incluindo dispositivos que não estão apontando para o servidor de gateway relacionado.", | ||||
| 		"health_explanation": "Integridade dos dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos Conectados)", | ||||
| 		"mac_not_found": "Número de série não encontrado, redirecionando você para a página Dispositivos", | ||||
| 		"memory_explanation": "Quantidade de dispositivos conectados com a memória correspondente usada%", | ||||
| 		"remove_from_blacklist": "Remover da lista negra", | ||||
| 		"success_added_blacklist": "Dispositivo adicionado à lista negra com sucesso!", | ||||
|   | ||||
| @@ -72,7 +72,18 @@ const BlinkModal = ({ show, toggleModal }) => { | ||||
|         } | ||||
|         toggleModal(); | ||||
|       }) | ||||
|       .catch(() => { | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|         setResult('error'); | ||||
|       }) | ||||
|       .finally(() => { | ||||
|   | ||||
| @@ -205,10 +205,11 @@ const DeviceCommands = () => { | ||||
|  | ||||
|   const columns = [ | ||||
|     { key: 'submitted', label: t('common.submitted'), filter: false, _style: { width: '20%' } }, | ||||
|     { key: 'command', label: t('common.command'), _style: { width: '15%' } }, | ||||
|     { key: 'command', label: t('common.command'), _style: { width: '0%' } }, | ||||
|     { key: 'status', label: t('common.status'), _style: { width: '0%' } }, | ||||
|     { key: 'executed', label: t('common.executed'), filter: false, _style: { width: '16%' } }, | ||||
|     { key: 'completed', label: t('common.completed'), filter: false, _style: { width: '16%' } }, | ||||
|     { key: 'errorCode', label: t('common.error_code'), filter: false, _style: { width: '8%' } }, | ||||
|     { key: 'errorCode', label: t('common.error_code'), filter: false }, | ||||
|     { | ||||
|       key: 'show_buttons', | ||||
|       label: '', | ||||
| @@ -317,16 +318,17 @@ const DeviceCommands = () => { | ||||
|                     {item.completed && item.completed !== 0 ? ( | ||||
|                       <FormattedDate date={item.completed} /> | ||||
|                     ) : ( | ||||
|                       'Pending' | ||||
|                       '-' | ||||
|                     )} | ||||
|                   </td> | ||||
|                 ), | ||||
|                 status: (item) => <td className="align-middle">{item.status}</td>, | ||||
|                 executed: (item) => ( | ||||
|                   <td className="align-middle"> | ||||
|                     {item.executed && item.executed !== 0 ? ( | ||||
|                       <FormattedDate date={item.executed} /> | ||||
|                     ) : ( | ||||
|                       'Pending' | ||||
|                       '-' | ||||
|                     )} | ||||
|                   </td> | ||||
|                 ), | ||||
| @@ -335,7 +337,7 @@ const DeviceCommands = () => { | ||||
|                     {item.submitted && item.submitted !== '' ? ( | ||||
|                       <FormattedDate date={item.submitted} /> | ||||
|                     ) : ( | ||||
|                       'Pending' | ||||
|                       '-' | ||||
|                     )} | ||||
|                   </td> | ||||
|                 ), | ||||
|   | ||||
| @@ -103,12 +103,17 @@ const ConfigureModal = ({ show, toggleModal }) => { | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         setResponseBody('Error while submitting command!'); | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|           body: `${t('common.general_error')}: ${e.response?.data?.ErrorDescription}`, | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|         setHadFailure(true); | ||||
|       }) | ||||
|       .finally(() => { | ||||
|   | ||||
| @@ -54,12 +54,17 @@ const DeviceActions = ({ device }) => { | ||||
|         if (newWindow) newWindow.opener = null; | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|           body: t('connect.error_trying_to_connect', { error: e.response?.data?.ErrorDescription }), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         setConnectLoading(false); | ||||
| @@ -68,6 +73,7 @@ const DeviceActions = ({ device }) => { | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (upgradeStatus.result !== undefined) { | ||||
|       if (upgradeStatus.result.success) { | ||||
|         addToast({ | ||||
|           title: upgradeStatus.result.success ? t('common.success') : t('common.error'), | ||||
|           body: upgradeStatus.result.success | ||||
| @@ -76,10 +82,11 @@ const DeviceActions = ({ device }) => { | ||||
|           color: upgradeStatus.result.success ? 'success' : 'danger', | ||||
|           autohide: true, | ||||
|         }); | ||||
|         setShowUpgradeModal(false); | ||||
|       } | ||||
|       setUpgradeStatus({ | ||||
|         loading: false, | ||||
|       }); | ||||
|       setShowUpgradeModal(false); | ||||
|     } | ||||
|   }, [upgradeStatus]); | ||||
|  | ||||
|   | ||||
| @@ -111,7 +111,18 @@ const DeviceFirmwareModal = ({ | ||||
|           }, | ||||
|         }); | ||||
|       }) | ||||
|       .catch(() => { | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|         setUpgradeStatus({ | ||||
|           loading: false, | ||||
|           result: { | ||||
|   | ||||
							
								
								
									
										75
									
								
								src/components/DeviceSearchBar/Input.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/components/DeviceSearchBar/Input.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import Select, { components } from 'react-select'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
|  | ||||
| const DeviceSearchBarInput = ({ search, results, history, action, isDisabled }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const [selected, setSelected] = useState(''); | ||||
|   const NoOptionsMessage = (props) => ( | ||||
|     <components.NoOptionsMessage {...props}> | ||||
|       <span>{t('common.no_devices_found')}</span> | ||||
|     </components.NoOptionsMessage> | ||||
|   ); | ||||
|  | ||||
|   const onInputChange = (value) => { | ||||
|     if (value === '' || value.match('^[a-fA-F0-9-*]+$')) { | ||||
|       setSelected(value); | ||||
|       search(value); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Select | ||||
|       components={{ NoOptionsMessage }} | ||||
|       options={results.map((serial) => ({ label: serial, value: serial }))} | ||||
|       filterOption={() => true} | ||||
|       inputValue={selected} | ||||
|       placeholder={t('common.search')} | ||||
|       isDisabled={isDisabled} | ||||
|       styles={{ | ||||
|         placeholder: (provided) => ({ | ||||
|           ...provided, | ||||
|           // disable placeholder mouse events | ||||
|           pointerEvents: 'none', | ||||
|           userSelect: 'none', | ||||
|           MozUserSelect: 'none', | ||||
|           WebkitUserSelect: 'none', | ||||
|           msUserSelect: 'none', | ||||
|         }), | ||||
|         input: (css) => ({ | ||||
|           ...css, | ||||
|           /* expand the Input Component div */ | ||||
|           flex: '1 1 auto', | ||||
|           /* expand the Input Component child div */ | ||||
|           '> div': { | ||||
|             width: '100%', | ||||
|           }, | ||||
|           /* expand the Input Component input */ | ||||
|           input: { | ||||
|             width: '100% !important', | ||||
|             textAlign: 'left', | ||||
|           }, | ||||
|         }), | ||||
|       }} | ||||
|       onInputChange={onInputChange} | ||||
|       onChange={(property) => | ||||
|         action === null ? history.push(`/devices/${property.value}`) : action(property.value) | ||||
|       } | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| DeviceSearchBarInput.propTypes = { | ||||
|   search: PropTypes.func.isRequired, | ||||
|   results: PropTypes.instanceOf(Array).isRequired, | ||||
|   history: PropTypes.instanceOf(Object).isRequired, | ||||
|   isDisabled: PropTypes.bool.isRequired, | ||||
|   action: PropTypes.func, | ||||
| }; | ||||
|  | ||||
| DeviceSearchBarInput.defaultProps = { | ||||
|   action: null, | ||||
| }; | ||||
|  | ||||
| export default DeviceSearchBarInput; | ||||
| @@ -1,12 +1,11 @@ | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { useHistory } from 'react-router-dom'; | ||||
| import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs'; | ||||
| import { useAuth } from 'ucentral-libs'; | ||||
| import { toJson } from 'utils/helper'; | ||||
| import DeviceSearchBarInput from './Input'; | ||||
|  | ||||
| const DeviceSearchBar = ({ action }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const history = useHistory(); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const [socket, setSocket] = useState(null); | ||||
| @@ -14,6 +13,7 @@ const DeviceSearchBar = ({ action }) => { | ||||
|   const [waitingSearch, setWaitingSearch] = useState(''); | ||||
|  | ||||
|   const search = (value) => { | ||||
|     if (socket) { | ||||
|       if (socket.readyState === WebSocket.OPEN) { | ||||
|         if (value.length > 1 && value.match('^[a-fA-F0-9-*]+$')) { | ||||
|           setWaitingSearch(''); | ||||
| @@ -23,12 +23,13 @@ const DeviceSearchBar = ({ action }) => { | ||||
|         } else { | ||||
|           setResults([]); | ||||
|         } | ||||
|     } else if (socket.readyState !== WebSocket.CONNECTING) { | ||||
|       } else if (socket.readyState !== WebSocket.CONNECTING && endpoints?.owgw !== undefined) { | ||||
|         setWaitingSearch(value); | ||||
|         setSocket(new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`)); | ||||
|       } else { | ||||
|         setWaitingSearch(value); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const closeSocket = () => { | ||||
| @@ -59,12 +60,20 @@ const DeviceSearchBar = ({ action }) => { | ||||
|   }, [socket]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (socket === null && endpoints?.owgw) { | ||||
|     if (socket === null && endpoints?.owgw !== undefined) { | ||||
|       setSocket(new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`)); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   return <SearchBar t={t} search={search} results={results} history={history} action={action} />; | ||||
|   return ( | ||||
|     <DeviceSearchBarInput | ||||
|       search={search} | ||||
|       results={results} | ||||
|       history={history} | ||||
|       action={action} | ||||
|       isDisabled={endpoints.owgw === undefined} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| DeviceSearchBar.propTypes = { | ||||
|   | ||||
| @@ -34,12 +34,17 @@ const EventQueueModal = ({ show, toggle }) => { | ||||
|         setResult(response.data); | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|           body: t('commands.unable_queue', { error: e.response?.data?.ErrorDescription }), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|       .finally(() => { | ||||
|         setLoading(false); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import React, { useState, useEffect } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import 'react-widgets/styles.css'; | ||||
| import { useAuth, useDevice } from 'ucentral-libs'; | ||||
| import { useAuth, useDevice, useToast } from 'ucentral-libs'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody'; | ||||
|  | ||||
| @@ -26,6 +26,7 @@ const ConfigureModal = ({ show, toggleModal }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const { deviceSerialNumber } = useDevice(); | ||||
|   const { addToast } = useToast(); | ||||
|   const [hadSuccess, setHadSuccess] = useState(false); | ||||
|   const [hadFailure, setHadFailure] = useState(false); | ||||
|   const [doingNow, setDoingNow] = useState(false); | ||||
| @@ -74,7 +75,18 @@ const ConfigureModal = ({ show, toggleModal }) => { | ||||
|       .then(() => { | ||||
|         setHadSuccess(true); | ||||
|       }) | ||||
|       .catch(() => { | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|         setResponseBody(t('commands.error')); | ||||
|         setHadFailure(true); | ||||
|       }) | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/components/FormattedDate/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/components/FormattedDate/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { CPopover } from '@coreui/react'; | ||||
| import { formatDaysAgo, prettyDate } from 'utils/helper'; | ||||
|  | ||||
| const FormattedDate = ({ date, size }) => { | ||||
|   if (size === 'lg') { | ||||
|     return ( | ||||
|       <CPopover content={prettyDate(date)} advancedOptions={{ animation: false }}> | ||||
|         <h2 className="d-inline-block">{date === 0 ? '-' : formatDaysAgo(date)}</h2> | ||||
|       </CPopover> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <CPopover content={prettyDate(date)} advancedOptions={{ animation: false }}> | ||||
|       <span className="d-inline-block">{date === 0 ? '-' : formatDaysAgo(date)}</span> | ||||
|     </CPopover> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| FormattedDate.propTypes = { | ||||
|   date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||||
|   size: PropTypes.string, | ||||
| }; | ||||
|  | ||||
| FormattedDate.defaultProps = { | ||||
|   date: 0, | ||||
|   size: 'md', | ||||
| }; | ||||
|  | ||||
| export default FormattedDate; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import React, { useState, useEffect, useMemo } from 'react'; | ||||
| import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react'; | ||||
| import CIcon from '@coreui/icons-react'; | ||||
| import { cilX } from '@coreui/icons'; | ||||
| @@ -32,6 +32,17 @@ const LatestStatisticsModal = ({ show, toggle }) => { | ||||
|       .catch(() => {}); | ||||
|   }; | ||||
|  | ||||
|   const latestStatsString = useMemo(() => { | ||||
|     if (latestStats) { | ||||
|       try { | ||||
|         return JSON.stringify(latestStats, null, 2); | ||||
|       } catch (e) { | ||||
|         return ''; | ||||
|       } | ||||
|     } | ||||
|     return ''; | ||||
|   }, [latestStats]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (show) { | ||||
|       getLatestStats(); | ||||
| @@ -52,13 +63,9 @@ const LatestStatisticsModal = ({ show, toggle }) => { | ||||
|       </CModalHeader> | ||||
|       <CModalBody> | ||||
|         <div style={{ textAlign: 'right' }}> | ||||
|           <CopyToClipboardButton | ||||
|             t={t} | ||||
|             size="lg" | ||||
|             content={JSON.stringify(latestStats ?? {}, null, 4)} | ||||
|           /> | ||||
|           <CopyToClipboardButton t={t} size="lg" content={latestStatsString} /> | ||||
|         </div> | ||||
|         <pre className="ignore">{JSON.stringify(latestStats, null, 2)}</pre> | ||||
|         <pre className="ignore">{latestStatsString}</pre> | ||||
|       </CModalBody> | ||||
|     </CModal> | ||||
|   ); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import React, { useState, useEffect, useCallback } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { CSpinner } from '@coreui/react'; | ||||
| import { CSpinner, CAlert } from '@coreui/react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { v4 as createUuid } from 'uuid'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| @@ -23,8 +23,10 @@ const StatisticsChartList = ({ deviceSerialNumber, setOptions, section, time }) | ||||
|     memory: [], | ||||
|     settings: {}, | ||||
|   }); | ||||
|   const [error, setError] = useState(false); | ||||
|  | ||||
|   const transformIntoDataset = (data) => { | ||||
|     try { | ||||
|       let sortedData = data.sort((a, b) => { | ||||
|         if (a.recorded > b.recorded) return 1; | ||||
|         if (b.recorded > a.recorded) return -1; | ||||
| @@ -244,9 +246,35 @@ const StatisticsChartList = ({ deviceSerialNumber, setOptions, section, time }) | ||||
|         setOptions([...sectionOptions, { value: 'memory', label: t('statistics.memory') }]); | ||||
|         setStatOptions({ ...newOptions }); | ||||
|       } | ||||
|       setError(undefined); | ||||
|     } catch (e) { | ||||
|       if (data?.length === 0) { | ||||
|         setError('nodata'); | ||||
|       } else { | ||||
|         setError('error'); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const getInterface = useCallback(() => { | ||||
|     if (error === 'error') { | ||||
|       return ( | ||||
|         <div style={{ textAlign: 'center' }}> | ||||
|           <CAlert color="danger" style={{ width: '240px', margin: 'auto' }}> | ||||
|             Error while parsing statistics | ||||
|           </CAlert> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
|     if (error === 'nodata') { | ||||
|       return ( | ||||
|         <div style={{ textAlign: 'center' }}> | ||||
|           <CAlert color="danger" style={{ width: '340px', margin: 'auto' }}> | ||||
|             No available statistics during this time period | ||||
|           </CAlert> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
|     if (statOptions.interfaceList.length === 0) return <p>N/A</p>; | ||||
|  | ||||
|     const interfaceToShow = statOptions.interfaceList.find( | ||||
| @@ -273,8 +301,9 @@ const StatisticsChartList = ({ deviceSerialNumber, setOptions, section, time }) | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return <p>N/A</p>; | ||||
|   }, [statOptions, section]); | ||||
|   }, [statOptions, section, error]); | ||||
|  | ||||
|   const getStatistics = () => { | ||||
|     setLoading(true); | ||||
|   | ||||
| @@ -25,6 +25,7 @@ const NetworkDiagram = ({ show, elements, setElements }) => { | ||||
|         onElementsRemove={onElementsRemove} | ||||
|         onLoad={onLoad} | ||||
|         snapToGrid | ||||
|         minZoom={0.1} | ||||
|         snapGrid={[20, 20]} | ||||
|       > | ||||
|         <MiniMap | ||||
|   | ||||
| @@ -47,7 +47,7 @@ const associationNode = (associationInfo) => ( | ||||
|   <div> | ||||
|     <CRow> | ||||
|       <CCol className="text-center"> | ||||
|         <h6>{associationInfo.bssid}</h6> | ||||
|         <h6>{associationInfo.station}</h6> | ||||
|       </CCol> | ||||
|     </CRow> | ||||
|     <CRow> | ||||
| @@ -92,7 +92,6 @@ const NetworkDiagram = ({ show, radios, associations }) => { | ||||
|     // Creating the association nodes and their edges | ||||
|     for (let i = 0; i < associations.length; i += 1) { | ||||
|       const assoc = associations[i]; | ||||
|  | ||||
|       // If the radio has not been added, we create a new unknown radio based on its index | ||||
|       if (radiosAdded[assoc.radio.radioIndex] === undefined) { | ||||
|         newElements.push({ | ||||
| @@ -107,7 +106,7 @@ const NetworkDiagram = ({ show, radios, associations }) => { | ||||
|  | ||||
|       // Adding the association | ||||
|       newElements.push({ | ||||
|         id: `a-${assoc.bssid}`, | ||||
|         id: `a-${assoc.station}`, | ||||
|         data: { label: associationNode(assoc) }, | ||||
|         position: { | ||||
|           x: getX(radiosAdded[assoc.radio.radioIndex]), | ||||
| @@ -120,9 +119,9 @@ const NetworkDiagram = ({ show, radios, associations }) => { | ||||
|  | ||||
|       // Creating the edge | ||||
|       newElements.push({ | ||||
|         id: `e-${assoc.radio.radioIndex}-${assoc.bssid}`, | ||||
|         id: `e-${assoc.radio.radioIndex}-${assoc.station}`, | ||||
|         source: `r-${assoc.radio.radioIndex}`, | ||||
|         target: `a-${assoc.bssid}`, | ||||
|         target: `a-${assoc.station}`, | ||||
|         arrowHeadType: 'arrowclosed', | ||||
|       }); | ||||
|     } | ||||
|   | ||||
| @@ -89,7 +89,18 @@ const ActionModal = ({ show, toggleModal }) => { | ||||
|         }); | ||||
|         toggleModal(); | ||||
|       }) | ||||
|       .catch(() => { | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|         setResult('error'); | ||||
|       }) | ||||
|       .finally(() => { | ||||
|   | ||||
| @@ -105,12 +105,17 @@ const TelemetryModal = ({ show, toggle }) => { | ||||
|         } | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|           body: t('telemetry.connection_failed', { error: e.response?.data?.ErrorDescription }), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|       .finally(() => setLoading(false)); | ||||
|   }; | ||||
|   | ||||
| @@ -23,13 +23,14 @@ import PropTypes from 'prop-types'; | ||||
| import 'react-widgets/styles.css'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import eventBus from 'utils/eventBus'; | ||||
| import { LoadingButton, useAuth, useDevice } from 'ucentral-libs'; | ||||
| import { LoadingButton, useAuth, useDevice, useToast } from 'ucentral-libs'; | ||||
| import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody'; | ||||
| import WaitingForTraceBody from './WaitingForTraceBody'; | ||||
|  | ||||
| const TraceModal = ({ show, toggleModal }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const { addToast } = useToast(); | ||||
|   const { deviceSerialNumber, getDeviceConnection } = useDevice(); | ||||
|   const [hadSuccess, setHadSuccess] = useState(false); | ||||
|   const [hadFailure, setHadFailure] = useState(false); | ||||
| @@ -94,7 +95,18 @@ const TraceModal = ({ show, toggleModal }) => { | ||||
|           setWaitingForTrace(true); | ||||
|         } | ||||
|       }) | ||||
|       .catch(() => { | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|         setResponseBody(t('commands.error')); | ||||
|         setHadFailure(true); | ||||
|       }) | ||||
|   | ||||
| @@ -20,31 +20,22 @@ import PropTypes from 'prop-types'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import eventBus from 'utils/eventBus'; | ||||
| import { prettyDateForFile } from 'utils/helper'; | ||||
| import { useAuth, useDevice } from 'ucentral-libs'; | ||||
| import { useAuth, useDevice, useToast } from 'ucentral-libs'; | ||||
| import WifiChannelTable from 'components/WifiScanResultModal/WifiChannelTable'; | ||||
| import 'react-widgets/styles.css'; | ||||
| import { CSVLink } from 'react-csv'; | ||||
| import Select from 'react-select'; | ||||
| import IeDisplay from 'components/WifiScanResultModal/IeDisplay'; | ||||
| import IE_OPTIONS from './IE_OPTIONS.json'; | ||||
|  | ||||
| const getIeOptions = () => { | ||||
|   const arr = []; | ||||
|   for (const [key, value] of Object.entries(IE_OPTIONS)) { | ||||
|     arr.push({ | ||||
|       label: `${key} (${value})`, | ||||
|       value, | ||||
|     }); | ||||
|   } | ||||
|   return arr; | ||||
| }; | ||||
| const allIes = Object.entries(IE_OPTIONS).map(([, value]) => value); | ||||
|  | ||||
| const WifiScanModal = ({ show, toggleModal }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const { deviceSerialNumber } = useDevice(); | ||||
|   const { addToast } = useToast(); | ||||
|   const [hadSuccess, setHadSuccess] = useState(false); | ||||
|   const [selectedIes, setSelectedIes] = useState(undefined); | ||||
|   const [ies, setIes] = useState([]); | ||||
|   const [hadFailure, setHadFailure] = useState(false); | ||||
|   const [errorCode, setErrorCode] = useState(0); | ||||
|   const [waiting, setWaiting] = useState(false); | ||||
| @@ -63,14 +54,6 @@ const WifiScanModal = ({ show, toggleModal }) => { | ||||
|     setActiveScan(!activeScan); | ||||
|   }; | ||||
|  | ||||
|   const onIesChange = (v) => { | ||||
|     if (v.find(({ value }) => value === '*')) { | ||||
|       setIes(getIeOptions()); | ||||
|     } else { | ||||
|       setIes(v); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setHadSuccess(false); | ||||
|     setHadFailure(false); | ||||
| @@ -82,7 +65,7 @@ const WifiScanModal = ({ show, toggleModal }) => { | ||||
|     setActiveScan(false); | ||||
|     setHideOptions(false); | ||||
|     setErrorCode(0); | ||||
|     setIes([]); | ||||
|     setSelectedIes(undefined); | ||||
|   }, [show]); | ||||
|  | ||||
|   const parseThroughList = (scanList) => { | ||||
| @@ -164,7 +147,7 @@ const WifiScanModal = ({ show, toggleModal }) => { | ||||
|       override_dfs: dfs, | ||||
|       bandwidth: bandwidth !== '' ? bandwidth : undefined, | ||||
|       activeScan, | ||||
|       ies: ies?.length > 0 ? ies.map(({ value }) => value) : undefined, | ||||
|       ies: allIes, | ||||
|     }; | ||||
|     const headers = { | ||||
|       Accept: 'application/json', | ||||
| @@ -190,7 +173,18 @@ const WifiScanModal = ({ show, toggleModal }) => { | ||||
|           setHadFailure(true); | ||||
|         } | ||||
|       }) | ||||
|       .catch(() => { | ||||
|       .catch((e) => { | ||||
|         if (e.response?.data?.ErrorDescription !== undefined) { | ||||
|           const split = e.response?.data?.ErrorDescription.split(':'); | ||||
|           if (split !== undefined && split.length >= 2) { | ||||
|             addToast({ | ||||
|               title: t('common.error'), | ||||
|               body: split[1], | ||||
|               color: 'danger', | ||||
|               autohide: true, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|         setHadFailure(true); | ||||
|       }) | ||||
|       .finally(() => { | ||||
| @@ -289,23 +283,6 @@ const WifiScanModal = ({ show, toggleModal }) => { | ||||
|               </CSelect> | ||||
|             </CCol> | ||||
|           </CRow> | ||||
|           <CRow className="mt-3"> | ||||
|             <CCol md="3"> | ||||
|               <p className="pl-2">{t('actions.request_ie')}:</p> | ||||
|             </CCol> | ||||
|             <CCol> | ||||
|               <Select | ||||
|                 isMulti | ||||
|                 closeMenuOnSelect={false} | ||||
|                 name="request_ie" | ||||
|                 options={[{ label: 'All', value: '*' }, ...getIeOptions()]} | ||||
|                 onChange={onIesChange} | ||||
|                 value={ies} | ||||
|                 className="basic-multi-select" | ||||
|                 classNamePrefix="select" | ||||
|               /> | ||||
|             </CCol> | ||||
|           </CRow> | ||||
|         </div> | ||||
|         <div hidden={!waiting}> | ||||
|           <CRow> | ||||
| @@ -332,10 +309,10 @@ const WifiScanModal = ({ show, toggleModal }) => { | ||||
|               </CCol> | ||||
|             </CRow> | ||||
|           )} | ||||
|           {selectedIes && <IeDisplay ies={selectedIes} setIes={setSelectedIes} />} | ||||
|           {selectedIes || channelList === null ? null : ( | ||||
|             <WifiChannelTable channels={channelList} setIes={setSelectedIes} /> | ||||
|           )} | ||||
|           {selectedIes && <IeDisplay ies={selectedIes} setIes={setSelectedIes} />} | ||||
|         </div> | ||||
|       </CModalBody> | ||||
|     </CModal> | ||||
|   | ||||
							
								
								
									
										4
									
								
								src/constants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/constants.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| export const AUTH_EXPIRED_TOKEN_CODE = 9; | ||||
| export const AUTH_INVALID_TOKEN_CODE = 8; | ||||
|  | ||||
| export const LOGOUT_ON_SEC_ERROR_CODES = [AUTH_EXPIRED_TOKEN_CODE, AUTH_INVALID_TOKEN_CODE]; | ||||
| @@ -11,7 +11,7 @@ const WebSocketContext = React.createContext({ | ||||
|   addDeviceListener: () => {}, | ||||
| }); | ||||
|  | ||||
| export const WebSocketProvider = ({ children }) => { | ||||
| export const WebSocketProvider = ({ children, setNewConnectionData }) => { | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const [isOpen, setIsOpen] = useState(false); | ||||
|   const ws = useRef(undefined); | ||||
| @@ -20,6 +20,9 @@ export const WebSocketProvider = ({ children }) => { | ||||
|  | ||||
|   const onMessage = useCallback((message) => { | ||||
|     const result = extractWebSocketResponse(message); | ||||
|     if (result?.type === 'device_connections_statistics') { | ||||
|       setNewConnectionData(result.content); | ||||
|     } | ||||
|     if (result?.type === 'NOTIFICATION') { | ||||
|       dispatch({ type: 'NEW_NOTIFICATION', notification: result.notification }); | ||||
|       pushNotification(result.notification); | ||||
| @@ -36,23 +39,29 @@ export const WebSocketProvider = ({ children }) => { | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   // useEffect for created the WebSocket and 'storing' it in useRef | ||||
|   useEffect(() => { | ||||
|     ws.current = new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`); | ||||
|   const onStartWebSocket = () => { | ||||
|     ws.current = new WebSocket(`${endpoints.owgw?.replace('https', 'wss')}/api/v1/ws`); | ||||
|     ws.current.onopen = () => { | ||||
|       setIsOpen(true); | ||||
|       ws.current?.send(`token:${currentToken}`); | ||||
|     }; | ||||
|     ws.current.onclose = () => { | ||||
|       setIsOpen(false); | ||||
|       setTimeout(onStartWebSocket, 3000); | ||||
|     }; | ||||
|     ws.current.onerror = () => { | ||||
|       setIsOpen(false); | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   // useEffect for created the WebSocket and 'storing' it in useRef | ||||
|   useEffect(() => { | ||||
|     if (endpoints?.owgw !== undefined) { | ||||
|       onStartWebSocket(); | ||||
|     } | ||||
|     const wsCurrent = ws?.current; | ||||
|     return () => wsCurrent?.close(); | ||||
|   }, []); | ||||
|   }, [endpoints]); | ||||
|  | ||||
|   // useEffect for generating global notifications | ||||
|   useEffect(() => { | ||||
| @@ -65,6 +74,7 @@ export const WebSocketProvider = ({ children }) => { | ||||
|       if (wsCurrent) wsCurrent.removeEventListener('message', onMessage); | ||||
|     }; | ||||
|   }, [ws?.current]); | ||||
|  | ||||
|   const values = useMemo( | ||||
|     () => ({ | ||||
|       lastMessage, | ||||
| @@ -83,6 +93,7 @@ export const WebSocketProvider = ({ children }) => { | ||||
|  | ||||
| WebSocketProvider.propTypes = { | ||||
|   children: PropTypes.node.isRequired, | ||||
|   setNewConnectionData: PropTypes.func.isRequired, | ||||
| }; | ||||
|  | ||||
| export const useGlobalWebSocket = () => React.useContext(WebSocketContext); | ||||
|   | ||||
| @@ -26,30 +26,11 @@ export const extractWebSocketResponse = (message) => { | ||||
|     if (data.command_response_id) { | ||||
|       return { data, type: 'COMMAND' }; | ||||
|     } | ||||
|     if (data.notification.type === 'device_connections_statistics') { | ||||
|       return { content: data.notification.content, type: 'device_connections_statistics' }; | ||||
|     } | ||||
|   } catch { | ||||
|     return undefined; | ||||
|   } | ||||
|   return undefined; | ||||
| }; | ||||
|  | ||||
| export const getStatusFromNotification = (notification) => { | ||||
|   let status = 'success'; | ||||
|   if (notification.content.warning?.length > 0) status = 'warning'; | ||||
|   if (notification.content.error?.length > 0) status = 'error'; | ||||
|  | ||||
|   return status; | ||||
| }; | ||||
|  | ||||
| export const getNotificationDescription = (t, notification) => { | ||||
|   if ( | ||||
|     notification.content.type === 'venue_configuration_update' || | ||||
|     notification.content.type === 'entity_configuration_update' | ||||
|   ) { | ||||
|     return t('configurations.notification_details', { | ||||
|       success: notification.content.success.length, | ||||
|       warning: notification.content.warning.length, | ||||
|       error: notification.content.error.length, | ||||
|     }); | ||||
|   } | ||||
|   return notification.content.details; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										17
									
								
								src/hooks/useToggle/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/hooks/useToggle/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useState } from 'react'; | ||||
|  | ||||
| const useToggle = (initialState) => { | ||||
|   const [value, setValue] = useState(initialState); | ||||
|  | ||||
|   return [ | ||||
|     value, | ||||
|     () => { | ||||
|       setValue(!value); | ||||
|     }, | ||||
|     (newValue) => { | ||||
|       setValue(newValue); | ||||
|     }, | ||||
|   ]; | ||||
| }; | ||||
|  | ||||
| export default useToggle; | ||||
							
								
								
									
										120
									
								
								src/layout/Devices.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/layout/Devices.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import { useAuth } from 'ucentral-libs'; | ||||
| import { CPopover } from '@coreui/react'; | ||||
| import { extraCompactSecondsToDetailed, secondsToDetailed } from 'utils/helper'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| const propTypes = { | ||||
|   newData: PropTypes.instanceOf(Object), | ||||
| }; | ||||
| const defaultProps = { | ||||
|   newData: undefined, | ||||
| }; | ||||
|  | ||||
| const SidebarDevices = ({ newData }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const [stats, setStats] = useState(); | ||||
|   const [lastUpdate, setLastUpdate] = useState(); | ||||
|   const [lastTime, setLastTime] = useState(); | ||||
|  | ||||
|   const getInitialStats = async () => { | ||||
|     const options = { | ||||
|       headers: { | ||||
|         Accept: 'application/json', | ||||
|         Authorization: `Bearer ${currentToken}`, | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
|     axiosInstance | ||||
|       .get(`${endpoints.owgw}/api/v1/devices?connectionStatistics=true`, options) | ||||
|       .then(({ data }) => { | ||||
|         setStats(data); | ||||
|         setLastUpdate(new Date()); | ||||
|       }) | ||||
|       .catch(() => {}); | ||||
|   }; | ||||
|  | ||||
|   const getTime = () => { | ||||
|     if (lastTime === undefined || lastUpdate === undefined) return null; | ||||
|  | ||||
|     const seconds = lastTime.getTime() - lastUpdate.getTime(); | ||||
|  | ||||
|     return Math.max(0, Math.floor(seconds / 1000)); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (newData !== undefined && Object.keys(newData).length > 0) { | ||||
|       setStats({ ...newData }); | ||||
|       setLastUpdate(new Date()); | ||||
|     } | ||||
|   }, [newData]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     getInitialStats(); | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const interval = setInterval(() => { | ||||
|       setLastTime(new Date()); | ||||
|     }, 1000); | ||||
|     return () => { | ||||
|       clearInterval(interval); | ||||
|     }; | ||||
|   }, []); | ||||
|  | ||||
|   if (!stats) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div | ||||
|       style={{ | ||||
|         position: 'absolute', | ||||
|         bottom: '0px', | ||||
|         width: '100%', | ||||
|         background: '#2f3d54 !important', | ||||
|         backgroundColor: '#2f3d54 !important', | ||||
|         borderTop: '3px solid #d8dbe0', | ||||
|         color: 'white', | ||||
|         textAlign: 'center', | ||||
|         paddingTop: '15px', | ||||
|         paddingBottom: '25px', | ||||
|       }} | ||||
|     > | ||||
|       <h3 style={{ marginBottom: '0px' }}>{stats?.connectedDevices ?? stats?.numberOfDevices}</h3> | ||||
|       <h6>Connected Devices</h6> | ||||
|       <CPopover | ||||
|         content={secondsToDetailed( | ||||
|           stats?.averageConnectionTime ?? stats?.averageConnectedTime, | ||||
|           t('common.day'), | ||||
|           t('common.days'), | ||||
|           t('common.hour'), | ||||
|           t('common.hours'), | ||||
|           t('common.minute'), | ||||
|           t('common.minutes'), | ||||
|           t('common.second'), | ||||
|           t('common.seconds'), | ||||
|         )} | ||||
|       > | ||||
|         <h3 style={{ marginBottom: '0px' }}> | ||||
|           {extraCompactSecondsToDetailed( | ||||
|             stats?.averageConnectionTime ?? stats?.averageConnectedTime, | ||||
|             t('common.day'), | ||||
|             t('common.days'), | ||||
|             t('common.seconds'), | ||||
|           )} | ||||
|         </h3> | ||||
|       </CPopover> | ||||
|       <h6>Avg. Connection Time</h6> | ||||
|       <h7 style={{ color: '#ebedef', fontStyle: 'italic' }}>{getTime()} seconds ago</h7> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| SidebarDevices.propTypes = propTypes; | ||||
| SidebarDevices.defaultProps = defaultProps; | ||||
|  | ||||
| export default React.memo(SidebarDevices); | ||||
							
								
								
									
										49
									
								
								src/layout/Sidebar/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/layout/Sidebar/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import React from 'react'; | ||||
| import { CSidebar, CSidebarBrand, CSidebarNav } from '@coreui/react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import styles from './index.module.scss'; | ||||
|  | ||||
| const Sidebar = ({ | ||||
|   showSidebar, | ||||
|   setShowSidebar, | ||||
|   logo, | ||||
|   options, | ||||
|   redirectTo, | ||||
|   logoHeight, | ||||
|   logoWidth, | ||||
| }) => ( | ||||
|   <CSidebar show={showSidebar} onShowChange={(val) => setShowSidebar(val)}> | ||||
|     <CSidebarBrand className="d-md-down-none" to={redirectTo}> | ||||
|       <img | ||||
|         className={[styles.sidebarImgFull, 'c-sidebar-brand-full'].join(' ')} | ||||
|         style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }} | ||||
|         src={logo} | ||||
|         alt="OpenWifi" | ||||
|       /> | ||||
|       <img | ||||
|         className={[styles.sidebarImgMinimized, 'c-sidebar-brand-minimized'].join(' ')} | ||||
|         style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }} | ||||
|         src={logo} | ||||
|         alt="OpenWifi" | ||||
|       /> | ||||
|     </CSidebarBrand> | ||||
|     <CSidebarNav>{options}</CSidebarNav> | ||||
|   </CSidebar> | ||||
| ); | ||||
|  | ||||
| Sidebar.propTypes = { | ||||
|   showSidebar: PropTypes.string.isRequired, | ||||
|   setShowSidebar: PropTypes.func.isRequired, | ||||
|   logo: PropTypes.string.isRequired, | ||||
|   options: PropTypes.node.isRequired, | ||||
|   redirectTo: PropTypes.string.isRequired, | ||||
|   logoHeight: PropTypes.string, | ||||
|   logoWidth: PropTypes.string, | ||||
| }; | ||||
|  | ||||
| Sidebar.defaultProps = { | ||||
|   logoHeight: null, | ||||
|   logoWidth: null, | ||||
| }; | ||||
|  | ||||
| export default React.memo(Sidebar); | ||||
							
								
								
									
										9
									
								
								src/layout/Sidebar/index.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/layout/Sidebar/index.module.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| .sidebarImgFull { | ||||
|   height: 75px; | ||||
|   width: 175px; | ||||
| } | ||||
|  | ||||
| .sidebarImgMinimized { | ||||
|   height: 75px; | ||||
|   width: 75px; | ||||
| } | ||||
| @@ -4,13 +4,20 @@ import routes from 'routes'; | ||||
| import { CSidebarNavItem } from '@coreui/react'; | ||||
| import { cilBarcode, cilRouter, cilSave, cilSettings, cilPeople } from '@coreui/icons'; | ||||
| import CIcon from '@coreui/icons-react'; | ||||
| import { Header, Sidebar, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs'; | ||||
| import { Header, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs'; | ||||
| import { WebSocketProvider } from 'contexts/WebSocketProvider'; | ||||
| import Sidebar from './Sidebar'; | ||||
| import SidebarDevices from './Devices'; | ||||
|  | ||||
| const TheLayout = () => { | ||||
|   const [showSidebar, setShowSidebar] = useState('responsive'); | ||||
|   const { endpoints, currentToken, user, avatar, logout } = useAuth(); | ||||
|   const { t, i18n } = useTranslation(); | ||||
|   const [newConnectionData, setNewConnectionData] = useState(); | ||||
|  | ||||
|   const onConnectionDataChange = React.useCallback((newData) => { | ||||
|     setNewConnectionData({ ...newData }); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <div className="c-app c-default-layout"> | ||||
| @@ -50,6 +57,7 @@ const TheLayout = () => { | ||||
|               to="/system" | ||||
|               icon={<CIcon content={cilSettings} size="xl" className="mr-3" />} | ||||
|             /> | ||||
|             <SidebarDevices newData={newConnectionData} /> | ||||
|           </> | ||||
|         } | ||||
|         redirectTo="/devices" | ||||
| @@ -71,7 +79,7 @@ const TheLayout = () => { | ||||
|         /> | ||||
|         <div className="c-body"> | ||||
|           <ToastProvider> | ||||
|             <WebSocketProvider> | ||||
|             <WebSocketProvider setNewConnectionData={onConnectionDataChange}> | ||||
|               <PageContainer t={t} routes={routes} redirectTo="/devices" /> | ||||
|             </WebSocketProvider> | ||||
|           </ToastProvider> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import { useParams } from 'react-router-dom'; | ||||
| import { useHistory, useParams } from 'react-router-dom'; | ||||
| import { CRow, CCol, CCard, CCardBody, CNav, CNavLink, CTabPane, CTabContent } from '@coreui/react'; | ||||
| import DeviceHealth from 'components/DeviceHealth'; | ||||
| import CommandHistory from 'components/CommandHistory'; | ||||
| @@ -7,7 +7,7 @@ import DeviceLogs from 'components/DeviceLogs'; | ||||
| import DeviceStatisticsCard from 'components/InterfaceStatistics'; | ||||
| import DeviceActionCard from 'components/DeviceActionCard'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import { DeviceProvider, useAuth } from 'ucentral-libs'; | ||||
| import { DeviceProvider, useAuth, useToast } from 'ucentral-libs'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import ConfigurationDisplay from 'components/ConfigurationDisplay'; | ||||
| import WifiAnalysis from 'components/WifiAnalysis'; | ||||
| @@ -23,7 +23,9 @@ const DevicePage = () => { | ||||
|   const [index, setIndex] = useState(0); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const [lastStats, setLastStats] = useState(null); | ||||
|   const { addToast } = useToast(); | ||||
|   const [status, setStatus] = useState(null); | ||||
|   const history = useHistory(); | ||||
|   const [deviceConfig, setDeviceConfig] = useState(null); | ||||
|   const [error, setError] = useState(false); | ||||
|   const [loading, setLoading] = useState(false); | ||||
| @@ -64,7 +66,16 @@ const DevicePage = () => { | ||||
|       .then((response) => { | ||||
|         if (response) setDeviceConfig({ ...deviceInfo, extendedInfo: response.data.extendedInfo }); | ||||
|       }) | ||||
|       .catch(() => { | ||||
|       .catch((e) => { | ||||
|         if (e.response?.status === 404 || e.response?.status === 400) { | ||||
|           addToast({ | ||||
|             title: t('common.error'), | ||||
|             body: t('device.mac_not_found'), | ||||
|             color: 'danger', | ||||
|             autohide: true, | ||||
|           }); | ||||
|           history.push('/devices'); | ||||
|         } | ||||
|         setDeviceConfig(deviceInfo); | ||||
|       }); | ||||
|   }; | ||||
|   | ||||
							
								
								
									
										189
									
								
								src/pages/SystemPage/ApiStatusCard/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/pages/SystemPage/ApiStatusCard/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { | ||||
|   CCard, | ||||
|   CCardBody, | ||||
|   CCardHeader, | ||||
|   CRow, | ||||
|   CCol, | ||||
|   CButton, | ||||
|   CPopover, | ||||
|   CModal, | ||||
|   CModalBody, | ||||
|   CModalHeader, | ||||
|   CModalTitle, | ||||
|   CDataTable, | ||||
| } from '@coreui/react'; | ||||
| import Select from 'react-select'; | ||||
| import CIcon from '@coreui/icons-react'; | ||||
| import { cilSync, cilX } from '@coreui/icons'; | ||||
| import { prettyDate } from 'utils/helper'; | ||||
| import useToggle from 'hooks/useToggle'; | ||||
| import FormattedDate from 'components/FormattedDate'; | ||||
|  | ||||
| const ApiStatusCard = ({ t, info, reload }) => { | ||||
|   const [types, setTypes] = useState([]); | ||||
|   const [showCerts, toggleCerts] = useToggle(); | ||||
|  | ||||
|   const submit = () => { | ||||
|     reload( | ||||
|       types.map((v) => v.value), | ||||
|       info.endpoint, | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <CCard> | ||||
|       <CCardHeader className="dark-header"> | ||||
|         <div style={{ fontWeight: '600' }} className=" text-value-lg float-left"> | ||||
|           {info.title} | ||||
|         </div> | ||||
|       </CCardHeader> | ||||
|       <CCardBody> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('common.endpoint')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true">{info.endpoint}</div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('system.hostname')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true">{info.hostname}</div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('system.os')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true">{info.os}</div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('system.processors')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true">{info.processors}</div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('common.start')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true"> | ||||
|               {info.start ? <FormattedDate date={info.start} /> : t('common.unknown')} | ||||
|             </div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('status.uptime')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true">{info.uptime}</div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('footer.version')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true">{info.version}</div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4"> | ||||
|             <div block="true">{t('common.certificates')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true"> | ||||
|               {info.certificates?.length > 0 ? ( | ||||
|                 <CButton className="ml-0 pl-0 py-0" color="link" onClick={toggleCerts}> | ||||
|                   {t('common.details')} ({info.certificates.length}) | ||||
|                 </CButton> | ||||
|               ) : ( | ||||
|                 <div>{t('common.unknown')}</div> | ||||
|               )} | ||||
|             </div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|         <CRow> | ||||
|           <CCol sm="4" className="pt-1"> | ||||
|             <div block="true">{t('system.reload_subsystems')}:</div> | ||||
|           </CCol> | ||||
|           <CCol> | ||||
|             <div block="true"> | ||||
|               {info.subsystems.length === 0 ? ( | ||||
|                 t('common.unknown') | ||||
|               ) : ( | ||||
|                 <div> | ||||
|                   <div className="float-left" style={{ width: '85%' }}> | ||||
|                     <Select | ||||
|                       isMulti | ||||
|                       closeMenuOnSelect={false} | ||||
|                       name="Subsystems" | ||||
|                       options={info.subsystems.map((sys) => ({ value: sys, label: sys }))} | ||||
|                       onChange={setTypes} | ||||
|                       value={types} | ||||
|                       className="basic-multi-select" | ||||
|                       classNamePrefix="select" | ||||
|                     /> | ||||
|                   </div> | ||||
|                   <div className="float-left text-right" style={{ width: '15%' }}> | ||||
|                     <CPopover content={t('system.reload')}> | ||||
|                       <CButton | ||||
|                         color="primary" | ||||
|                         variant="outline" | ||||
|                         onClick={submit} | ||||
|                         disabled={types.length === 0} | ||||
|                       > | ||||
|                         <CIcon content={cilSync} /> | ||||
|                       </CButton> | ||||
|                     </CPopover> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               )} | ||||
|             </div> | ||||
|           </CCol> | ||||
|         </CRow> | ||||
|       </CCardBody> | ||||
|       <CModal size="lg" show={showCerts} onClose={toggleCerts}> | ||||
|         <CModalHeader className="p-1"> | ||||
|           <CModalTitle className="pl-1 pt-1">{t('common.certificates')}</CModalTitle> | ||||
|           <div className="text-right"> | ||||
|             <CPopover content={t('common.close')}> | ||||
|               <CButton color="primary" variant="outline" className="ml-2" onClick={toggleCerts}> | ||||
|                 <CIcon content={cilX} /> | ||||
|               </CButton> | ||||
|             </CPopover> | ||||
|           </div> | ||||
|         </CModalHeader> | ||||
|         <CModalBody> | ||||
|           <CDataTable | ||||
|             addTableClasses="table-sm" | ||||
|             border | ||||
|             items={info?.certificates.map((cert) => ({ | ||||
|               ...cert, | ||||
|               expiresOn: prettyDate(cert.expiresOn), | ||||
|             }))} | ||||
|           /> | ||||
|         </CModalBody> | ||||
|       </CModal> | ||||
|     </CCard> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| ApiStatusCard.propTypes = { | ||||
|   t: PropTypes.func.isRequired, | ||||
|   info: PropTypes.instanceOf(Object).isRequired, | ||||
|   reload: PropTypes.func.isRequired, | ||||
| }; | ||||
|  | ||||
| export default React.memo(ApiStatusCard); | ||||
| @@ -1,22 +1,135 @@ | ||||
| import React from 'react'; | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { v4 as createUuid } from 'uuid'; | ||||
| import { CRow, CCol } from '@coreui/react'; | ||||
| import { useAuth, useToast } from 'ucentral-libs'; | ||||
| import { secondsToDetailed } from 'utils/helper'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { SystemPage as Page, useToast, useAuth } from 'ucentral-libs'; | ||||
| import axiosInstance from 'utils/axiosInstance'; | ||||
| import ApiStatusCard from './ApiStatusCard'; | ||||
|  | ||||
| const SystemPage = () => { | ||||
|   const { t } = useTranslation(); | ||||
|   const { currentToken, endpoints } = useAuth(); | ||||
|   const { addToast } = useToast(); | ||||
|   const [endpointsInfo, setEndpointsInfo] = useState([]); | ||||
|  | ||||
|   return ( | ||||
|     <Page | ||||
|       t={t} | ||||
|       currentToken={currentToken} | ||||
|       endpoints={endpoints} | ||||
|       addToast={addToast} | ||||
|       axiosInstance={axiosInstance} | ||||
|     /> | ||||
|   ); | ||||
|   const getSystemInfo = async (key, endpoint) => { | ||||
|     let systemInfo = { | ||||
|       title: key, | ||||
|       endpoint, | ||||
|       hostname: t('common.unknown'), | ||||
|       os: t('common.unknown'), | ||||
|       processors: t('common.unknown'), | ||||
|       uptime: t('common.unknown'), | ||||
|       version: t('common.unknown'), | ||||
|       certificates: [], | ||||
|       subsystems: [], | ||||
|     }; | ||||
|  | ||||
|     const options = { | ||||
|       headers: { | ||||
|         Accept: 'application/json', | ||||
|         Authorization: `Bearer ${currentToken}`, | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
|     await axiosInstance | ||||
|       .get(`${endpoint}/api/v1/system?command=info`, options) | ||||
|       .then((newInfo) => { | ||||
|         systemInfo = { ...systemInfo, ...newInfo.data }; | ||||
|         systemInfo.uptime = secondsToDetailed( | ||||
|           newInfo.data.uptime, | ||||
|           t('common.day'), | ||||
|           t('common.days'), | ||||
|           t('common.hour'), | ||||
|           t('common.hours'), | ||||
|           t('common.minute'), | ||||
|           t('common.minutes'), | ||||
|           t('common.second'), | ||||
|           t('common.seconds'), | ||||
|         ); | ||||
|         systemInfo.start = newInfo.data.start; | ||||
|       }) | ||||
|       .catch(() => {}); | ||||
|     await axiosInstance | ||||
|       .post(`${endpoint}/api/v1/system`, { command: 'getsubsystemnames' }, options) | ||||
|       .then((newSubs) => { | ||||
|         systemInfo.subsystems = newSubs.data.list.sort((a, b) => { | ||||
|           if (a < b) return -1; | ||||
|           if (a > b) return 1; | ||||
|           return 0; | ||||
|         }); | ||||
|       }) | ||||
|       .catch(() => {}); | ||||
|  | ||||
|     return systemInfo; | ||||
|   }; | ||||
|  | ||||
|   const reload = (subsystems, endpoint) => { | ||||
|     const options = { | ||||
|       headers: { | ||||
|         Accept: 'application/json', | ||||
|         Authorization: `Bearer ${currentToken}`, | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
|     const parameters = { | ||||
|       command: 'reload', | ||||
|       subsystems, | ||||
|     }; | ||||
|  | ||||
|     axiosInstance | ||||
|       .post(`${endpoint}/api/v1/system?command=info`, parameters, options) | ||||
|       .then(() => { | ||||
|         addToast({ | ||||
|           title: t('common.success'), | ||||
|           body: t('system.success_reload'), | ||||
|           color: 'success', | ||||
|           autohide: true, | ||||
|         }); | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         addToast({ | ||||
|           title: t('common.error'), | ||||
|           body: t('system.error_reloading', { error: e.response?.data?.ErrorDescription }), | ||||
|           color: 'danger', | ||||
|           autohide: true, | ||||
|         }); | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   const getAllInfo = async () => { | ||||
|     const promises = []; | ||||
|  | ||||
|     for (const [key, value] of Object.entries(endpoints)) { | ||||
|       promises.push(getSystemInfo(key, value)); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const results = await Promise.all(promises); | ||||
|       setEndpointsInfo(results); | ||||
|     } catch (e) { | ||||
|       addToast({ | ||||
|         title: t('common.error'), | ||||
|         body: t('system.error_fetching'), | ||||
|         color: 'danger', | ||||
|         autohide: true, | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     getAllInfo(); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <CRow> | ||||
|       {endpointsInfo.map((info) => ( | ||||
|         <CCol sm="12" lg="6" xxl="4" key={createUuid()}> | ||||
|           <ApiStatusCard t={t} info={info} reload={reload} /> | ||||
|         </CCol> | ||||
|       ))} | ||||
|     </CRow> | ||||
|   ); | ||||
| }; | ||||
| export default SystemPage; | ||||
|   | ||||
| @@ -13,7 +13,7 @@ const Routes = () => { | ||||
|       path="/" | ||||
|       name="Devices" | ||||
|       render={(props) => | ||||
|         currentToken !== '' && Object.keys(endpoints).length !== 0 ? ( | ||||
|         currentToken !== '' && endpoints && Object.keys(endpoints).length !== 0 ? ( | ||||
|           <TheLayout {...props} /> | ||||
|         ) : ( | ||||
|           <ToastProvider> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import * as axios from 'axios'; | ||||
| import axiosRetry from 'axios-retry'; | ||||
| import { LOGOUT_ON_SEC_ERROR_CODES } from 'constants'; | ||||
|  | ||||
| const axiosInstance = axios.create(); | ||||
|  | ||||
| @@ -27,7 +28,7 @@ axiosInstance.interceptors.response.use( | ||||
|           retries += 1; | ||||
|           localStorage.setItem('sec_retries', retries); | ||||
|         } | ||||
|         if (error.response.data?.ErrorCode === 9) { | ||||
|         if (LOGOUT_ON_SEC_ERROR_CODES.includes(error.response.data?.ErrorCode)) { | ||||
|           localStorage.removeItem('access_token'); | ||||
|           localStorage.removeItem('gateway_endpoints'); | ||||
|           sessionStorage.clear(); | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| export const cleanTimestamp = (timestamp) => timestamp.replace('T', ' ').replace('Z', ' '); | ||||
|  | ||||
| export const cleanBytesString = (bytes, decimals = 2) => { | ||||
|   const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | ||||
|   if (!bytes || bytes === 0) { | ||||
| @@ -132,6 +130,25 @@ export const compactSecondsToDetailed = (seconds, dayLabel, daysLabel, secondsLa | ||||
|   return finalString; | ||||
| }; | ||||
|  | ||||
| export const extraCompactSecondsToDetailed = (seconds) => { | ||||
|   let secondsLeft = seconds; | ||||
|   const days = Math.floor(secondsLeft / (3600 * 24)); | ||||
|   secondsLeft -= days * (3600 * 24); | ||||
|   const hours = Math.floor(secondsLeft / 3600); | ||||
|   secondsLeft -= hours * 3600; | ||||
|   const minutes = Math.floor(secondsLeft / 60); | ||||
|   secondsLeft -= minutes * 60; | ||||
|  | ||||
|   let finalString = ''; | ||||
|  | ||||
|   finalString = `${finalString}${prettyNumber(days)}:`; | ||||
|   finalString = `${finalString}${prettyNumber(hours)}:`; | ||||
|   finalString = `${finalString}${prettyNumber(minutes)}:`; | ||||
|   finalString = `${finalString}${prettyNumber(secondsLeft)}`; | ||||
|  | ||||
|   return finalString; | ||||
| }; | ||||
|  | ||||
| export const validateEmail = (email) => { | ||||
|   const regex = /\S+@\S+\.\S+/; | ||||
|   return regex.test(email); | ||||
| @@ -143,3 +160,25 @@ export const testRegex = (value, regexString) => { | ||||
| }; | ||||
|  | ||||
| export const datesSameDay = (first, second) => first.getDate() === second.getDate(); | ||||
|  | ||||
| const units = { | ||||
|   year: 24 * 60 * 60 * 1000 * 365, | ||||
|   month: (24 * 60 * 60 * 1000 * 365) / 12, | ||||
|   day: 24 * 60 * 60 * 1000, | ||||
|   hour: 60 * 60 * 1000, | ||||
|   minute: 60 * 1000, | ||||
|   second: 1000, | ||||
| }; | ||||
|  | ||||
| const rtf = new Intl.RelativeTimeFormat('en', { localeMatcher: 'best fit', style: 'long' }); | ||||
| export const formatDaysAgo = (d1, d2 = new Date()) => { | ||||
|   const convertedTimestamp = unixToDateString(d1); | ||||
|   const date = new Date(convertedTimestamp); | ||||
|   const elapsed = date - d2; | ||||
|  | ||||
|   for (const [key] of Object.entries(units)) | ||||
|     if (Math.abs(elapsed) > units[key] || key === 'second') | ||||
|       return rtf.format(Math.round(elapsed / units[key]), key); | ||||
|  | ||||
|   return prettyDate(date); | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user