[WIFI-12885] New Monitoring and Default Firmware pages

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-08-25 16:55:31 +02:00
parent 30d882e1c0
commit be52ed7d44
32 changed files with 2408 additions and 104 deletions

505
package-lock.json generated
View File

@@ -1,16 +1,18 @@
{
"name": "ucentral-client",
"version": "2.11.0(1)",
"version": "2.11.0(7)",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.11.0(1)",
"version": "2.11.0(7)",
"license": "ISC",
"dependencies": {
"@chakra-ui/anatomy": "^2.1.1",
"@chakra-ui/icons": "^2.0.18",
"@chakra-ui/react": "^2.3.6",
"@chakra-ui/styled-system": "^2.9.0",
"@chakra-ui/theme-tools": "^2.0.12",
"@chakra-ui/utils": "^2.0.14",
"@emotion/react": "^11.10.6",
@@ -52,7 +54,7 @@
"react-virtualized-auto-sizer": "^1.0.15",
"react-window": "^1.8.9",
"source-map-explorer": "^2.5.3",
"typescript": "^4.8.4",
"typescript": "^5.0.4",
"uuid": "^9.0.0",
"vite": "^4.2.1",
"yup": "^0.32.11",
@@ -1760,8 +1762,9 @@
}
},
"node_modules/@chakra-ui/anatomy": {
"version": "2.0.7",
"license": "MIT"
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.2.0.tgz",
"integrity": "sha512-cD8Ms5C8+dFda0LrORMdxiFhAZwOIY1BSlCadz6/mHUIgNdQy13AHPrXiq6qWdMslqVHq10k5zH7xMPLt6kjFg=="
},
"node_modules/@chakra-ui/avatar": {
"version": "2.2.0",
@@ -2533,6 +2536,15 @@
"lodash.mergewith": "4.6.2"
}
},
"node_modules/@chakra-ui/react/node_modules/@chakra-ui/styled-system": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.3.4.tgz",
"integrity": "sha512-Lozbedu+GBj4EbHB/eGv475SFDLApsIEN9gNKiZJBJAE1HIhHn3Seh1iZQSrHC/Beq+D5cQq3Z+yPn3bXtFU7w==",
"dependencies": {
"csstype": "^3.0.11",
"lodash.mergewith": "4.6.2"
}
},
"node_modules/@chakra-ui/react/node_modules/@chakra-ui/utils": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-2.0.11.tgz",
@@ -2612,13 +2624,20 @@
}
},
"node_modules/@chakra-ui/styled-system": {
"version": "2.3.4",
"license": "MIT",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.9.1.tgz",
"integrity": "sha512-jhYKBLxwOPi9/bQt9kqV3ELa/4CjmNNruTyXlPp5M0v0+pDMUngPp48mVLoskm9RKZGE0h1qpvj/jZ3K7c7t8w==",
"dependencies": {
"@chakra-ui/shared-utils": "2.0.5",
"csstype": "^3.0.11",
"lodash.mergewith": "4.6.2"
}
},
"node_modules/@chakra-ui/styled-system/node_modules/@chakra-ui/shared-utils": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz",
"integrity": "sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q=="
},
"node_modules/@chakra-ui/switch": {
"version": "2.0.14",
"license": "MIT",
@@ -2648,6 +2667,15 @@
"react": ">=18"
}
},
"node_modules/@chakra-ui/system/node_modules/@chakra-ui/styled-system": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.3.4.tgz",
"integrity": "sha512-Lozbedu+GBj4EbHB/eGv475SFDLApsIEN9gNKiZJBJAE1HIhHn3Seh1iZQSrHC/Beq+D5cQq3Z+yPn3bXtFU7w==",
"dependencies": {
"csstype": "^3.0.11",
"lodash.mergewith": "4.6.2"
}
},
"node_modules/@chakra-ui/system/node_modules/@chakra-ui/utils": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-2.0.11.tgz",
@@ -2738,6 +2766,11 @@
"@chakra-ui/styled-system": ">=2.0.0"
}
},
"node_modules/@chakra-ui/theme-tools/node_modules/@chakra-ui/anatomy": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.0.7.tgz",
"integrity": "sha512-vzcB2gcsGCxhrKbldQQV6LnBPys4eSSsH2UA2mLsT+J3WlXw0aodZw0eE/nH7yLxe4zaQ4Gnc0KjkFW4EWNKSg=="
},
"node_modules/@chakra-ui/theme-utils": {
"version": "2.0.1",
"license": "MIT",
@@ -2747,6 +2780,20 @@
"lodash.mergewith": "4.6.2"
}
},
"node_modules/@chakra-ui/theme-utils/node_modules/@chakra-ui/styled-system": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.3.4.tgz",
"integrity": "sha512-Lozbedu+GBj4EbHB/eGv475SFDLApsIEN9gNKiZJBJAE1HIhHn3Seh1iZQSrHC/Beq+D5cQq3Z+yPn3bXtFU7w==",
"dependencies": {
"csstype": "^3.0.11",
"lodash.mergewith": "4.6.2"
}
},
"node_modules/@chakra-ui/theme/node_modules/@chakra-ui/anatomy": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.0.7.tgz",
"integrity": "sha512-vzcB2gcsGCxhrKbldQQV6LnBPys4eSSsH2UA2mLsT+J3WlXw0aodZw0eE/nH7yLxe4zaQ4Gnc0KjkFW4EWNKSg=="
},
"node_modules/@chakra-ui/toast": {
"version": "4.0.0",
"license": "MIT",
@@ -2766,6 +2813,15 @@
"react-dom": ">=18"
}
},
"node_modules/@chakra-ui/toast/node_modules/@chakra-ui/styled-system": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.3.4.tgz",
"integrity": "sha512-Lozbedu+GBj4EbHB/eGv475SFDLApsIEN9gNKiZJBJAE1HIhHn3Seh1iZQSrHC/Beq+D5cQq3Z+yPn3bXtFU7w==",
"dependencies": {
"csstype": "^3.0.11",
"lodash.mergewith": "4.6.2"
}
},
"node_modules/@chakra-ui/tooltip": {
"version": "2.2.0",
"license": "MIT",
@@ -2954,10 +3010,55 @@
"version": "0.3.0",
"license": "MIT"
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.17.17",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz",
"integrity": "sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
@@ -2969,6 +3070,276 @@
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -3917,9 +4288,10 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
"version": "7.3.8",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -4037,9 +4409,10 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.3.8",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -4076,9 +4449,10 @@
}
},
"node_modules/@typescript-eslint/utils/node_modules/semver": {
"version": "7.3.8",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -5138,9 +5512,9 @@
}
},
"node_modules/esbuild": {
"version": "0.17.17",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.17.tgz",
"integrity": "sha512-/jUywtAymR8jR4qsa2RujlAF7Krpt5VWi72Q2yuLD4e/hvtNcFQ0I1j8m/bxq238pf3/0KO5yuXNpuLx8BE1KA==",
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@@ -5149,28 +5523,28 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.17.17",
"@esbuild/android-arm64": "0.17.17",
"@esbuild/android-x64": "0.17.17",
"@esbuild/darwin-arm64": "0.17.17",
"@esbuild/darwin-x64": "0.17.17",
"@esbuild/freebsd-arm64": "0.17.17",
"@esbuild/freebsd-x64": "0.17.17",
"@esbuild/linux-arm": "0.17.17",
"@esbuild/linux-arm64": "0.17.17",
"@esbuild/linux-ia32": "0.17.17",
"@esbuild/linux-loong64": "0.17.17",
"@esbuild/linux-mips64el": "0.17.17",
"@esbuild/linux-ppc64": "0.17.17",
"@esbuild/linux-riscv64": "0.17.17",
"@esbuild/linux-s390x": "0.17.17",
"@esbuild/linux-x64": "0.17.17",
"@esbuild/netbsd-x64": "0.17.17",
"@esbuild/openbsd-x64": "0.17.17",
"@esbuild/sunos-x64": "0.17.17",
"@esbuild/win32-arm64": "0.17.17",
"@esbuild/win32-ia32": "0.17.17",
"@esbuild/win32-x64": "0.17.17"
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/escalade": {
@@ -8064,9 +8438,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.22",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.22.tgz",
"integrity": "sha512-XseknLAfRHzVWjCEtdviapiBtfLdgyzExD50Rg2ePaucEesyh8Wv4VPdW0nbyDa1ydbrAxV19jvMT4+LFmcNUA==",
"version": "8.4.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
"integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==",
"funding": [
{
"type": "opencollective",
@@ -8808,9 +9182,9 @@
}
},
"node_modules/rollup": {
"version": "3.21.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.3.tgz",
"integrity": "sha512-VnPfEG51nIv2xPLnZaekkuN06q9ZbnyDcLkaBdJa/W7UddyhOfMP2yOPziYQfeY7k++fZM8FdQIummFN5y14kA==",
"version": "3.28.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz",
"integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==",
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -8894,9 +9268,10 @@
}
},
"node_modules/semver": {
"version": "6.3.0",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@@ -9541,14 +9916,15 @@
}
},
"node_modules/typescript": {
"version": "4.8.4",
"license": "Apache-2.0",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
@@ -9744,14 +10120,13 @@
}
},
"node_modules/vite": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz",
"integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==",
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"dependencies": {
"esbuild": "^0.17.5",
"postcss": "^8.4.21",
"resolve": "^1.22.1",
"rollup": "^3.18.0"
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
@@ -9759,12 +10134,16 @@
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
@@ -9777,6 +10156,9 @@
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
@@ -9928,9 +10310,10 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.11.0(1)",
"version": "2.11.0(7)",
"description": "",
"private": true,
"main": "index.tsx",
@@ -15,8 +15,10 @@
"author": "",
"license": "ISC",
"dependencies": {
"@chakra-ui/anatomy": "^2.1.1",
"@chakra-ui/icons": "^2.0.18",
"@chakra-ui/react": "^2.3.6",
"@chakra-ui/styled-system": "^2.9.0",
"@chakra-ui/theme-tools": "^2.0.12",
"@chakra-ui/utils": "^2.0.14",
"@emotion/react": "^11.10.6",
@@ -58,7 +60,7 @@
"react-virtualized-auto-sizer": "^1.0.15",
"react-window": "^1.8.9",
"source-map-explorer": "^2.5.3",
"typescript": "^4.8.4",
"typescript": "^5.0.4",
"uuid": "^9.0.0",
"vite": "^4.2.1",
"yup": "^0.32.11",

View File

@@ -223,6 +223,7 @@
"day": "Tag",
"days": "Tage",
"default": "Standard",
"defaults": "Standardeinstellungen",
"description": "Beschreibung",
"details": "Einzelheiten",
"device_details": "Gerätedetails",
@@ -702,11 +703,32 @@
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
},
"firmware": {
"confirm_default_data": "Bitte bestätigen Sie die untenstehenden Informationen und klicken Sie auf „Bestätigen“, sobald Sie bereit sind, den Vorgang zu starten",
"create_success": "Neue Standard-Firmware-Einstellungen erstellt!",
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
"default_created_error_one": "{{count}} Fehler beim Versuch, eine neue Einstellung zu erstellen",
"default_created_error_other": "{{count}} Fehler beim Versuch, eine neue Einstellung zu erstellen",
"default_created_one": "{{count}} Standard-Firmware-Einstellung erstellt",
"default_created_other": "{{count}} Standard-Firmware-Einstellungen erstellt",
"default_found_one": "Für den Gerätetyp {{count}} wurde eine gültige Revision gefunden",
"default_found_other": "Gültige Revisionen für {{count}} Gerätetypen gefunden",
"default_mass_delete_success_one": " {{count}} Standard-Firmware-Einstellung gelöscht!",
"default_mass_delete_success_other": " {{count}} Standard-Firmware-Einstellungen gelöscht!",
"default_not_found_one": "Keine gültigen Firmware-Versionen für den Gerätetyp {{count}} ",
"default_not_found_other": "Keine gültigen Firmware-Versionen für {{count}} Gerätetypen",
"default_title": "",
"default_update_success": "Standard-Firmware für {{deviceType}}aktualisiert!",
"delete_success": "Standard-Firmware-Einstellung gelöscht!",
"edit_default_title": "Dies ist die aktuelle Firmware, die als Mindestversion für neue APs vom Typ {{deviceType}}verwendet wird. Wenn ein neuer {{deviceType}} AP eine Verbindung zum Gateway herstellt, wird er automatisch auf diese Version aktualisiert.",
"fetching_defaults": "Alle verfügbaren Firmware für ausgewählte Gerätetypen werden abgerufen...",
"last_db_update_modal": "Firmware-Datenbank",
"last_db_update_title": "Datenbank",
"one": "Firmware",
"select_default_device_types": "Bitte wählen Sie alle Gerätetypen aus, auf die Sie diese neue Standard-Firmware-Regel anwenden möchten. Wenn Sie den gewünschten Gerätetyp nicht finden können, bedeutet dies, dass bereits eine Regel angewendet wurde.",
"select_default_revision": "Sie können jetzt die Mindestversion auswählen, auf die Ihre Gerätetypen abzielen sollen",
"start_db_update": "Datenbankaktualisierung starten",
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern",
"update_success": "Standard-Firmware-Informationen gespeichert!"
},
"footer": {
"powered_by": "Unterstützt von",
@@ -1008,6 +1030,9 @@
"current_live_devices": "Aktuelle Live-Geräte",
"currently_running_one": "Derzeit wird {{count}} Simulation ausgeführt",
"currently_running_other": "Derzeit laufen {{count}} Simulationen",
"delete_devices_confirm": "Sind Sie sicher, dass Sie alle Geräte und deren Statistiken vom Gateway entfernen möchten? Diese Aktion ist nicht rückgängig zu machen",
"delete_devices_loading": "Dieser Vorgang kann bis zu 5 Minuten dauern",
"delete_simulation_devices": "Geräte löschen",
"delete_success": "Gelöschte Simulation!",
"duration": "Dauer",
"error_devices": "Fehler Geräte",

View File

@@ -223,6 +223,7 @@
"day": "Day",
"days": "Days",
"default": "Default",
"defaults": "Defaults",
"description": "Description",
"details": "Details",
"device_details": "Device Details",
@@ -702,11 +703,32 @@
"venues_under_root": "Venues cannot be created directly under the root entity"
},
"firmware": {
"confirm_default_data": "Please confirm the information below and click 'Confirm' once you are ready to start the process",
"create_success": "Created new default firmware settings!",
"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes",
"default_created_error_one": "{{count}} error while trying to create new setting",
"default_created_error_other": "{{count}} errors while trying to create new setting",
"default_created_one": "{{count}} default firmware setting created",
"default_created_other": "{{count}} default firmware settings created",
"default_found_one": "Found valid revision for {{count}} device type",
"default_found_other": "Found valid revisions for {{count}} device types",
"default_mass_delete_success_one": "Deleted {{count}} default firmware setting!",
"default_mass_delete_success_other": "Deleted {{count}} default firmware settings!",
"default_not_found_one": "No valid firmware versions for {{count}} device type",
"default_not_found_other": "No valid firmware versions for {{count}} device types",
"default_title": "Default Firmware",
"default_update_success": "Updated default firmware for {{deviceType}}!",
"delete_success": "Deleted default firmware setting!",
"edit_default_title": "This is the current firmware that is used as the minimum version for new APs of type {{deviceType}}. If a new {{deviceType}} AP connects to the gateway, it will be automatically upgraded to this version.",
"fetching_defaults": "Fetching all available firmware for selected device types...",
"last_db_update_modal": "Firmware Database",
"last_db_update_title": "Database",
"one": "Firmware",
"select_default_device_types": "Please select all device types that you want to target with this new default firmware rule. If you cannot find your desired device type, it means they already have an applied rule.",
"select_default_revision": "You can now select the minimum revision you want your device types to target",
"start_db_update": "Start Database Update",
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete"
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete",
"update_success": "Saved default firmware information!"
},
"footer": {
"powered_by": "Powered By",
@@ -1008,6 +1030,9 @@
"current_live_devices": "Current Live Devices",
"currently_running_one": "There is currently {{count}} simulation running",
"currently_running_other": "There are currently {{count}} simulations running",
"delete_devices_confirm": "Are you sure you want to remove all devices and their statistics from the gateway? This action is not reversible",
"delete_devices_loading": "This process may take up to 5 minutes",
"delete_simulation_devices": "Delete Devices",
"delete_success": "Deleted Simulation!",
"duration": "Duration",
"error_devices": "Error Devices",

View File

@@ -223,6 +223,7 @@
"day": "Día",
"days": "días",
"default": "Defecto",
"defaults": "Valores predeterminados",
"description": "Descripción",
"details": "Detalles",
"device_details": "Detalles del dispositivo",
@@ -702,11 +703,32 @@
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
},
"firmware": {
"confirm_default_data": "Confirme la información a continuación y haga clic en 'Confirmar' una vez que esté listo para comenzar el proceso",
"create_success": "¡Se crearon nuevas configuraciones de firmware predeterminadas!",
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
"default_created_error_one": "{{count}} error al intentar crear una nueva configuración",
"default_created_error_other": "{{count}} errores al intentar crear una nueva configuración",
"default_created_one": "{{count}} configuración de firmware predeterminada creada",
"default_created_other": "{{count}} ajustes de firmware predeterminados creados",
"default_found_one": "Se encontró una revisión válida para el tipo de dispositivo {{count}} ",
"default_found_other": "Se encontraron revisiones válidas para {{count}} tipos de dispositivos",
"default_mass_delete_success_one": "¡Se eliminó {{count}} configuración de firmware predeterminada!",
"default_mass_delete_success_other": "¡Se eliminaron {{count}} configuraciones de firmware predeterminadas!",
"default_not_found_one": "No hay versiones de firmware válidas para el tipo de dispositivo {{count}} ",
"default_not_found_other": "No hay versiones de firmware válidas para {{count}} tipos de dispositivos",
"default_title": "",
"default_update_success": "¡Firmware predeterminado actualizado para {{deviceType}}!",
"delete_success": "¡Configuración de firmware predeterminada eliminada!",
"edit_default_title": "Este es el firmware actual que se utiliza como versión mínima para los nuevos AP de tipo {{deviceType}}. Si un nuevo AP {{deviceType}} se conecta a la puerta de enlace, se actualizará automáticamente a esta versión.",
"fetching_defaults": "Obteniendo todo el firmware disponible para los tipos de dispositivos seleccionados...",
"last_db_update_modal": "Base de datos de firmware",
"last_db_update_title": "Base de datos",
"one": "Firmware",
"select_default_device_types": "Seleccione todos los tipos de dispositivos a los que desea apuntar con esta nueva regla de firmware predeterminada. Si no puede encontrar el tipo de dispositivo deseado, significa que ya tienen una regla aplicada.",
"select_default_revision": "Ahora puede seleccionar la revisión mínima a la que desea que se dirijan sus tipos de dispositivos",
"start_db_update": "Iniciar actualización de la base de datos",
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse",
"update_success": "¡Información de firmware predeterminada guardada!"
},
"footer": {
"powered_by": "energizado por",
@@ -1008,6 +1030,9 @@
"current_live_devices": "Dispositivos activos actuales",
"currently_running_one": "Actualmente hay {{count}} simulación en ejecución",
"currently_running_other": "Actualmente hay {{count}} simulaciones ejecutándose",
"delete_devices_confirm": "¿Está seguro de que desea eliminar todos los dispositivos y sus estadísticas de la puerta de enlace? Esta acción no es reversible",
"delete_devices_loading": "Este proceso puede tardar hasta 5 minutos.",
"delete_simulation_devices": "BORRAR DISPOSITIVOS",
"delete_success": "¡Simulación eliminada!",
"duration": "Duración",
"error_devices": "Dispositivos de error",

View File

@@ -223,6 +223,7 @@
"day": "journée",
"days": "Journées",
"default": "Défaut",
"defaults": "Valeurs par défaut",
"description": "La description",
"details": "Détails",
"device_details": "Détails de l'appareil",
@@ -702,11 +703,32 @@
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
},
"firmware": {
"confirm_default_data": "Veuillez confirmer les informations ci-dessous et cliquez sur \"Confirmer\" une fois que vous êtes prêt à démarrer le processus",
"create_success": "Création de nouveaux paramètres de firmware par défaut !",
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
"default_created_error_one": "{{count}} erreur lors de la tentative de création d'un nouveau paramètre",
"default_created_error_other": "{{count}} erreurs lors de la tentative de création d'un nouveau paramètre",
"default_created_one": "{{count}} paramètre de micrologiciel par défaut créé",
"default_created_other": "{{count}} paramètres de micrologiciel par défaut créés",
"default_found_one": "Révision valide trouvée pour le type d'appareil {{count}} ",
"default_found_other": "Révisions valides trouvées pour {{count}} types d'appareils",
"default_mass_delete_success_one": "Paramètre de micrologiciel par défaut {{count}} supprimé !",
"default_mass_delete_success_other": " {{count}} paramètres de micrologiciel par défaut supprimés !",
"default_not_found_one": "Aucune version de micrologiciel valide pour le type d'appareil {{count}} ",
"default_not_found_other": "Aucune version de micrologiciel valide pour {{count}} types d'appareils",
"default_title": "",
"default_update_success": "Firmware par défaut mis à jour pour {{deviceType}} !",
"delete_success": "Paramètre de micrologiciel par défaut supprimé !",
"edit_default_title": "Il s'agit du micrologiciel actuel utilisé comme version minimale pour les nouveaux points d'accès de type {{deviceType}}. Si un nouveau point d'accès {{deviceType}} se connecte à la passerelle, il sera automatiquement mis à niveau vers cette version.",
"fetching_defaults": "Récupération de tous les micrologiciels disponibles pour les types d'appareils sélectionnés...",
"last_db_update_modal": "Base de données du micrologiciel",
"last_db_update_title": "Base de données",
"one": "Micrologiciel",
"select_default_device_types": "Veuillez sélectionner tous les types d'appareils que vous souhaitez cibler avec cette nouvelle règle de micrologiciel par défaut. Si vous ne trouvez pas le type d'appareil souhaité, cela signifie qu'une règle est déjà appliquée.",
"select_default_revision": "Vous pouvez maintenant sélectionner la révision minimale que vous souhaitez que vos types d'appareils ciblent",
"start_db_update": "Démarrer la mise à jour de la base de données",
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes",
"update_success": "Informations sur le micrologiciel par défaut enregistrées !"
},
"footer": {
"powered_by": "Alimenté par",
@@ -1008,6 +1030,9 @@
"current_live_devices": "Appareils en direct actuels",
"currently_running_one": "Il y a actuellement {{count}} simulation en cours",
"currently_running_other": "Il y a actuellement {{count}} simulations en cours d'exécution",
"delete_devices_confirm": "Voulez-vous vraiment supprimer tous les appareils et leurs statistiques de la passerelle ? Cette action n'est pas réversible",
"delete_devices_loading": "Ce processus peut prendre jusqu'à 5 minutes",
"delete_simulation_devices": "Supprimer des appareils",
"delete_success": "Simulation supprimée !",
"duration": "Durée",
"error_devices": "Périphériques d'erreur",

View File

@@ -223,6 +223,7 @@
"day": "Dia",
"days": "Dias",
"default": "Padrão",
"defaults": "Predefinições",
"description": "Descrição",
"details": "Detalhes",
"device_details": "Detalhes do dispositivo",
@@ -702,11 +703,32 @@
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
},
"firmware": {
"confirm_default_data": "Confirme as informações abaixo e clique em 'Confirmar' quando estiver pronto para iniciar o processo",
"create_success": "Criou novas configurações de firmware padrão!",
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
"default_created_error_one": "{{count}} erro ao tentar criar uma nova configuração",
"default_created_error_other": "{{count}} erros ao tentar criar uma nova configuração",
"default_created_one": "{{count}} configuração de firmware padrão criada",
"default_created_other": "{{count}} configurações de firmware padrão criadas",
"default_found_one": "Revisão válida encontrada para {{count}} tipo de dispositivo",
"default_found_other": "Foram encontradas revisões válidas para {{count}} tipos de dispositivo",
"default_mass_delete_success_one": "Configuração de firmware padrão {{count}} excluída!",
"default_mass_delete_success_other": "Excluídas {{count}} configurações de firmware padrão!",
"default_not_found_one": "Nenhuma versão de firmware válida para {{count}} tipo de dispositivo",
"default_not_found_other": "Nenhuma versão de firmware válida para {{count}} tipos de dispositivo",
"default_title": "",
"default_update_success": "Firmware padrão atualizado para {{deviceType}}!",
"delete_success": "Configuração de firmware padrão excluída!",
"edit_default_title": "Este é o firmware atual usado como versão mínima para novos APs do tipo {{deviceType}}. Se um novo AP {{deviceType}} se conectar ao gateway, ele será atualizado automaticamente para esta versão.",
"fetching_defaults": "Buscando todo o firmware disponível para os tipos de dispositivos selecionados...",
"last_db_update_modal": "banco de dados de firmware",
"last_db_update_title": "base de dados",
"one": "Firmware",
"select_default_device_types": "Selecione todos os tipos de dispositivos que deseja segmentar com esta nova regra de firmware padrão. Se você não conseguir encontrar o tipo de dispositivo desejado, significa que eles já têm uma regra aplicada.",
"select_default_revision": "Agora você pode selecionar a revisão mínima para a qual deseja que seus tipos de dispositivo sejam direcionados",
"start_db_update": "Iniciar atualização do banco de dados",
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída",
"update_success": "Informações de firmware padrão salvas!"
},
"footer": {
"powered_by": "Distribuído por",
@@ -1008,6 +1030,9 @@
"current_live_devices": "Dispositivos ativos atuais",
"currently_running_one": "Atualmente, há {{count}} simulação em execução",
"currently_running_other": "Existem atualmente {{count}} simulações em execução",
"delete_devices_confirm": "Tem certeza de que deseja remover todos os dispositivos e suas estatísticas do gateway? Esta ação não é reversível",
"delete_devices_loading": "Este processo pode levar até 5 minutos",
"delete_simulation_devices": "Apagar dispositivos",
"delete_success": "Simulação excluída!",
"duration": "Duração",
"error_devices": "Dispositivos de Erro",

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
import { Pencil, X } from '@phosphor-icons/react';
import { Pencil, Stop } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { ConfirmCloseAlertModal } from '../../Modals/ConfirmCloseAlert';
@@ -48,7 +48,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
colorScheme="gray"
type="button"
onClick={toggle}
rightIcon={isEditing ? <X size={20} /> : <Pencil size={20} />}
rightIcon={isEditing ? <Stop size={20} /> : <Pencil size={20} />}
isLoading={isLoading}
isDisabled={isDisabled}
ml={ml}
@@ -68,7 +68,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
colorScheme="gray"
type="button"
onClick={toggle}
icon={isEditing ? <X size={20} /> : <Pencil size={20} />}
icon={isEditing ? <Stop size={20} /> : <Pencil size={20} />}
isLoading={isLoading}
isDisabled={isDisabled}
ml={ml}

View File

@@ -149,7 +149,7 @@ export const DataGrid = <TValue extends object>({
...tableOptions,
});
if (isLoading && data.length === 0) {
if (isLoading && !options.showAsCard && data.length === 0) {
return (
<Center>
<Spinner size="xl" />

View File

@@ -24,6 +24,11 @@ const chakraStyles: (
placeholder: (provided) => ({
...provided,
lineHeight: '1',
pointerEvents: 'none',
userSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
msUserSelect: 'none',
}),
container: (provided) => ({
...provided,
@@ -31,6 +36,10 @@ const chakraStyles: (
backgroundColor: colorMode === 'light' ? 'white' : 'gray.600',
borderRadius: '15px',
}),
input: (provided) => ({
...provided,
gridArea: '1 / 2 / 4 / 4 !important',
}),
});
interface SearchOption extends OptionBase {

View File

@@ -0,0 +1,103 @@
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { axiosGw } from 'constants/axiosInstances';
import { AtLeast } from 'models/General';
export type DefaultFirmware = {
deviceType: string;
description: string;
uri: string;
revision: string;
imageCreationDate: number;
created: number;
lastModified: number;
};
export type DefaultFirmwareResponse = {
firmwares: DefaultFirmware[];
};
const getDefaultFirmware = async () =>
axiosGw.get('default_firmwares').then((response) => response.data as DefaultFirmwareResponse);
export const useGetDefaultFirmware = () =>
useQuery(['default_firmwares', 'all'], getDefaultFirmware, {
staleTime: 1000 * 60,
});
const getDefaultFirmwareByDeviceType = async (context: QueryFunctionContext<[string, string]>) =>
axiosGw.get(`default_firmware/${context.queryKey[1]}`).then((response) => response.data as DefaultFirmware);
export const useGetDefaultFirmwareByDeviceType = (deviceType: string) =>
useQuery(['default_firmwares', deviceType], getDefaultFirmwareByDeviceType);
export const createDefaultFirmware = async (defaultFirmware: DefaultFirmware) =>
axiosGw
.post(`default_firmware/${defaultFirmware.deviceType}`, defaultFirmware)
.then((response) => response.data as DefaultFirmware);
export const useCreateDefaultFirmware = () => {
const queryClient = useQueryClient();
return useMutation(createDefaultFirmware, {
onSuccess: () => {
queryClient.invalidateQueries(['default_firmwares']);
},
});
};
const deleteDefaultFirmware = async (deviceType: string) =>
axiosGw.delete(`default_firmware/${deviceType}`).then((response) => response.data as DefaultFirmware);
export const useDeleteDefaultFirmware = () => {
const queryClient = useQueryClient();
return useMutation(deleteDefaultFirmware, {
onSuccess: () => {
queryClient.invalidateQueries(['default_firmwares']);
},
});
};
const deleteBatchDefaultFirmware = async (deviceTypes: string[]) => {
const promises = deviceTypes.map((deviceType) =>
axiosGw
.delete(`default_firmware/${deviceType}`)
.then(() => ({
deviceType,
success: true,
}))
.catch((e) => ({
deviceType,
error: axios.isAxiosError(e) ? e.response?.data.ErrorDescription : e,
})),
);
const res = await Promise.allSettled(promises);
return res;
};
export const useDeleteBatchDefaultFirmware = () => {
const queryClient = useQueryClient();
return useMutation(deleteBatchDefaultFirmware, {
onSuccess: () => {
queryClient.invalidateQueries(['default_firmwares']);
},
});
};
export const updateDefaultFirmware = async (defaultFirmware: AtLeast<DefaultFirmware, 'deviceType'>) =>
axiosGw
.put(`default_firmware/${defaultFirmware.deviceType}`, defaultFirmware)
.then((response) => response.data as DefaultFirmware);
export const useUpdateDefaultFirmware = () => {
const queryClient = useQueryClient();
return useMutation(updateDefaultFirmware, {
onSuccess: () => {
queryClient.invalidateQueries(['default_firmwares']);
},
});
};

View File

@@ -12,7 +12,7 @@ const getAvailableFirmwareBatch = async (deviceType: string, limit: number, offs
.get(`firmwares?deviceType=${deviceType}&limit=${limit}&offset=${offset}`)
.then(({ data }: { data: { firmwares: Firmware[] } }) => data);
const getAllAvailableFirmware = async (deviceType: string) => {
export const getAllAvailableFirmware = async (deviceType: string) => {
const limit = 500;
let offset = 0;
let data: { firmwares: Firmware[] } = { firmwares: [] };

View File

@@ -199,3 +199,36 @@ export const useUpdateSystemLogLevels = ({ endpoint, token }: { endpoint: string
},
});
};
export type SystemResources = {
currRealMem: number;
currVirtMem: number;
numberOfFileDescriptors: number;
peakRealMem: number;
peakVirtMem: number;
};
export const useGetSystemResources = ({
endpoint,
token,
onSuccess,
}: {
endpoint: string;
token: string;
onSuccess?: (data: SystemResources) => void;
}) =>
useQuery(
['systemResources', endpoint],
() =>
axiosInstance
.get(`${endpoint}/api/v1/system?command=resources`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(({ data }: { data: SystemResources }) => data),
{
refetchInterval: 5 * 1000,
onSuccess,
},
);

View File

@@ -17,6 +17,7 @@ import {
Tooltip,
useBreakpoint,
Portal,
useDisclosure,
} from '@chakra-ui/react';
import { ArrowCircleLeft } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
@@ -27,11 +28,20 @@ export type NavbarProps = {
toggleSidebar: () => void;
activeRoute?: string;
languageSwitcher?: React.ReactNode;
favoritesButton?: React.ReactNode;
rightElements?: React.ReactNode;
};
export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarProps) => {
export const Navbar = ({
toggleSidebar,
activeRoute,
languageSwitcher,
favoritesButton,
rightElements = null,
}: NavbarProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const menuProps = useDisclosure();
const [scrolled, setScrolled] = useState(false);
const breakpoint = useBreakpoint();
const { colorMode, toggleColorMode } = useColorMode();
@@ -76,6 +86,20 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
window.addEventListener('scroll', changeNavbar);
let timeout: NodeJS.Timeout;
const onMouseEnter = () => {
if (timeout) {
clearTimeout(timeout);
}
menuProps.onOpen();
};
const onMouseLeave = () => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => menuProps.onClose(), 100);
};
return (
<Portal>
<Flex
@@ -109,11 +133,14 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
justifyContent="center"
>
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
<Heading size="lg">{activeRoute}</Heading>
{activeRoute && activeRoute.length > 0 ? (
<Heading size="lg" mr={4}>
{activeRoute}
</Heading>
) : null}
<Tooltip label={t('common.go_back')}>
<IconButton
mt={1}
ml={4}
colorScheme="blue"
aria-label={t('common.go_back')}
onClick={goBack}
@@ -123,6 +150,8 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
</Tooltip>
<Box ms="auto" w={{ base: 'unset' }}>
<Flex alignItems="center" flexDirection="row">
{rightElements}
{favoritesButton}
<Tooltip hasArrow label={t('common.theme')}>
<IconButton
aria-label={t('common.theme')}
@@ -132,27 +161,33 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
/>
</Tooltip>
{languageSwitcher}
<HStack spacing={{ base: '0', md: '6' }} ml={1} mr={4}>
<Menu>
<MenuButton py={2} transition="all 0.3s" _focus={{ boxShadow: 'none' }}>
<Box ml={1} mr={4}>
<Menu {...menuProps} gutter={0}>
<MenuButton
py={2}
transition="all 0.3s"
_focus={{ boxShadow: 'none' }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<HStack>
{!isCompact && <Text fontWeight="bold">{user?.name}</Text>}
<Avatar h="40px" w="40px" fontSize="0.8rem" lineHeight="2rem" src={avatar} name={user?.name} />
</HStack>
</MenuButton>
<Portal>
<MenuList
bg={useColorModeValue('white', 'gray.900')}
borderColor={useColorModeValue('gray.200', 'gray.700')}
>
<MenuItem onClick={goToProfile} w="100%">
{t('account.title')}
</MenuItem>
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
</MenuList>
</Portal>
<MenuList
bg={useColorModeValue('white', 'gray.900')}
borderColor={useColorModeValue('gray.200', 'gray.700')}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<MenuItem onClick={goToProfile} w="100%">
{t('account.title')}
</MenuItem>
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
</MenuList>
</Menu>
</HStack>
</Box>
</Flex>
</Box>
</Flex>

View File

@@ -89,7 +89,7 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
</Box>
</>
),
[user?.userRole, location],
[user?.userRole, location, topNav],
);
return (

View File

@@ -0,0 +1,188 @@
/* eslint-disable prefer-promise-reject-errors */
import * as React from 'react';
import { CheckCircleIcon, WarningIcon } from '@chakra-ui/icons';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Center,
Flex,
Heading,
Spinner,
Text,
} from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { compactDate } from 'helpers/dateFormatting';
import { getRevision } from 'helpers/stringHelper';
import { DefaultFirmware } from 'hooks/Network/DefaultFirmware';
import { getAllAvailableFirmware } from 'hooks/Network/Firmware';
const getDefaultFirmware = async (deviceType: string, revision: string) =>
getAllAvailableFirmware(deviceType)
.then((res) => {
const found = res.firmwares.find((firmware) => firmware.revision === revision);
if (!found) {
return { error: { deviceType, error: 'Could not find available firmware for this revision' } };
}
return {
success: {
deviceType,
description: '',
revision,
imageCreationDate: found.imageDate,
uri: found.uri,
created: 0,
lastModified: 0,
},
};
})
.catch(() => ({ error: { deviceType, error: 'Could not fetch firmware available for this device type' } }));
const getComputedData = async (deviceTypes: string[], revision: string) => {
const defaultFirmwares: DefaultFirmware[] = [];
const errors: { deviceType: string; error: string }[] = [];
const promises = deviceTypes.map((deviceType) => getDefaultFirmware(deviceType, revision));
const results = await Promise.allSettled(promises);
results.forEach((result) => {
if (result.status === 'fulfilled') {
if (result.value.error) {
errors.push(result.value.error);
} else if (result.value.success) {
defaultFirmwares.push(result.value.success);
}
}
});
return { defaultFirmwares, errors };
};
type Props = {
deviceTypes: string[];
revision: string;
goNext: (defaultFirmwares: DefaultFirmware[]) => Promise<void>;
setNextCallback: React.Dispatch<React.SetStateAction<(() => (Promise<void> | void) | undefined) | undefined>>;
};
const ConfirmDefaultFirmwareCreation = ({ deviceTypes, revision, goNext, setNextCallback }: Props) => {
const { t } = useTranslation();
const getCompleteData = useQuery(
['default_firmware', 'computed_data'],
() => getComputedData(deviceTypes, revision),
{
refetchOnMount: true,
},
);
const onNext = async () => {
if (getCompleteData.data) {
await goNext(getCompleteData.data.defaultFirmwares);
}
};
React.useEffect(() => {
if (getCompleteData.data) {
setNextCallback(() => onNext);
} else {
setNextCallback(undefined);
}
}, [getCompleteData.data, setNextCallback]);
return (
<>
<Heading mb={4} size="sm">
{t('firmware.confirm_default_data')}
</Heading>
{getCompleteData.isFetching ? (
<Center my={4} display="flex" flexDirection="column">
<Spinner size="xl" />
<Heading size="sm" mt={2}>
{t('firmware.fetching_defaults')}
</Heading>
</Center>
) : null}
{getCompleteData.data && !getCompleteData.isFetching ? (
<>
<Heading size="sm" mb={2}>
{t('firmware.one')}: {getRevision(revision)}
</Heading>
<Accordion allowToggle allowMultiple>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
<CheckCircleIcon color="green.500" mr={2} mt={-0.5} />
{t('firmware.default_found', { count: getCompleteData.data?.defaultFirmwares.length })}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Accordion allowToggle allowMultiple>
{getCompleteData.data?.defaultFirmwares.map((data) => (
<AccordionItem key={data.deviceType}>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
{data.deviceType}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Flex>
<Text>Image Date: </Text>
<Text ml={2}>{compactDate(data.imageCreationDate)}</Text>
</Flex>
<Flex>
<Text>URI: </Text>
<Text ml={2}>{data.uri}</Text>
</Flex>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
<WarningIcon color="red.500" mr={2} mt={-0.5} />
{t('firmware.default_not_found', { count: getCompleteData.data?.errors.length })} (cannot create
valid default firmware)
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Accordion allowToggle allowMultiple>
{getCompleteData.data?.errors.map((error) => (
<AccordionItem key={error.deviceType}>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
{error.deviceType}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>{error.error}</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</AccordionPanel>
</AccordionItem>
</Accordion>
</>
) : null}
</>
);
};
export default ConfirmDefaultFirmwareCreation;

View File

@@ -0,0 +1,81 @@
import * as React from 'react';
import { CheckCircleIcon, WarningIcon } from '@chakra-ui/icons';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Flex,
Heading,
List,
ListItem,
Text,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { CreateDefaultFirmwareResult } from './utils';
type Props = {
results: CreateDefaultFirmwareResult[];
};
const CreateDefaultFirmwareResults = ({ results }: Props) => {
const { t } = useTranslation();
const successes = results.filter((result) => !result.error);
const errors = results.filter((result) => result.error);
return (
<>
<Heading size="sm">{t('controller.devices.results')}: </Heading>
<Accordion allowToggle allowMultiple>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
<CheckCircleIcon color="green.500" mr={2} mt={-0.5} />
{t('firmware.default_created', { count: successes.length })}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<List>
{successes.map((success) => (
<ListItem key={success.deviceType}>
<Text>{success.deviceType}</Text>
</ListItem>
))}
</List>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
<WarningIcon color="red.500" mr={2} mt={-0.5} />
{t('firmware.default_created_error_other', { count: errors.length })}
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<List>
{errors.map((error) => (
<ListItem key={error.deviceType}>
<Flex>
<Text>{error.deviceType} -</Text>
<Text ml={2}>{error.error}</Text>
</Flex>
</ListItem>
))}
</List>
</AccordionPanel>
</AccordionItem>
</Accordion>
</>
);
};
export default CreateDefaultFirmwareResults;

View File

@@ -0,0 +1,98 @@
import * as React from 'react';
import { Heading } from '@chakra-ui/react';
import { GroupBase, MultiValue, OptionBase, Select } from 'chakra-react-select';
import { useTranslation } from 'react-i18next';
import { useGetDefaultFirmware } from 'hooks/Network/DefaultFirmware';
import { useGetDeviceTypes } from 'hooks/Network/Firmware';
interface Option extends OptionBase {
label: string;
value: string;
}
type Props = {
goNext: (deviceTypes: string[]) => void;
setNextCallback: React.Dispatch<React.SetStateAction<(() => (Promise<void> | void) | undefined) | undefined>>;
};
const DeviceTypeSelection = ({ goNext, setNextCallback }: Props) => {
const { t } = useTranslation();
const getFirmware = useGetDefaultFirmware();
const getDeviceTypes = useGetDeviceTypes();
const [selectedDeviceTypes, setSelectedDeviceTypes] = React.useState<string[]>([]);
const availableDevicesTypes = React.useMemo(() => {
const alreadyCreatedDeviceTypes = getFirmware.data?.firmwares.map((firmware) => firmware.deviceType);
const deviceTypes = getDeviceTypes.data?.deviceTypes;
return deviceTypes?.filter((deviceType) => !alreadyCreatedDeviceTypes?.includes(deviceType));
}, [getDeviceTypes.data, getFirmware.data]);
const handleChange = (newValue: MultiValue<Option>) => {
const deviceTypes = newValue.map(({ value }) => value);
const allIndex = deviceTypes.indexOf('*');
if (allIndex === 0) {
setSelectedDeviceTypes(availableDevicesTypes ?? []);
} else if (allIndex > 0) {
setSelectedDeviceTypes(availableDevicesTypes ?? []);
} else {
setSelectedDeviceTypes(deviceTypes);
}
};
const options = React.useMemo(() => {
if (availableDevicesTypes?.length === selectedDeviceTypes.length)
return availableDevicesTypes?.map((deviceType) => ({ value: deviceType, label: deviceType })) ?? [];
return [
{ value: '*', label: t('common.all') },
...(availableDevicesTypes?.map((deviceType) => ({ value: deviceType, label: deviceType })) ?? []),
];
}, [availableDevicesTypes, selectedDeviceTypes.length]);
const onNext = () => {
goNext(selectedDeviceTypes.sort((a, b) => a.localeCompare(b)));
};
React.useEffect(() => {
if (selectedDeviceTypes.length === 0) {
setNextCallback(undefined);
} else {
setNextCallback(() => onNext);
}
}, [selectedDeviceTypes, onNext]);
return (
<>
<Heading mb={4} size="sm">
{t('firmware.select_default_device_types')}
</Heading>
<Select<Option, true, GroupBase<Option>>
chakraStyles={{
control: (provided) => ({
...provided,
borderRadius: '15px',
border: '2px solid',
}),
dropdownIndicator: (provided) => ({
...provided,
backgroundColor: 'unset',
border: 'unset',
}),
}}
isMulti
closeMenuOnSelect={false}
options={options}
value={
selectedDeviceTypes.map((value) => ({
value,
label: value,
})) as MultiValue<Option>
}
isClearable={selectedDeviceTypes.length > 1}
onChange={handleChange}
isDisabled={!availableDevicesTypes}
/>
</>
);
};
export default DeviceTypeSelection;

View File

@@ -0,0 +1,72 @@
import * as React from 'react';
import { Box, Flex, Heading, Select, Switch, Text, useBoolean } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { compactDate } from 'helpers/dateFormatting';
import { getRevision } from 'helpers/stringHelper';
import { useGetFirmwareDeviceType } from 'hooks/Network/Firmware';
type Props = {
deviceTypes: string[];
goNext: (revision: string) => void;
setNextCallback: React.Dispatch<React.SetStateAction<(() => (Promise<void> | void) | undefined) | undefined>>;
};
const DefaultFirmwareRevisionSelection = ({ deviceTypes, goNext, setNextCallback }: Props) => {
const { t } = useTranslation();
const [isShowingDev, setIsShowingDev] = useBoolean();
const [revision, setRevision] = React.useState<string>('');
const getFirmware = useGetFirmwareDeviceType({
deviceType: deviceTypes[0] ?? 'eap_101',
});
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setRevision(event.target.value);
};
const onNext = () => {
goNext(revision);
};
const firmwareOptions = React.useMemo(() => {
if (isShowingDev) return getFirmware.data?.sort((a, b) => (a.imageDate > b.imageDate ? -1 : 1));
return getFirmware.data
?.filter((fms) => !fms.revision.includes('dev'))
?.sort((a, b) => (a.imageDate > b.imageDate ? -1 : 1));
}, [getFirmware.data, isShowingDev]);
React.useEffect(() => {
if (revision.length === 0) {
setNextCallback(undefined);
} else {
setNextCallback(() => onNext);
}
}, [revision, onNext]);
return (
<>
<Heading mb={4} size="sm">
{t('firmware.select_default_revision')}
</Heading>
<Flex mb={2}>
<Text>{t('controller.firmware.show_dev_releases')}</Text>
<Switch ml={2} isChecked={isShowingDev} onChange={setIsShowingDev.toggle} size="lg" />
</Flex>
<Box w="unset">
<Select w="unset" value={revision} onChange={handleChange}>
{revision.length === 0 && (
<option key="default" value="" disabled>
{' '}
</option>
)}
{firmwareOptions?.map((firmware) => (
<option key={firmware.revision} value={firmware.revision}>
{compactDate(firmware.imageDate)} - {getRevision(firmware.revision)}
</option>
))}
</Select>
</Box>
</>
);
};
export default DefaultFirmwareRevisionSelection;

View File

@@ -0,0 +1,121 @@
import React from 'react';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { ArrowRight } from '@phosphor-icons/react';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import ConfirmDefaultFirmwareCreation from './ConfirmDefaultFirmwareCreation';
import CreateDefaultFirmwareResults from './CreateDefaultFirmwareResults';
import DeviceTypeSelection from './DeviceTypeSelection';
import DefaultFirmwareRevisionSelection from './SelectRevision';
import { CreateDefaultFirmwareResult, createDefaultFirmwareBatch } from './utils';
import { CreateButton } from 'components/Buttons/CreateButton';
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
import { Modal } from 'components/Modals/Modal';
import { DefaultFirmware } from 'hooks/Network/DefaultFirmware';
const CreateDefaultFirmwareModal = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [currentStep, setCurrentStep] = React.useState<'device_type' | 'revision' | 'confirm' | 'result'>(
'device_type',
);
const modalProps = useDisclosure();
const closeConfirmModal = useDisclosure();
const [deviceTypes, setDeviceTypes] = React.useState<string[]>([]);
const [revision, setRevision] = React.useState<string>('');
const [results, setResults] = React.useState<CreateDefaultFirmwareResult[]>([]);
const [nextCallback, setNextCallback] = React.useState<() => (Promise<void> | void) | undefined>();
const [isLoading, setIsLoading] = React.useState(false);
const closeCancelAndForm = () => {
modalProps.onClose();
closeConfirmModal.onClose();
setCurrentStep('device_type');
setDeviceTypes([]);
setRevision('');
setResults([]);
};
const onClose = () => {
if (currentStep === 'device_type' || currentStep === 'result') {
closeCancelAndForm();
} else {
closeConfirmModal.onOpen();
}
};
const finishDeviceType = (newDeviceTypes: string[]) => {
setDeviceTypes(newDeviceTypes);
setCurrentStep('revision');
setNextCallback(undefined);
};
const finishRevision = (newRevision: string) => {
setRevision(newRevision);
setCurrentStep('confirm');
setNextCallback(undefined);
};
const startCreation = async (defaults: DefaultFirmware[]) => {
setIsLoading(true);
const newResults = await createDefaultFirmwareBatch(defaults);
setIsLoading(false);
queryClient.invalidateQueries(['default_firmwares']);
setResults(newResults);
setCurrentStep('result');
setNextCallback(undefined);
};
return (
<>
<CreateButton onClick={modalProps.onOpen} mr={2} />
<Modal
{...modalProps}
onClose={onClose}
title={`${t('common.create')} ${t('common.default')} ${t('firmware.one')}`}
topRightButtons={
currentStep === 'result' ? null : (
<Button
onClick={nextCallback}
isDisabled={nextCallback === undefined}
rightIcon={<ArrowRight />}
colorScheme="blue"
isLoading={isLoading}
>
{currentStep === 'confirm' ? t('common.start') : t('common.next')}
</Button>
)
}
>
<Box pb={8}>
{currentStep === 'device_type' ? (
<DeviceTypeSelection goNext={finishDeviceType} setNextCallback={setNextCallback} />
) : null}
{currentStep === 'revision' ? (
<DefaultFirmwareRevisionSelection
deviceTypes={deviceTypes}
goNext={finishRevision}
setNextCallback={setNextCallback}
/>
) : null}
{currentStep === 'confirm' ? (
<ConfirmDefaultFirmwareCreation
deviceTypes={deviceTypes}
revision={revision}
goNext={startCreation}
setNextCallback={setNextCallback}
/>
) : null}
{currentStep === 'result' ? <CreateDefaultFirmwareResults results={results} /> : null}
</Box>
</Modal>
<ConfirmCloseAlertModal
isOpen={closeConfirmModal.isOpen}
confirm={closeCancelAndForm}
cancel={closeConfirmModal.onClose}
/>
</>
);
};
export default CreateDefaultFirmwareModal;

View File

@@ -0,0 +1,30 @@
import { isAxiosError } from 'axios';
import { DefaultFirmware, createDefaultFirmware } from 'hooks/Network/DefaultFirmware';
export type CreateDefaultFirmwareResult =
| {
deviceType: string;
error?: undefined;
}
| {
deviceType: string;
error: string;
};
export const createFms = async (defaultFirmware: DefaultFirmware): Promise<CreateDefaultFirmwareResult> =>
createDefaultFirmware(defaultFirmware)
.then(() => ({
deviceType: defaultFirmware.deviceType,
}))
.catch((e) => ({
deviceType: defaultFirmware.deviceType,
error: isAxiosError(e) ? e.response?.data?.ErrorDescription : 'Unknown error',
}));
export const createDefaultFirmwareBatch = async (
defaultFirmware: DefaultFirmware[],
): Promise<CreateDefaultFirmwareResult[]> => {
const promises = defaultFirmware.map((fms) => createFms(fms));
const responses = await Promise.all(promises);
return responses;
};

View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import {
useDisclosure,
Box,
Button,
Center,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Tooltip,
useToast,
} from '@chakra-ui/react';
import { Trash } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { useDeleteBatchDefaultFirmware, useGetDefaultFirmware } from 'hooks/Network/DefaultFirmware';
const DeleteDefaultFirmwaresButton = () => {
const { t } = useTranslation();
const toast = useToast();
const popoverProps = useDisclosure();
const getFirmware = useGetDefaultFirmware();
const deleteFirmware = useDeleteBatchDefaultFirmware();
const handleDelete = () => {
deleteFirmware.mutate(getFirmware.data?.firmwares.map((firmware) => firmware.deviceType) ?? [], {
onSuccess: () => {
toast({
id: `firmware-delete-success`,
title: t('common.success'),
description: t('firmware.default_mass_delete_success', { count: getFirmware.data?.firmwares.length }),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
popoverProps.onClose();
},
});
};
return (
<Popover isOpen={popoverProps.isOpen} onOpen={popoverProps.onOpen} onClose={popoverProps.onClose}>
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={popoverProps.isOpen}>
<Box>
<PopoverTrigger>
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.delete')} <b>{getFirmware.data?.firmwares.length} Settings</b>
</PopoverHeader>
<PopoverBody>
Are you sure you want to delete these {getFirmware.data?.firmwares.length} default firmware settings?
</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={popoverProps.onClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDelete} isLoading={deleteFirmware.isLoading}>
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
);
};
export default DeleteDefaultFirmwaresButton;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import {
Box,
Button,
Center,
Flex,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
Tooltip,
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { DefaultFirmware, useDeleteDefaultFirmware } from 'hooks/Network/DefaultFirmware';
import { AxiosError } from 'models/Axios';
type Props = {
firmware: DefaultFirmware;
handleViewDetails: (firmware: DefaultFirmware) => void;
};
const Actions = ({ firmware, handleViewDetails }: Props) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const deleteFirmware = useDeleteDefaultFirmware();
const handleDeleteClick = () =>
deleteFirmware.mutate(firmware.deviceType, {
onSuccess: () => {
toast({
id: `firmware-delete-success`,
title: t('common.success'),
description: t('firmware.delete_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
queryClient.invalidateQueries(['defaultFirmwares']);
},
onError: (error) => {
const e = error as AxiosError;
toast({
id: `firmware-delete-error`,
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
});
const handleDetailsClick = () => handleViewDetails(firmware);
return (
<Flex>
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
<Box>
<PopoverTrigger>
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
</PopoverTrigger>
</Box>
</Tooltip>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>
{t('crud.delete')} <b>{firmware.deviceType}</b>
</PopoverHeader>
<PopoverBody>{t('crud.delete_confirm', { obj: 'setting' })}</PopoverBody>
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onClose}>
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteFirmware.isLoading}>
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
</PopoverContent>
</Popover>
<Tooltip hasArrow label={t('common.view_details')} placement="top">
<IconButton
aria-label={t('common.view_details')}
ml={2}
colorScheme="blue"
icon={<MagnifyingGlass size={20} />}
size="sm"
onClick={handleDetailsClick}
/>
</Tooltip>
</Flex>
);
};
export default Actions;

View File

@@ -0,0 +1,319 @@
import * as React from 'react';
import { CopyIcon } from '@chakra-ui/icons';
import {
Alert,
AlertDescription,
AlertIcon,
Box,
Flex,
Grid,
GridItem,
Heading,
IconButton,
Select,
Switch,
Text,
Textarea,
Tooltip,
UseDisclosureReturn,
useBoolean,
useClipboard,
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { DefaultFirmware, useUpdateDefaultFirmware } from 'hooks/Network/DefaultFirmware';
import { useGetFirmwareDeviceType } from 'hooks/Network/Firmware';
import { AtLeast } from 'models/General';
import { AxiosError } from 'models/Axios';
import { getRevision } from 'helpers/stringHelper';
import { compactDate } from 'helpers/dateFormatting';
import { Firmware } from 'models/Firmware';
import { Modal } from 'components/Modals/Modal';
import { SaveButton } from 'components/Buttons/SaveButton';
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
type Props = {
modalProps: UseDisclosureReturn;
defaultFirmware: DefaultFirmware;
};
const EditDefaultFirmwareModal = ({ modalProps, defaultFirmware }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const copy = useClipboard(defaultFirmware.uri);
const warningModalProps = useDisclosure();
const [isEditing, setIsEditing] = useBoolean();
const updateFirmware = useUpdateDefaultFirmware();
const [showDev, setShowDev] = useBoolean(false);
const getFirmware = useGetFirmwareDeviceType({ deviceType: defaultFirmware.deviceType });
const [formValues, setFormValues] = React.useState<{
revision: string;
imageDate: number;
uri: string;
description: string;
}>({
revision: defaultFirmware.revision,
uri: defaultFirmware.uri,
imageDate: defaultFirmware.imageCreationDate,
description: defaultFirmware.description,
});
const currentFirmware = {
revision: defaultFirmware.revision,
uri: defaultFirmware.uri,
imageDate: defaultFirmware.imageCreationDate,
};
const availableFirmware = React.useMemo(() => {
let allAvailable: (
| Firmware
| {
revision: string;
uri: string;
imageDate: number;
}
)[] = getFirmware.data ?? [];
if (!allAvailable.find(({ revision }) => revision === currentFirmware.revision)) {
allAvailable = [currentFirmware, ...allAvailable];
}
allAvailable.sort((a, b) => b.imageDate - a.imageDate);
return allAvailable;
}, [getFirmware.data, currentFirmware]);
const options = availableFirmware
?.filter(({ revision }) => (showDev ? true : !revision.includes('dev')))
.map((firmware) => ({
value: firmware.revision,
label:
firmware.revision === currentFirmware.revision
? `${compactDate(firmware.imageDate)} - ${getRevision(firmware.revision)} (Current Value)`
: `${compactDate(firmware.imageDate)} - ${getRevision(firmware.revision)}`,
}));
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === currentFirmware.revision) {
setFormValues({
...formValues,
revision: currentFirmware.revision,
uri: currentFirmware.uri,
imageDate: currentFirmware.imageDate,
});
} else {
const found = availableFirmware.find(({ revision }) => revision === e.target.value);
if (found) {
setFormValues({
...formValues,
revision: found.revision,
uri: found.uri,
imageDate: found.imageDate,
});
}
}
};
const onClose = () => {
if (isEditing) {
warningModalProps.onOpen();
} else {
modalProps.onClose();
}
};
const onSave = () => {
const newValues: AtLeast<DefaultFirmware, 'deviceType'> = {
deviceType: defaultFirmware.deviceType,
description: formValues.description,
};
if (formValues.revision !== currentFirmware.revision) {
newValues.revision = formValues.revision;
newValues.uri = formValues.uri;
newValues.imageCreationDate = formValues.imageDate;
}
updateFirmware.mutate(newValues, {
onSuccess: () => {
toast({
id: `config-edit-success`,
title: t('common.success'),
description: t('firmware.default_update_success', { deviceType: defaultFirmware.deviceType }),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setIsEditing.off();
modalProps.onClose();
},
onError: (error) => {
const e = error as AxiosError;
toast({
id: `config-edit-error`,
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
});
};
const onCloseAfterWarning = () => {
setIsEditing.off();
warningModalProps.onClose();
modalProps.onClose();
};
React.useEffect(() => {
copy.setValue(defaultFirmware.uri);
setFormValues({
revision: defaultFirmware.revision,
description: defaultFirmware.description,
uri: defaultFirmware.uri,
imageDate: defaultFirmware.imageCreationDate,
});
}, [defaultFirmware.uri, defaultFirmware.description, defaultFirmware.revision, isEditing]);
// TODO -> Only thing editable is the firmware and description. Make sure I can deal with a revision not existing anymore
return (
<>
<Modal
{...modalProps}
onClose={onClose}
title={defaultFirmware.deviceType}
topRightButtons={
<>
<SaveButton hidden={!isEditing} onClick={onSave} isLoading={updateFirmware.isLoading} />
<ToggleEditButton toggleEdit={setIsEditing.toggle} isEditing={isEditing} />
</>
}
>
<Box>
<Alert status="info">
<AlertIcon />
<AlertDescription>
{t('firmware.edit_default_title', { deviceType: defaultFirmware.deviceType })}
</AlertDescription>
</Alert>
{isEditing ? (
<Box mb={4}>
<Heading size="md" mb={2}>
New Values
</Heading>
<Heading size="sm">{t('commands.revision')}</Heading>
<Flex mb={2}>
<Text>{t('controller.firmware.show_dev_releases')}</Text>
<Switch ml={2} isChecked={showDev} onChange={setShowDev.toggle} size="md" mt={0.5} />
</Flex>
<Box display="inline-flex">
<Select value={formValues.revision} onChange={onSelectChange}>
{options?.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</Select>
</Box>
<Grid>
<GridItem colSpan={1} alignContent="center" alignItems="center" display="flex">
<Heading size="sm">URI</Heading>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
{formValues.uri}
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<Heading size="sm">{t('commands.image_date')}</Heading>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<FormattedDate date={formValues.imageDate} />
</GridItem>
</Grid>
<Heading size="sm">{t('common.description')}</Heading>
<Textarea
value={formValues.description}
onChange={(e) =>
setFormValues({
...formValues,
description: e.target.value,
})
}
/>
</Box>
) : null}
<Grid templateColumns="repeat(1, 1fr)" gap={1} mt={4} w="100%">
<GridItem colSpan={1} alignContent="center" alignItems="center" hidden={!isEditing}>
<Heading size="md">Current Values</Heading>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<Heading size="sm">{t('commands.revision')}</Heading>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
{getRevision(defaultFirmware.revision)}
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center" display="flex">
<Heading size="sm">URI</Heading>
<Tooltip
label={copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')}
hasArrow
closeOnClick={false}
shouldWrapChildren
>
<IconButton
aria-label={t('common.copy')}
icon={<CopyIcon h={4} w={4} />}
onClick={(e) => {
copy.onCopy();
e.stopPropagation();
}}
size="xs"
colorScheme="teal"
mx={0.5}
/>
</Tooltip>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
{defaultFirmware.uri}
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<Heading size="sm">{t('commands.image_date')}</Heading>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<FormattedDate date={defaultFirmware.imageCreationDate} />
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<Heading size="sm">{t('common.modified')}</Heading>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<FormattedDate date={defaultFirmware.lastModified} />
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<Heading size="sm">{t('common.description')}</Heading>
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
{defaultFirmware.description.length === 0 ? (
<Text fontStyle="italic">{t('common.none')}</Text>
) : (
<Text>{defaultFirmware.description}</Text>
)}
</GridItem>
</Grid>
</Box>
</Modal>
<ConfirmCloseAlertModal
isOpen={warningModalProps.isOpen}
confirm={onCloseAfterWarning}
cancel={warningModalProps.onClose}
/>
</>
);
};
export default EditDefaultFirmwareModal;

View File

@@ -0,0 +1,119 @@
import * as React from 'react';
import { Box, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import CreateDefaultFirmwareModal from '../CreateModal';
import DeleteDefaultFirmwaresButton from '../DeleteButton';
import Actions from './Actions';
import EditDefaultFirmware from './EditModal';
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
import { DefaultFirmware, useGetDefaultFirmware } from 'hooks/Network/DefaultFirmware';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { getRevision } from 'helpers/stringHelper';
import { DataGrid } from 'components/DataTables/DataGrid';
const DefaultFirmwareList = () => {
const { t } = useTranslation();
const tableController = useDataGrid({
tableSettingsId: 'controller.default_firmware.table',
defaultOrder: ['deviceType', 'revision', 'modified', 'description', 'actions'],
defaultSortBy: [{ id: 'deviceType', desc: true }],
});
const getFirmware = useGetDefaultFirmware();
const modalProps = useDisclosure();
const [selectedFirmware, setSelectedFirmware] = React.useState<DefaultFirmware | undefined>();
const onViewDetails = (config: DefaultFirmware) => {
setSelectedFirmware(config);
modalProps.onOpen();
};
const onRowClick = React.useCallback((row: DefaultFirmware) => () => onViewDetails(row), []);
const dateCell = React.useCallback((v: number) => <FormattedDate date={v} />, []);
const actionCell = React.useCallback(
(firmware: DefaultFirmware) => <Actions firmware={firmware} handleViewDetails={onViewDetails} />,
[],
);
const revisionCell = React.useCallback((v: string) => getRevision(v), []);
const columns: DataGridColumn<DefaultFirmware>[] = React.useMemo(
() =>
[
{
id: 'deviceType',
header: t('common.type'),
accessorKey: 'deviceType',
meta: {
customWidth: '150px',
alwaysShow: true,
anchored: true,
},
},
{
id: 'revision',
header: t('commands.revision'),
accessorKey: 'revision',
cell: ({ cell }) => revisionCell(cell.row.original.revision),
meta: {
customWidth: '150px',
alwaysShow: true,
},
},
{
id: 'modified',
header: t('common.modified'),
accessorKey: 'modified',
cell: ({ cell }) => dateCell(cell.row.original.lastModified),
meta: {
customWidth: '50px',
},
},
{
id: 'description',
header: t('common.description'),
accessorKey: 'description',
},
{
id: 'actions',
header: t('common.actions'),
accessorKey: 'actions',
cell: (v) => actionCell(v.cell.row.original),
enableSorting: false,
meta: {
customWidth: '50px',
alwaysShow: true,
},
},
] satisfies DataGridColumn<DefaultFirmware>[],
[dateCell],
);
return (
<Box overflowX="auto" w="100%">
<DataGrid<DefaultFirmware>
controller={tableController}
header={{
title: `${t('common.default')} ${t('firmware.one')} ${
getFirmware.data ? `(${getFirmware.data.firmwares.length})` : ''
}`,
objectListed: t('analytics.firmware'),
otherButtons: <DeleteDefaultFirmwaresButton />,
addButton: <CreateDefaultFirmwareModal />,
}}
columns={columns}
data={getFirmware.data?.firmwares}
isLoading={getFirmware.isFetching}
options={{
onRowClick: (firmw) => onRowClick(firmw),
refetch: getFirmware.refetch,
minimumHeight: '200px',
showAsCard: true,
isHidingControls: true,
isManual: true,
}}
/>
{selectedFirmware ? <EditDefaultFirmware modalProps={modalProps} defaultFirmware={selectedFirmware} /> : null}
</Box>
);
};
export default DefaultFirmwareList;

View File

@@ -0,0 +1,6 @@
import React from 'react';
import DefaultFirmwareList from './List';
const DefaultFirmwarePage = () => <DefaultFirmwareList />;
export default DefaultFirmwarePage;

View File

@@ -0,0 +1,18 @@
import * as Yup from 'yup';
export const DefaultFirmwareSchema = (t: (str: string) => string) =>
Yup.object()
.shape({
deviceType: Yup.string().required(t('form.required')),
description: Yup.string(),
uri: Yup.string().required(t('form.required')),
revision: Yup.string().required(t('form.required')),
imageCreationDate: Yup.date().required(t('form.required')),
})
.default({
deviceType: '',
description: '',
uri: '',
revision: '',
imageCreationDate: '',
});

View File

@@ -175,6 +175,7 @@ const InterfaceChart = ({ data, format }: Props) => {
},
},
y: {
beginAtZero: true,
grid: {
color: colorMode === 'dark' ? 'white' : undefined,
},

View File

@@ -55,7 +55,7 @@ const FirmwareListTable = () => {
[],
);
const revisionCell = React.useCallback((v: string) => getRevision(v), []);
const uriCell = React.useCallback((v: string) => <UriCell uri={v} />, []);
const uriCell = React.useCallback((v: string) => <UriCell key={v} uri={v} />, []);
const actionCell = React.useCallback(
(firmw: Firmware) => (
<Tooltip hasArrow label={t('common.view_details')} placement="top">

View File

@@ -0,0 +1,291 @@
import * as React from 'react';
import { Box, Center, Divider, Flex, Heading, useBreakpoint, useColorMode } from '@chakra-ui/react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
CoreChartOptions,
ElementChartOptions,
PluginChartOptions,
DatasetChartOptions,
ScaleChartOptions,
LineControllerChartOptions,
} from 'chart.js';
import { _DeepPartialObject } from 'chart.js/types/utils';
import { Line } from 'react-chartjs-2';
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
import { SystemResources, useGetSystemResources } from 'hooks/Network/System';
import { Card } from 'components/Containers/Card';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { CardBody } from 'components/Containers/Card/CardBody';
import { bytesString } from 'helpers/stringHelper';
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
const getDivisionFactor = (maxBytes: number) => {
if (maxBytes < 1024) {
return { factor: 1, unit: 'B' };
}
if (maxBytes < 1024 * 1024) {
return { factor: 1024, unit: 'KB' };
}
if (maxBytes < 1024 * 1024 * 1024) {
return { factor: 1024 * 1024, unit: 'MB' };
}
return { factor: 1024 * 1024 * 1024, unit: 'GB' };
};
interface Props {
endpoint: EndpointApiResponse;
token: string;
}
const MonitoringSystemCard = ({ endpoint, token }: Props) => {
const { colorMode } = useColorMode();
const [cumulativeData, setCumulativeData] = React.useState<(SystemResources & { timestamp: Date })[]>([]);
const breakpoint = useBreakpoint();
const isVertical = React.useMemo(
() => breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md',
[breakpoint],
);
const onNewData = (data: SystemResources) => {
if (cumulativeData.length > 100) {
setCumulativeData((prev) => [...prev.slice(1), { ...data, timestamp: new Date() }]);
} else {
setCumulativeData((prev) => [...prev, { ...data, timestamp: new Date() }]);
}
};
const getResources = useGetSystemResources({ endpoint: endpoint.uri, token, onSuccess: onNewData });
const data = React.useMemo(() => {
const labels = [] as string[];
const currentRealMemory = [] as string[];
const peakRealMemory = [] as string[];
const currentVirtualMemory = [] as string[];
const peakVirtualMemory = [] as string[];
const numberOfFileDescriptors = [] as number[];
let highestRealMem = 0;
let highestVirtualMem = 0;
for (const curr of cumulativeData) {
if (curr.currRealMem > highestRealMem) highestRealMem = curr.currRealMem;
if (curr.currVirtMem > highestVirtualMem) highestVirtualMem = curr.currVirtMem;
}
const realMemFactor = getDivisionFactor(highestRealMem);
const virtualMemFactor = getDivisionFactor(highestVirtualMem);
for (const curr of cumulativeData) {
labels.push(curr.timestamp.toLocaleTimeString());
currentRealMemory.push((Math.floor((curr.currRealMem / realMemFactor.factor) * 100) / 100).toFixed(2));
peakRealMemory.push((Math.floor((curr.peakRealMem / realMemFactor.factor) * 100) / 100).toFixed(2));
currentVirtualMemory.push((Math.floor((curr.currVirtMem / virtualMemFactor.factor) * 100) / 100).toFixed(2));
peakVirtualMemory.push((Math.floor((curr.peakVirtMem / virtualMemFactor.factor) * 100) / 100).toFixed(2));
numberOfFileDescriptors.push(curr.numberOfFileDescriptors);
}
const datasets = [
{
label: 'Curr. Real Mem.',
data: currentRealMemory,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
pointRadius: 0,
},
{
label: 'Curr. Virt. Mem.',
data: currentVirtualMemory,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.5)',
pointRadius: 0,
},
{
label: 'File Descriptors',
data: numberOfFileDescriptors,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
pointRadius: 0,
},
] as const;
const newData = {
labels,
realMemFactor,
virtualMemFactor,
dataTick: (value: number) => {
try {
const temp = String(value);
if (temp.includes('.')) {
return Number(temp).toFixed(1);
}
return temp;
} catch (e) {
return value;
}
},
datasets,
};
return newData;
}, [cumulativeData]);
const options: (
factor?: number,
unit?: string,
) => _DeepPartialObject<
CoreChartOptions<'line'> &
ElementChartOptions<'line'> &
PluginChartOptions<'line'> &
DatasetChartOptions<'line'> &
ScaleChartOptions<'line'> &
LineControllerChartOptions
> = React.useMemo(
() => (_?: number, unit?: string) => ({
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: false,
},
title: {
display: false,
},
tooltip: {
mode: 'index',
position: 'nearest',
intersect: false,
callbacks: {
label: (context) => `${context.dataset.label}: ${context.formattedValue} ${unit ?? ''}`,
},
},
},
scales: {
x: {
grid: {
color: colorMode === 'dark' ? 'white' : undefined,
},
ticks: {
color: colorMode === 'dark' ? 'white' : undefined,
maxRotation: 10,
autoSkip: true,
maxTicksLimit: 10,
},
},
y: {
grid: {
color: colorMode === 'dark' ? 'white' : undefined,
},
ticks: {
color: colorMode === 'dark' ? 'white' : undefined,
callback: (tickValue) => (unit ? `${data.dataTick(tickValue as number)} ${unit}` : tickValue),
},
beginAtZero: true,
},
},
elements: {
line: {
tension: 0.4,
},
},
hover: {
mode: 'nearest',
intersect: true,
},
}),
[colorMode, data],
);
if (getResources.error || getResources.isLoading) return null;
return (
<Card>
<CardHeader>
<Heading size="md" pt={0}>
{endpoint.type}
</Heading>
</CardHeader>
<CardBody>
{isVertical ? (
<Box w="100%" display="block">
<Box mb={4}>
<Heading size="sm">Real Memory (Peak: {bytesString(getResources.data?.peakRealMem ?? 0)})</Heading>
<Box position="relative" w="100%">
<Line
options={options(data.realMemFactor.factor, data.realMemFactor.unit)}
data={{ ...data, datasets: [data.datasets[0]] }}
/>
</Box>
</Box>
<Box>
<Heading size="sm">Virtual Memory (Peak: {bytesString(getResources.data?.peakVirtMem ?? 0)})</Heading>
<Box position="relative" w="100%">
<Line
options={options(data.virtualMemFactor.factor, data.virtualMemFactor.unit)}
data={{ ...data, datasets: [data.datasets[1]] }}
/>
</Box>
</Box>
<Box>
<Heading size="sm">File Descriptors</Heading>
<Box position="relative" w="100%">
<Line options={options()} data={{ ...data, datasets: [data.datasets[2]] }} height={180} />
</Box>
</Box>
</Box>
) : (
<Box w="100%" display="block">
<Flex w="100%">
<Box w="100%">
<Center>
<Heading size="sm">Real Memory (Peak: {bytesString(getResources.data?.peakRealMem ?? 0)})</Heading>
</Center>
<Box position="relative" w="100%">
<Line
options={options(data.realMemFactor.factor, data.realMemFactor.unit)}
data={{ ...data, datasets: [data.datasets[0]] }}
height={180}
/>
</Box>
</Box>
<Divider height="180px" mx={4} orientation="vertical" />
<Box w="100%">
<Center>
<Heading size="sm">Virtual Memory (Peak: {bytesString(getResources.data?.peakVirtMem ?? 0)})</Heading>
</Center>
<Box position="relative" w="100%">
<Line
options={options(data.virtualMemFactor.factor, data.virtualMemFactor.unit)}
data={{ ...data, datasets: [data.datasets[1]] }}
height={180}
/>
</Box>
</Box>
<Divider height="180px" mx={4} orientation="vertical" />
<Box w="100%">
<Center>
<Heading size="sm">File Descriptors</Heading>
</Center>
<Box position="relative" w="100%">
<Line options={options()} data={{ ...data, datasets: [data.datasets[2]] }} height={180} />
</Box>
</Box>
</Flex>
</Box>
)}
</CardBody>
</Card>
);
};
export default MonitoringSystemCard;

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { SimpleGrid, Spacer } from '@chakra-ui/react';
import { v4 as uuid } from 'uuid';
import { RefreshButton } from '../../components/Buttons/RefreshButton';
import SystemTile from './MonitoringSystemCard';
import { useAuth } from 'contexts/AuthProvider';
import { useGetEndpoints } from 'hooks/Network/Endpoints';
import { Card } from 'components/Containers/Card';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { axiosSec } from 'constants/axiosInstances';
type MonitoringPageProps = {
isOnlySec?: boolean;
};
const MonitoringPage = ({ isOnlySec }: MonitoringPageProps) => {
const { token } = useAuth();
const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} });
const endpointsList = React.useMemo(() => {
if (!token || (!isOnlySec && !endpoints)) return null;
const endpointList = endpoints ? [...endpoints] : [];
endpointList.push({
uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '',
type: isOnlySec ? '' : 'owsec',
id: 0,
vendor: 'owsec',
authenticationType: '',
});
return endpointList
.sort((a, b) => {
if (a.type < b.type) return -1;
if (a.type > b.type) return 1;
return 0;
})
.map((endpoint) => <SystemTile key={uuid()} endpoint={endpoint} token={token} />);
}, [endpoints, token]);
return (
<>
<Card mb="20px">
<CardHeader variant="unstyled" px={4} py={2}>
<Spacer />
<RefreshButton onClick={refetch} isFetching={isFetching} />
</CardHeader>
</Card>
<SimpleGrid minChildWidth="1000px" spacing="20px">
{endpointsList}
</SimpleGrid>
</>
);
};
export default MonitoringPage;

View File

@@ -3,6 +3,7 @@ import { Barcode, FloppyDisk, Info, ListBullets, TerminalWindow, UsersThree, Wif
import { Route } from 'models/Routes';
const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations'));
const DefaultFirmwarePage = React.lazy(() => import('pages/DefaultFirmware'));
const DevicePage = React.lazy(() => import('pages/Device'));
const DashboardPage = React.lazy(() => import('pages/Devices/Dashboard'));
const AllDevicesPage = React.lazy(() => import('pages/Devices/ListCard'));
@@ -16,6 +17,7 @@ const FirmwareDashboard = React.lazy(() => import('pages/Firmware/Dashboard'));
const ProfilePage = React.lazy(() => import('pages/Profile'));
const ScriptsPage = React.lazy(() => import('pages/Scripts'));
const UsersPage = React.lazy(() => import('pages/UsersPage'));
const MonitoringPage = React.lazy(() => import('pages/MonitoringPage'));
const EndpointsPage = React.lazy(() => import('pages/EndpointsPage'));
const SystemConfigurationPage = React.lazy(() => import('pages/SystemConfigurationPage'));
@@ -82,12 +84,26 @@ const routes: Route[] = [
component: ScriptsPage,
},
{
id: 'configurations',
id: 'defaults-group',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/configurations',
name: 'configurations.title',
name: 'common.defaults',
icon: () => <Barcode size={28} weight="bold" />,
component: DefaultConfigurationsPage,
children: [
{
id: 'configurations',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/configurations',
name: 'configurations.title',
component: DefaultConfigurationsPage,
},
{
id: 'default_firmware',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/firmware/defaults',
name: 'firmware.one',
component: DefaultFirmwarePage,
},
],
},
{
id: 'logs-group',
@@ -162,13 +178,6 @@ const routes: Route[] = [
name: 'system.title',
icon: () => <Info size={28} weight="bold" />,
children: [
{
id: 'system-services',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/services',
name: 'system.services',
component: EndpointsPage,
},
{
id: 'system-configuration',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
@@ -176,6 +185,20 @@ const routes: Route[] = [
name: 'system.configuration',
component: SystemConfigurationPage,
},
{
id: 'system-monitoring',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/systemMonitoring',
name: 'analytics.monitoring',
component: MonitoringPage,
},
{
id: 'system-services',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/services',
name: 'system.services',
component: EndpointsPage,
},
],
},
];