Compare commits

...

49 Commits

Author SHA1 Message Date
TIP Automation User
4b79a0b74c Chg: update image tag in helm values to v2.7.0 2022-10-05 11:32:56 +00:00
TIP Automation User
c158f0aef8 Chg: update image tag in helm values to v2.7.0-RC2 2022-09-29 23:27:39 +00:00
jaspreetsachdev
4e5c6a9426 Merge pull request #112 from Telecominfraproject/main
Fixes for WIFI-10904 and others
2022-09-29 19:15:21 -04:00
jaspreetsachdev
7ad184cb48 Merge branch 'release/v2.7.0' into main 2022-09-29 19:15:03 -04:00
Charles Bourque
41a7d5d0a8 Merge pull request #111 from stephb9959/main
[WIFI-10904] Websocket more resilient in case of disconnection
2022-09-23 12:42:28 +01:00
Charles
78c48e004c [WIFI-10904] Websocket more resilient in case of disconnection
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-09-23 12:41:00 +01:00
Charles Bourque
7106d61881 Merge pull request #110 from stephb9959/main
[WIFI-10904] Connection statistics on the sidebar
2022-09-22 19:55:28 +01:00
Charles
8ead4c4708 [WIFI-10904] Connection statistics on the sidebar
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-09-22 19:54:21 +01:00
Charles Bourque
52ca7d3503 Merge pull request #109 from stephb9959/main
[WIFI-10894] Status column added to command history
2022-09-21 13:55:11 +01:00
Charles Bourque
7d504da0a8 Merge pull request #108 from stephb9959/main
[WIFI-10894] Status column added to command history
2022-09-21 13:54:10 +01:00
Charles
c6dee2252b [WIFI-10894] Status column added to command history
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-09-21 13:53:24 +01:00
TIP Automation User
680c4a9ec4 Chg: update image tag in helm values to v2.7.0-RC1 2022-09-16 19:54:57 +00:00
Charles Bourque
3887d57fa4 Merge pull request #107 from stephb9959/main
[WIFI-10857] Fixed display when there are no entries
2022-09-15 16:33:44 +01:00
Charles
d733daed9d [WIFI-10857] Fixed display when there are no entries
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-09-15 16:33:01 +01:00
Charles Bourque
de8651ab52 Merge pull request #106 from stephb9959/main
[WIFI-10850] Error descriptions on command failures
2022-09-15 12:46:01 +01:00
Charles
0ce641d10b [WIFI-10850] Error descriptions on command failures
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-09-15 12:45:16 +01:00
Charles Bourque
316224b424 Merge pull request #105 from stephb9959/main
[WIFI-10832] Redirecting on invalid/not found serial numbers on device page
2022-09-14 08:55:35 +01:00
Charles
cf9bbce284 [WIFI-10832] Redirecting on invalid/not found serial numbers on device page
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-09-14 08:53:33 +01:00
Charles Bourque
6eae6c046e Merge pull request #104 from stephb9959/main
[WIFI-10714] System page fix for RRM and other endpoints witthout sub…
2022-09-02 18:13:23 +01:00
Charles
837a430228 [WIFI-10714] System page fix for RRM and other endpoints witthout subsystems
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-09-02 18:12:45 +01:00
Charles Bourque
71431f8fb5 Merge pull request #103 from stephb9959/main
[WIFI-10583] Reacting to more cases where a token might be expired/invalid
2022-08-18 10:48:22 +01:00
Charles
0c7cd1f299 [WIFI-10583] Reacting to more cases where a token might be expired/invalid
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-08-18 10:46:52 +01:00
Dmitry Dunaev
674682e919 Merge pull request #102 from Telecominfraproject/fix/wifi-10414-cve-image
[WIFI-10414] Fix: vulnerable NodeJS image
2022-08-17 16:34:42 +03:00
Dmitry Dunaev
a5ca8115af [WIFI-10414] Fix: vulnerable NodeJS image
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-08-15 11:33:40 +03:00
Charles Bourque
d4338fce42 Merge pull request #101 from stephb9959/main
[WIFI-10548] Network diagram now showing all associations
2022-08-11 11:21:56 +01:00
Charles
14e8135f81 [WIFI-10548] Network diagram now showing all associations
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-08-11 11:20:04 +01:00
Charles Bourque
e925f07505 Merge pull request #100 from stephb9959/main
[WIFI-10515] Crash fix when receiving corrupted statistics
2022-08-08 16:59:27 +01:00
Charles
b792b51bd0 [WIFI-10515] Crash fix when receiving corrupted statistics
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-08-08 16:58:38 +01:00
Charles Bourque
fb64813b2a Merge pull request #99 from stephb9959/main
[WIFI-10259] WifiScan now sending all IE options
2022-07-26 12:29:11 +01:00
Charles
b16e0e33ab [WIFI-10259] WifiScan now sending all IE options, removed selection options
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-07-26 12:20:11 +01:00
Charles
818921e4a2 2.7.0(0): version bump and crash fix on missing endpoints
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-07-26 11:47:34 +01:00
Charles Bourque
6c437459ca Merge pull request #98 from stephb9959/main
2.6.29
2022-06-29 20:51:47 +01:00
Charles
b276901874 Merge remote-tracking branch 'upstream/main' 2022-06-29 20:48:58 +01:00
Charles
85b92f46f5 [WIFI-9921] Telemetry now only showing selected types when receiving messages
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-06-28 15:03:51 +01:00
Charles
237b8b5ede [WIFI-9773] Wifi Scan request sometimes stalling
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-06-21 18:12:17 +01:00
Charles
438d008c34 2.6.27: wifi analysis with no records fix
Signed-off-by: Charles <charles.bourque96@gmail.com>
2022-06-21 18:12:17 +01:00
Johann Hoffmann
53a3de1ebc Supress curl output in PR cleanup workflow
Signed-off-by: Johann Hoffmann <johann.hoffmann@mailbox.org>
2022-06-17 13:52:04 +02:00
Johann Hoffmann
2d35747e75 [WIFI-9534] Add condition to avoid deleting default and release branch images
Signed-off-by: Johann Hoffmann <johann.hoffmann@mailbox.org>
2022-06-17 13:51:29 +02:00
Johann Hoffmann
71feebea6d Temporarily disable cleanup for merges into release branches
Signed-off-by: Johann Hoffmann <johann.hoffmann@mailbox.org>
2022-06-15 14:49:53 +02:00
Charles
c8c75e7a70 Merge pull request #95 from stephb9959/main
2.6.27: wifi analysis with no records fix
2022-06-10 16:40:17 +01:00
Charles
7b2263e9a5 2.6.27: wifi analysis with no records fix 2022-06-10 16:14:03 +01:00
Charles
9cd216bbba Merge pull request #93 from stephb9959/main
2.6.26
2022-06-08 19:22:35 +01:00
Charles
e032ff4485 2.6.26: upgrade ucentral-libs version 2022-06-08 19:21:59 +01:00
Charles
fbe9ca5dd9 Merge pull request #189 from Telecominfraproject/main
TIP merge into Arilia repo
2022-06-08 19:10:00 +01:00
Charles
4533bb6dd7 Merge pull request #90 from clayface/kafka_telemetry
WIFI-7947: Telemetry: Add lifetime and kafka/websocket options
2022-06-01 21:02:18 +01:00
Charles
3320c03603 Merge pull request #92 from Telecominfraproject/2.7.0
[NO-JIRA] 2.7.0
2022-06-01 16:47:11 +01:00
Charles
c3574d96d7 2.7.0 2022-06-01 16:44:07 +01:00
Matthew Hagan
bc12b598ce Telemetry: add Kafka, Websocket output choice
Signed-off-by: Matthew Hagan <mathagan@fb.com>
2022-05-17 22:29:40 +01:00
Matthew Hagan
a34f679c43 Telemetry: add lifetime option
Signed-off-by: Matthew Hagan <mathagan@fb.com>
2022-05-13 15:56:56 +01:00
51 changed files with 2101 additions and 682 deletions

View File

@@ -17,4 +17,10 @@ jobs:
steps:
- run: |
export PR_BRANCH_TAG=$(echo ${GITHUB_HEAD_REF#refs/heads/} | tr '/' '-')
curl -uucentral:${{ secrets.DOCKER_REGISTRY_PASSWORD }} -X DELETE "https://tip.jfrog.io/artifactory/tip-wlan-cloud-ucentral/owgw-ui/$PR_BRANCH_TAG"
if [[ ! $PR_BRANCH_TAG =~ (main|master|release-*) ]]; then
echo "PR branch is $PR_BRANCH_TAG, deleting Docker image"
curl -s -uucentral:${{ secrets.DOCKER_REGISTRY_PASSWORD }} -X DELETE "https://tip.jfrog.io/artifactory/tip-wlan-cloud-ucentral/owgw-ui/$PR_BRANCH_TAG"
else
echo "PR branch is $PR_BRANCH_TAG, not deleting Docker image"
fi

View File

@@ -1,4 +1,4 @@
FROM node:14-alpine3.11 AS build
FROM node:18.7.0-alpine3.15 AS build
COPY package.json package-lock.json /
@@ -8,7 +8,7 @@ COPY . .
RUN npm run build
FROM nginx:1.20.1-alpine AS runtime
FROM nginx:1.22.0-alpine AS runtime
COPY --from=build /build/ /usr/share/nginx/html/

View File

@@ -8,7 +8,7 @@ fullnameOverride: ""
images:
owgwui:
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
tag: main
tag: v2.7.0
pullPolicy: Always
services:

522
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.6.25",
"version": "2.7.0(8)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.6.25",
"version": "2.7.0(8)",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -16,6 +16,7 @@
"apexcharts": "^3.27.1",
"axios": "^0.21.1",
"axios-retry": "^3.1.9",
"buffer": "^6.0.3",
"dagre": "^0.8.5",
"i18next": "^20.3.1",
"i18next-browser-languagedetector": "^6.1.2",
@@ -34,7 +35,7 @@
"react-tooltip": "^4.2.21",
"react-widgets": "^5.1.1",
"sass": "^1.35.1",
"ucentral-libs": "^1.0.60",
"ucentral-libs": "^1.0.61",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -2068,6 +2069,64 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2960,7 +3019,7 @@
"node_modules/ansi-html": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
"integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
"integrity": "sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA==",
"dev": true,
"engines": [
"node >= 0.8.0"
@@ -3432,6 +3491,25 @@
"node": ">=0.10.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -3574,6 +3652,29 @@
"url": "https://opencollective.com/browserslist"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -6221,9 +6322,9 @@
}
},
"node_modules/eventsource": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz",
"integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz",
"integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==",
"dev": true,
"dependencies": {
"original": "^1.0.0"
@@ -7277,60 +7378,6 @@
"node": ">=12"
}
},
"node_modules/html-minifier-terser/node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/html-minifier-terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/html-minifier-terser/node_modules/terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"acorn": "^8.5.0"
},
"peerDependenciesMeta": {
"acorn": {
"optional": true
}
}
},
"node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -7786,6 +7833,25 @@
"postcss": "^8.1.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -8445,12 +8511,6 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
"node_modules/json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
"integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==",
"dev": true
},
"node_modules/json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
@@ -9145,9 +9205,9 @@
}
},
"node_modules/moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
@@ -11739,9 +11799,9 @@
"dev": true
},
"node_modules/semver-regex": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz",
"integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz",
"integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==",
"dev": true,
"engines": {
"node": ">=8"
@@ -12244,17 +12304,22 @@
}
},
"node_modules/sockjs-client": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz",
"integrity": "sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.0.tgz",
"integrity": "sha512-qVHJlyfdHFht3eBFZdKEXKTlb7I4IV41xnVNo8yUKA1UHcPJwgW2SvTq9LhnjjCywSkSK7c/e4nghU0GOoMCRQ==",
"dev": true,
"dependencies": {
"debug": "^3.2.6",
"eventsource": "^1.0.7",
"faye-websocket": "^0.11.3",
"debug": "^3.2.7",
"eventsource": "^1.1.0",
"faye-websocket": "^0.11.4",
"inherits": "^2.0.4",
"json3": "^3.3.3",
"url-parse": "^1.5.3"
"url-parse": "^1.5.10"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://tidelift.com/funding/github/npm/sockjs-client"
}
},
"node_modules/sockjs-client/node_modules/debug": {
@@ -13012,6 +13077,24 @@
"node": ">=6"
}
},
"node_modules/terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz",
@@ -13046,26 +13129,6 @@
}
}
},
"node_modules/terser-webpack-plugin/node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/terser-webpack-plugin/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/terser-webpack-plugin/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -13131,39 +13194,23 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/terser-webpack-plugin/node_modules/terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"node_modules/terser/node_modules/acorn": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"dev": true,
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
"acorn": "bin/acorn"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"acorn": "^8.5.0"
},
"peerDependenciesMeta": {
"acorn": {
"optional": true
}
"node": ">=0.4.0"
}
},
"node_modules/terser-webpack-plugin/node_modules/terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true,
"engines": {
"node": ">= 8"
}
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/text-table": {
"version": "0.2.0",
@@ -13359,9 +13406,9 @@
}
},
"node_modules/ucentral-libs": {
"version": "1.0.60",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-1.0.60.tgz",
"integrity": "sha512-PRw2QTcbnHdrA8rPQhREI1FOKyuZUt48H3KcSGQgHpik2Ni+0una7jRfMFXwOU9yHzxQAlYG0EWLrzBnrKRvGA==",
"version": "1.0.61",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-1.0.61.tgz",
"integrity": "sha512-RMUFLC6PMeh4S1MSkDXYjpQfh4yWeZX5Rm5FTRNbfYfaLKuL8CbRZjnuGPFrgABGQRWk5TITxXQASYBpmOq1dQ==",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -14208,7 +14255,7 @@
"node_modules/webpack-dev-server/node_modules/glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
"dev": true,
"dependencies": {
"is-glob": "^3.1.0",
@@ -16353,6 +16400,55 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -17039,7 +17135,7 @@
"ansi-html": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
"integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
"integrity": "sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA==",
"dev": true
},
"ansi-html-community": {
@@ -17392,6 +17488,11 @@
}
}
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -17511,6 +17612,15 @@
"picocolors": "^1.0.0"
}
},
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -19522,9 +19632,9 @@
"dev": true
},
"eventsource": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz",
"integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz",
"integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==",
"dev": true,
"requires": {
"original": "^1.0.0"
@@ -20349,41 +20459,6 @@
"param-case": "^3.0.4",
"relateurl": "^0.2.7",
"terser": "^5.10.0"
},
"dependencies": {
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
}
}
}
}
},
"html-parse-stringify": {
@@ -20730,6 +20805,11 @@
"dev": true,
"requires": {}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -21199,12 +21279,6 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
"json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
"integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==",
"dev": true
},
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
@@ -21741,9 +21815,9 @@
}
},
"moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"mrmime": {
"version": "1.0.0",
@@ -23616,9 +23690,9 @@
"dev": true
},
"semver-regex": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz",
"integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz",
"integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==",
"dev": true
},
"send": {
@@ -24042,17 +24116,16 @@
}
},
"sockjs-client": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.2.tgz",
"integrity": "sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.0.tgz",
"integrity": "sha512-qVHJlyfdHFht3eBFZdKEXKTlb7I4IV41xnVNo8yUKA1UHcPJwgW2SvTq9LhnjjCywSkSK7c/e4nghU0GOoMCRQ==",
"dev": true,
"requires": {
"debug": "^3.2.6",
"eventsource": "^1.0.7",
"faye-websocket": "^0.11.3",
"debug": "^3.2.7",
"eventsource": "^1.1.0",
"faye-websocket": "^0.11.4",
"inherits": "^2.0.4",
"json3": "^3.3.3",
"url-parse": "^1.5.3"
"url-parse": "^1.5.10"
},
"dependencies": {
"debug": {
@@ -24649,6 +24722,32 @@
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true
},
"terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"dependencies": {
"acorn": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"dev": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
}
}
},
"terser-webpack-plugin": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz",
@@ -24662,20 +24761,6 @@
"terser": "^5.7.2"
},
"dependencies": {
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -24718,25 +24803,6 @@
"requires": {
"has-flag": "^4.0.0"
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
}
}
}
}
},
@@ -24902,9 +24968,9 @@
}
},
"ucentral-libs": {
"version": "1.0.60",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-1.0.60.tgz",
"integrity": "sha512-PRw2QTcbnHdrA8rPQhREI1FOKyuZUt48H3KcSGQgHpik2Ni+0una7jRfMFXwOU9yHzxQAlYG0EWLrzBnrKRvGA==",
"version": "1.0.61",
"resolved": "https://registry.npmjs.org/ucentral-libs/-/ucentral-libs-1.0.61.tgz",
"integrity": "sha512-RMUFLC6PMeh4S1MSkDXYjpQfh4yWeZX5Rm5FTRNbfYfaLKuL8CbRZjnuGPFrgABGQRWk5TITxXQASYBpmOq1dQ==",
"requires": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -25587,7 +25653,7 @@
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
"dev": true,
"requires": {
"is-glob": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.6.25",
"version": "2.7.0(8)",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -10,6 +10,7 @@
"apexcharts": "^3.27.1",
"axios": "^0.21.1",
"axios-retry": "^3.1.9",
"buffer": "^6.0.3",
"dagre": "^0.8.5",
"i18next": "^20.3.1",
"i18next-browser-languagedetector": "^6.1.2",
@@ -28,7 +29,7 @@
"react-tooltip": "^4.2.21",
"react-widgets": "^5.1.1",
"sass": "^1.35.1",
"ucentral-libs": "^1.0.60",
"ucentral-libs": "^1.0.61",
"uuid": "^8.3.2"
},
"main": "index.js",

View File

@@ -8,6 +8,7 @@
"factory_reset": "Auf Werkseinstellungen zurückgesetzt",
"firmware_upgrade": "Firmware Aktualisierung",
"reboot": "Gerät neustarten",
"request_ie": "Fordern Sie IEs an",
"telemetry": "Telemetrie",
"title": "Geräte Administrations",
"trace": "Tcpdump starten",
@@ -27,6 +28,7 @@
"error": "Fehler beim Senden des Befehls!",
"error_delete_log": "Fehler beim Versuch zu löschen: {{error}}",
"event_queue": "Ereigniswarteschlange",
"reboot_start": "Der Neustartvorgang hat begonnen!",
"success": "Befehl wurde erfolgreich übermittelt",
"title": "Gerätebefehle",
"unable_queue": "Anfrage für Ereigniswarteschlange kann nicht abgeschlossen werden: {{error}}"
@@ -148,6 +150,7 @@
"need_date": "Du brauchst ein Datum...",
"no": "Nein",
"no_addresses_found": "Keine Adressen gefunden",
"no_clients_found": "Keine Kunden gefunden",
"no_devices_found": "Keine Geräte gefunden",
"no_items": "Keine Gegenstände",
"none": "Keiner",
@@ -323,15 +326,19 @@
"device": {
"add_to_blacklist": "Gerät zur Blacklist hinzufügen",
"all_devices": "Alle Geräte",
"already_running_command": "Gerät führt bereits einen Befehl aus, bitte versuchen Sie es später erneut",
"blacklisted_on": "Datum",
"capabilities": "Fähigkeiten",
"certificate_explanation": "Zertifikate der angeschlossenen Geräte",
"count_explanation": "Geräte, die auf diese Gateway-Instanz verweisen",
"edit_blacklist": "Gerät auf der schwarzen Liste bearbeiten",
"error_adding_blacklist": "Fehler beim Hinzufügen des Geräts zur Blacklist: {{error}}",
"error_edit_blacklist": "Fehler beim Bearbeiten der schwarzen Liste: {{error}}",
"error_fetching_device": "Fehler beim Abrufen der Geräteinformationen: {{error}}",
"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}",
"firmware_count_explanation": "Dies ist die Gesamtzahl der Geräte, die diesem Firmware-Server hinzugefügt wurden, einschließlich der Geräte, die derzeit nicht auf den zugehörigen Gateway-Server verweisen.",
"health_explanation": "Zustand der verbundenen Geräte ((Geräte = 100 % * 100 + Geräte > 90 % * 95 + Geräte > 60 % * 75 + Geräte < 60 % * 35) / Verbundene Geräte)",
"mac_not_found": "Seriennummer nicht gefunden, Sie werden zur Seite „Geräte“ weitergeleitet",
"memory_explanation": "Anzahl verbundener Geräte mit entsprechendem belegtem Speicher %",
"remove_from_blacklist": "Von der schwarzen Liste entfernen",
"success_added_blacklist": "Gerät erfolgreich zur Blacklist hinzugefügt!",
@@ -424,7 +431,7 @@
"to_release": "Zu",
"unknown_firmware_status": "Unbekannter Firmware-Status",
"upgrade": "Aktualisierung",
"upgrade_command_submitted": "Upgrade-Befehl erfolgreich gesendet",
"upgrade_command_submitted": "Aktualisierung läuft...",
"upgrade_to_latest": "Neueste",
"upgrade_to_version": "Upgrade auf diese Revision",
"upgrading": "Upgrade durchführen..."
@@ -715,6 +722,8 @@
"connection_failed": "Verbindung konnte nicht hergestellt werden. Fehler: {{error}}",
"interval": "Intervall",
"last_update": "Letztes Update",
"lifetime": "Dauer",
"outputmode": "Ausgabemodus",
"types": "Typen"
},
"trace": {
@@ -725,7 +734,7 @@
"title": "Tcpdump",
"trace": "Spur",
"trace_not_successful": "Trace nicht erfolgreich: Gateway hat folgenden Fehler gemeldet: {{error}}",
"wait_for_file": "Möchten Sie warten, bis die Trace-Datei fertig ist?",
"wait_for_file": "Warten, bis die Trace-Datei fertig ist?",
"waiting_directions": "Bitte warten Sie auf die Trace-Datendatei. Dies könnte eine Weile dauern. Sie können das Warten beenden und die Ablaufverfolgungsdatei später aus der Befehlstabelle abrufen.",
"waiting_seconds": "Verstrichene Zeit: {{seconds}} Sekunden"
},
@@ -809,6 +818,7 @@
"radios": "Radios",
"scan_warning": "Ihr 5G-Funkgerät befindet sich auf einem Radarkanal, Sie müssen „Override DFS“ aktivieren, um das Scannen aller 5G-Kanäle zu ermöglichen",
"title": "WLAN-Analyse",
"vendor": "Verkäufer"
"vendor": "Verkäufer",
"waiting_for_data": "Warten auf Empfang von Gerätedaten. Bitte schauen Sie später noch einmal nach"
}
}

View File

@@ -8,6 +8,7 @@
"factory_reset": "Factory Reset",
"firmware_upgrade": "Firmware Upgrade",
"reboot": "Reboot",
"request_ie": "Request IEs",
"telemetry": "Telemetry",
"title": "Commands",
"trace": "Trace",
@@ -27,6 +28,7 @@
"error": "Error while submitting command!",
"error_delete_log": "Error while trying to delete: {{error}}",
"event_queue": "Event Queue",
"reboot_start": "Reboot process has started!",
"success": "Command submitted successfully, you can look at the Commands log for the result",
"title": "Command History",
"unable_queue": "Unable to complete event queue request: {{error}}"
@@ -148,6 +150,7 @@
"need_date": "You need a date...",
"no": "No",
"no_addresses_found": "No Addresses Found",
"no_clients_found": "No Clients Found",
"no_devices_found": "No Devices Found",
"no_items": "No Items",
"none": "None",
@@ -323,15 +326,19 @@
"device": {
"add_to_blacklist": "Add Device To Blacklist",
"all_devices": "All Devices",
"already_running_command": "Device is already executing a command, please try later",
"blacklisted_on": "Date",
"capabilities": "Capabilities",
"certificate_explanation": "Certificates of connected devices",
"count_explanation": "Devices pointing at this gateway instance",
"edit_blacklist": "Edit Blacklisted Device",
"error_adding_blacklist": "Error adding device to blacklist: {{error}}",
"error_edit_blacklist": "Error editing blacklist: {{error}}",
"error_fetching_device": "Error fetching device information: {{error}}",
"error_fetching_devices": "Error while fetching devices: {{error}}",
"firmware_count_explanation": "This is the total amount of devices that were added to this firmware server, including devices not currently pointing at the related gateway server.",
"health_explanation": "Health of connected devices ((Devices=100% * 100 + Devices>90% * 95 + Devices>60% * 75 + Devices<60% * 35) / ConnectedDevices)",
"mac_not_found": "Serial number not found, redirecting you to the Devices page",
"memory_explanation": "Amount of connected devices with corresponding memory used percentage",
"remove_from_blacklist": "Remove from blacklist",
"success_added_blacklist": "Device successfully added to blacklist!",
@@ -424,7 +431,7 @@
"to_release": "To",
"unknown_firmware_status": "Unknown Firmware Status",
"upgrade": "Upgrade",
"upgrade_command_submitted": "Upgrade Command Submitted Successfully",
"upgrade_command_submitted": "Upgrade in progress...",
"upgrade_to_latest": "Latest",
"upgrade_to_version": "Upgrade to this Revision",
"upgrading": "Upgrading..."
@@ -715,6 +722,8 @@
"connection_failed": "Failed to create connection. Error: {{error}}",
"interval": "Interval",
"last_update": "Last Update",
"lifetime": "Duration",
"outputmode": "Output Mode",
"types": "Types"
},
"trace": {
@@ -725,7 +734,7 @@
"title": "Trace",
"trace": "Trace",
"trace_not_successful": "Trace not successful: gateway reported the following error : {{error}}",
"wait_for_file": "Would you like to wait until the trace file is ready?",
"wait_for_file": "Wait until the trace file is ready?",
"waiting_directions": "Please wait for the trace data file. This may take some time. You can exit the wait and retrieve the trace file from the commands table later.",
"waiting_seconds": "Time Elapsed: {{seconds}} seconds"
},
@@ -809,6 +818,7 @@
"radios": "Radios",
"scan_warning": "Your 5G radio is on a radar channel, you must enable “Override DFS” to allow scanning of all 5G channels",
"title": "Wi-Fi Analysis",
"vendor": "Vendor"
"vendor": "Vendor",
"waiting_for_data": "Waiting to receive device data. Please check again later"
}
}

View File

@@ -8,6 +8,7 @@
"factory_reset": "Restablecimiento De Fábrica",
"firmware_upgrade": "Actualización de firmware",
"reboot": "Reiniciar",
"request_ie": "Solicitar IE",
"telemetry": "Telemetria",
"title": "Comandos",
"trace": "Rastro",
@@ -27,6 +28,7 @@
"error": "¡Error al enviar el comando!",
"error_delete_log": "Error al intentar eliminar: {{error}}",
"event_queue": "Cola de eventos",
"reboot_start": "¡El proceso de reinicio ha comenzado!",
"success": "Comando enviado con éxito, puede consultar el registro de Comandos para ver el resultado",
"title": "Historial de Comandos",
"unable_queue": "No se pudo completar la solicitud de cola de eventos: {{error}}"
@@ -148,6 +150,7 @@
"need_date": "Necesitas una cita ...",
"no": "No",
"no_addresses_found": "No se encontraron direcciones",
"no_clients_found": "No se encontraron clientes",
"no_devices_found": "No se encontraron dispositivos",
"no_items": "No hay articulos",
"none": "Ninguna",
@@ -323,15 +326,19 @@
"device": {
"add_to_blacklist": "Agregar dispositivo a la lista negra",
"all_devices": "Todos los dispositivos",
"already_running_command": "El dispositivo ya está ejecutando un comando, intente más tarde",
"blacklisted_on": "Fecha",
"capabilities": "capacidades",
"certificate_explanation": "Certificados de dispositivos conectados",
"count_explanation": "Dispositivos que apuntan a esta instancia de puerta de enlace",
"edit_blacklist": "Editar dispositivo incluido en la lista negra",
"error_adding_blacklist": "Error al agregar el dispositivo a la lista negra: {{error}}",
"error_edit_blacklist": "Error al editar la lista negra: {{error}}",
"error_fetching_device": "Error al obtener la información del dispositivo: {{error}}",
"error_fetching_devices": "Error al recuperar dispositivos: {{error}}",
"firmware_count_explanation": "Esta es la cantidad total de dispositivos que se agregaron a este servidor de firmware, incluidos los dispositivos que actualmente no apuntan al servidor de puerta de enlace relacionado.",
"health_explanation": "Estado de los dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos conectados)",
"mac_not_found": "Número de serie no encontrado, lo redirige a la página Dispositivos",
"memory_explanation": "Cantidad de dispositivos conectados con la memoria correspondiente utilizada%",
"remove_from_blacklist": "ELIMINAR DE LA LISTA NEGRA",
"success_added_blacklist": "¡Dispositivo agregado exitosamente a la lista negra!",
@@ -424,7 +431,7 @@
"to_release": "A",
"unknown_firmware_status": "Estado de firmware desconocido",
"upgrade": "Mejorar",
"upgrade_command_submitted": "El comando de actualización se envió correctamente",
"upgrade_command_submitted": "Actualización en curso...",
"upgrade_to_latest": "último",
"upgrade_to_version": "Actualizar a esta revisión",
"upgrading": "Actualizando ..."
@@ -715,6 +722,8 @@
"connection_failed": "No se pudo crear la conexión. Error: {{error}}",
"interval": "intervalo",
"last_update": "Última actualización",
"lifetime": "Duración",
"outputmode": "Modo salida",
"types": "Los tipos"
},
"trace": {
@@ -725,7 +734,7 @@
"title": "Rastro",
"trace": "Rastro",
"trace_not_successful": "Seguimiento fallido: la puerta de enlace informó el siguiente error: {{error}}",
"wait_for_file": "¿Le gustaría esperar hasta que el archivo de seguimiento esté listo?",
"wait_for_file": "¿Esperar hasta que el archivo de rastreo esté listo?",
"waiting_directions": "Espere el archivo de datos de seguimiento. Esto puede tomar algo de tiempo. Puede salir de la espera y recuperar el archivo de seguimiento de la tabla de comandos más tarde.",
"waiting_seconds": "Tiempo transcurrido: {{seconds}} segundos"
},
@@ -809,6 +818,7 @@
"radios": "Radios",
"scan_warning": "Su radio 5G está en un canal de radar, debe habilitar \"Anular DFS\" para permitir el escaneo de todos los canales 5G",
"title": "Análisis de Wi-Fi",
"vendor": "Vendedor"
"vendor": "Vendedor",
"waiting_for_data": "Esperando recibir datos del dispositivo. Vuelva a consultar más tarde"
}
}

View File

@@ -8,6 +8,7 @@
"factory_reset": "Retour aux paramètres d'usine",
"firmware_upgrade": "Mise à jour du firmware",
"reboot": "Redémarrer",
"request_ie": "Demander des IE",
"telemetry": "Télémétrie",
"title": "Les commandes",
"trace": "Trace",
@@ -27,6 +28,7 @@
"error": "Erreur lors de la soumission de la commande !",
"error_delete_log": "Erreur lors de la tentative de suppression : {{error}}",
"event_queue": "File d'attente d'événements",
"reboot_start": "Le processus de redémarrage a commencé !",
"success": "Commande soumise avec succès, vous pouvez consulter le journal des commandes pour le résultat",
"title": "Historique des commandes",
"unable_queue": "Impossible de terminer la demande de file d'attente d'événements: {{error}}"
@@ -148,6 +150,7 @@
"need_date": "Vous avez besoin d'un rendez-vous...",
"no": "Non",
"no_addresses_found": "Aucune adresse trouvée",
"no_clients_found": "Aucun client trouvé",
"no_devices_found": "Aucun périphérique trouvé",
"no_items": "Pas d'objet",
"none": "Aucun",
@@ -323,15 +326,19 @@
"device": {
"add_to_blacklist": "Ajouter un appareil à la liste noire",
"all_devices": "Tous les dispositifs",
"already_running_command": "L'appareil exécute déjà une commande, veuillez réessayer plus tard",
"blacklisted_on": "Rendez-vous amoureux",
"capabilities": "Capacités",
"certificate_explanation": "Certificats des appareils connectés",
"count_explanation": "Périphériques pointant vers cette instance de passerelle",
"edit_blacklist": "Modifier l'appareil sur liste noire",
"error_adding_blacklist": "Erreur lors de l'ajout de l'appareil à la liste noire : {{error}}",
"error_edit_blacklist": "Erreur lors de la modification de la liste noire : {{error}}",
"error_fetching_device": "Erreur lors de la récupération des informations sur l'appareil : {{error}}",
"error_fetching_devices": "Erreur lors de la récupération des appareils : {{error}}",
"firmware_count_explanation": "Il s'agit du nombre total d'appareils qui ont été ajoutés à ce serveur de micrologiciel, y compris les appareils qui ne pointent pas actuellement vers le serveur de passerelle associé.",
"health_explanation": "Santé des appareils connectés ((Appareils = 100 % * 100 + Appareils> 90 % * 95 + Appareils> 60 % * 75 + Appareils < 60 % * 35) / Appareils connectés)",
"mac_not_found": "Numéro de série introuvable, vous redirigeant vers la page Appareils",
"memory_explanation": "Nombre d'appareils connectés avec la mémoire correspondante utilisée %",
"remove_from_blacklist": "Supprimer de la liste noire",
"success_added_blacklist": "Appareil ajouté avec succès à la liste noire !",
@@ -424,7 +431,7 @@
"to_release": "à",
"unknown_firmware_status": "État du micrologiciel inconnu",
"upgrade": "Améliorer",
"upgrade_command_submitted": "Commande de mise à niveau soumise avec succès",
"upgrade_command_submitted": "Mise à jour en cours...",
"upgrade_to_latest": "Dernier",
"upgrade_to_version": "Mettre à niveau vers cette révision",
"upgrading": "Mise à niveau..."
@@ -715,6 +722,8 @@
"connection_failed": "Échec de la création de la connexion. Erreur : {{error}}",
"interval": "Intervalle",
"last_update": "Dernière mise à jour",
"lifetime": "Durée",
"outputmode": "Mode de sortie",
"types": "Les types"
},
"trace": {
@@ -725,7 +734,7 @@
"title": "Trace",
"trace": "Trace",
"trace_not_successful": "Trace non réussie : la passerelle a signalé l'erreur suivante : {{error}}",
"wait_for_file": "Souhaitez-vous attendre que le fichier de trace soit prêt ?",
"wait_for_file": "Attendre que le fichier de trace soit prêt ?",
"waiting_directions": "Veuillez attendre le fichier de données de trace. Cela peut prendre un certain temps. Vous pouvez quitter l'attente et récupérer le fichier de trace de la table des commandes plus tard.",
"waiting_seconds": "Temps écoulé : {{seconds}} secondes"
},
@@ -809,6 +818,7 @@
"radios": "Radios",
"scan_warning": "Votre radio 5G est sur un canal radar, vous devez activer \"Override DFS\" pour permettre le balayage de tous les canaux 5G",
"title": "Analyse Wi-Fi",
"vendor": "vendeur"
"vendor": "vendeur",
"waiting_for_data": "En attente de réception des données de l'appareil. Veuillez revérifier plus tard"
}
}

View File

@@ -8,6 +8,7 @@
"factory_reset": "Restauração de fábrica",
"firmware_upgrade": "Atualização de firmware",
"reboot": "Reiniciar",
"request_ie": "Solicitar IEs",
"telemetry": "Telemetria",
"title": "Comandos",
"trace": "Vestígio",
@@ -27,6 +28,7 @@
"error": "Erro ao enviar comando!",
"error_delete_log": "Erro ao tentar excluir: {{error}}",
"event_queue": "Fila de Eventos",
"reboot_start": "O processo de reinicialização foi iniciado!",
"success": "Comando enviado com sucesso, você pode consultar o log de Comandos para ver o resultado",
"title": "Histórico de Comandos",
"unable_queue": "Incapaz de completar o pedido de fila de eventos: {{error}}"
@@ -148,6 +150,7 @@
"need_date": "Você precisa de um encontro ...",
"no": "Não",
"no_addresses_found": "Nenhum endereço encontrado",
"no_clients_found": "Nenhum cliente encontrado",
"no_devices_found": "Nenhum dispositivo encontrado",
"no_items": "Nenhum item",
"none": "Nenhum",
@@ -323,15 +326,19 @@
"device": {
"add_to_blacklist": "Adicionar dispositivo à lista negra",
"all_devices": "Todos os dispositivos",
"already_running_command": "O dispositivo já está executando um comando, tente mais tarde",
"blacklisted_on": "Encontro",
"capabilities": "Recursos",
"certificate_explanation": "Certificados de dispositivos conectados",
"count_explanation": "Dispositivos apontando para esta instância de gateway",
"edit_blacklist": "Editar dispositivo na lista negra",
"error_adding_blacklist": "Erro ao adicionar dispositivo à lista negra: {{error}}",
"error_edit_blacklist": "Erro ao editar a lista negra: {{error}}",
"error_fetching_device": "Erro ao buscar informações do dispositivo: {{error}}",
"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}",
"firmware_count_explanation": "Esta é a quantidade total de dispositivos que foram adicionados a este servidor de firmware, incluindo dispositivos que não estão apontando para o servidor de gateway relacionado.",
"health_explanation": "Integridade dos dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos Conectados)",
"mac_not_found": "Número de série não encontrado, redirecionando você para a página Dispositivos",
"memory_explanation": "Quantidade de dispositivos conectados com a memória correspondente usada%",
"remove_from_blacklist": "Remover da lista negra",
"success_added_blacklist": "Dispositivo adicionado à lista negra com sucesso!",
@@ -424,7 +431,7 @@
"to_release": "Para",
"unknown_firmware_status": "Status de firmware desconhecido",
"upgrade": "Melhorar",
"upgrade_command_submitted": "Comando de atualização enviado com sucesso",
"upgrade_command_submitted": "Atualização em andamento...",
"upgrade_to_latest": "Mais recentes",
"upgrade_to_version": "Atualize para esta revisão",
"upgrading": "Atualizando ..."
@@ -715,6 +722,8 @@
"connection_failed": "Falha ao criar conexão. Erro: {{error}}",
"interval": "intervalo",
"last_update": "Última atualização",
"lifetime": "Duração",
"outputmode": "Modo saída",
"types": "Tipos"
},
"trace": {
@@ -725,7 +734,7 @@
"title": "Vestígio",
"trace": "Vestígio",
"trace_not_successful": "O rastreamento não foi bem-sucedido: o gateway relatou o seguinte erro: {{error}}",
"wait_for_file": "Você gostaria de esperar até que o arquivo de rastreamento esteja pronto?",
"wait_for_file": "Esperar até que o arquivo de rastreamento esteja pronto?",
"waiting_directions": "Aguarde o arquivo de dados de rastreamento. Isto pode tomar algum tempo. Você pode sair da espera e recuperar o arquivo de rastreamento da tabela de comandos mais tarde.",
"waiting_seconds": "Tempo decorrido: {{seconds}} segundos"
},
@@ -809,6 +818,7 @@
"radios": "Rádios",
"scan_warning": "Seu rádio 5G está em um canal de radar, você deve habilitar “Override DFS” para permitir a varredura de todos os canais 5G",
"title": "Análise de Wi-Fi",
"vendor": "fornecedor"
"vendor": "fornecedor",
"waiting_for_data": "Aguardando para receber dados do dispositivo. Verifique novamente mais tarde"
}
}

View File

@@ -72,7 +72,18 @@ const BlinkModal = ({ show, toggleModal }) => {
}
toggleModal();
})
.catch(() => {
.catch((e) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setResult('error');
})
.finally(() => {

View File

@@ -205,10 +205,11 @@ const DeviceCommands = () => {
const columns = [
{ key: 'submitted', label: t('common.submitted'), filter: false, _style: { width: '20%' } },
{ key: 'command', label: t('common.command'), _style: { width: '15%' } },
{ key: 'command', label: t('common.command'), _style: { width: '0%' } },
{ key: 'status', label: t('common.status'), _style: { width: '0%' } },
{ key: 'executed', label: t('common.executed'), filter: false, _style: { width: '16%' } },
{ key: 'completed', label: t('common.completed'), filter: false, _style: { width: '16%' } },
{ key: 'errorCode', label: t('common.error_code'), filter: false, _style: { width: '8%' } },
{ key: 'errorCode', label: t('common.error_code'), filter: false },
{
key: 'show_buttons',
label: '',
@@ -317,16 +318,17 @@ const DeviceCommands = () => {
{item.completed && item.completed !== 0 ? (
<FormattedDate date={item.completed} />
) : (
'Pending'
'-'
)}
</td>
),
status: (item) => <td className="align-middle">{item.status}</td>,
executed: (item) => (
<td className="align-middle">
{item.executed && item.executed !== 0 ? (
<FormattedDate date={item.executed} />
) : (
'Pending'
'-'
)}
</td>
),
@@ -335,7 +337,7 @@ const DeviceCommands = () => {
{item.submitted && item.submitted !== '' ? (
<FormattedDate date={item.submitted} />
) : (
'Pending'
'-'
)}
</td>
),
@@ -358,6 +360,7 @@ const DeviceCommands = () => {
shape="square"
size="sm"
className="mx-2"
disabled={item.completed === 0}
onClick={() => {
toggleDetails(item);
}}

View File

@@ -103,12 +103,17 @@ const ConfigureModal = ({ show, toggleModal }) => {
})
.catch((e) => {
setResponseBody('Error while submitting command!');
addToast({
title: t('common.error'),
body: `${t('common.general_error')}: ${e.response?.data?.ErrorDescription}`,
color: 'danger',
autohide: true,
});
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setHadFailure(true);
})
.finally(() => {

View File

@@ -54,12 +54,17 @@ const DeviceActions = ({ device }) => {
if (newWindow) newWindow.opener = null;
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('connect.error_trying_to_connect', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
})
.finally(() => {
setConnectLoading(false);
@@ -68,18 +73,20 @@ const DeviceActions = ({ device }) => {
useEffect(() => {
if (upgradeStatus.result !== undefined) {
addToast({
title: upgradeStatus.result.success ? t('common.success') : t('common.error'),
body: upgradeStatus.result.success
? t('firmware.upgrade_command_submitted')
: upgradeStatus.result.error,
color: upgradeStatus.result.success ? 'success' : 'danger',
autohide: true,
});
if (upgradeStatus.result.success) {
addToast({
title: upgradeStatus.result.success ? t('common.success') : t('common.error'),
body: upgradeStatus.result.success
? t('firmware.upgrade_command_submitted')
: upgradeStatus.result.error,
color: upgradeStatus.result.success ? 'success' : 'danger',
autohide: true,
});
setShowUpgradeModal(false);
}
setUpgradeStatus({
loading: false,
});
setShowUpgradeModal(false);
}
}, [upgradeStatus]);

View File

@@ -111,7 +111,18 @@ const DeviceFirmwareModal = ({
},
});
})
.catch(() => {
.catch((e) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setUpgradeStatus({
loading: false,
result: {

View File

@@ -414,6 +414,8 @@ const DeviceListTable = ({
nextClassName="page-item"
nextLinkClassName="page-link"
activeClassName="active"
pageRangeDisplayed={5}
marginPagesDisplayed={1}
/>
</div>
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>

View File

@@ -21,6 +21,7 @@ const DeviceList = () => {
const [overrides, setOverrides] = useState({});
const [page, setPage] = useState(parseInt(sessionStorage.getItem('deviceTable') ?? 0, 10));
const { currentToken, endpoints } = useAuth();
const [deviceToRefresh, setDeviceToRefresh] = useState(undefined);
const [upgradeStatus, setUpgradeStatus] = useState({
loading: false,
});
@@ -372,14 +373,17 @@ const DeviceList = () => {
getCount();
}, []);
useEffect(() => {
if (deviceToRefresh) refreshDevice(deviceToRefresh.serial);
}, [deviceToRefresh?.timestamp]);
useEffect(() => {
if (lastMessage && lastMessage.type === 'DEVICE') {
const { serialNumber: msgSerial, isConnected } = lastMessage;
if (overrides[msgSerial] === undefined || overrides[msgSerial] !== isConnected) {
setOverrides({ ...overrides, [msgSerial]: isConnected });
}
const { serialNumber: msgSerial, timestamp } = lastMessage;
if (timestamp !== deviceToRefresh?.timestamp)
setDeviceToRefresh({ serial: msgSerial, timestamp });
}
}, [lastMessage, overrides]);
}, [lastMessage, deviceToRefresh]);
useEffect(() => {
if (upgradeStatus.result !== undefined) {

View File

@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Select, { components } from 'react-select';
import { useTranslation } from 'react-i18next';
const DeviceSearchBarInput = ({ search, results, history, action, isDisabled }) => {
const { t } = useTranslation();
const [selected, setSelected] = useState('');
const NoOptionsMessage = (props) => (
<components.NoOptionsMessage {...props}>
<span>{t('common.no_devices_found')}</span>
</components.NoOptionsMessage>
);
const onInputChange = (value) => {
if (value === '' || value.match('^[a-fA-F0-9-*]+$')) {
setSelected(value);
search(value);
}
};
return (
<Select
components={{ NoOptionsMessage }}
options={results.map((serial) => ({ label: serial, value: serial }))}
filterOption={() => true}
inputValue={selected}
placeholder={t('common.search')}
isDisabled={isDisabled}
styles={{
placeholder: (provided) => ({
...provided,
// disable placeholder mouse events
pointerEvents: 'none',
userSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
msUserSelect: 'none',
}),
input: (css) => ({
...css,
/* expand the Input Component div */
flex: '1 1 auto',
/* expand the Input Component child div */
'> div': {
width: '100%',
},
/* expand the Input Component input */
input: {
width: '100% !important',
textAlign: 'left',
},
}),
}}
onInputChange={onInputChange}
onChange={(property) =>
action === null ? history.push(`/devices/${property.value}`) : action(property.value)
}
/>
);
};
DeviceSearchBarInput.propTypes = {
search: PropTypes.func.isRequired,
results: PropTypes.instanceOf(Array).isRequired,
history: PropTypes.instanceOf(Object).isRequired,
isDisabled: PropTypes.bool.isRequired,
action: PropTypes.func,
};
DeviceSearchBarInput.defaultProps = {
action: null,
};
export default DeviceSearchBarInput;

View File

@@ -1,12 +1,11 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs';
import { useAuth } from 'ucentral-libs';
import { toJson } from 'utils/helper';
import DeviceSearchBarInput from './Input';
const DeviceSearchBar = ({ action }) => {
const { t } = useTranslation();
const history = useHistory();
const { currentToken, endpoints } = useAuth();
const [socket, setSocket] = useState(null);
@@ -14,20 +13,22 @@ const DeviceSearchBar = ({ action }) => {
const [waitingSearch, setWaitingSearch] = useState('');
const search = (value) => {
if (socket.readyState === WebSocket.OPEN) {
if (value.length > 1 && value.match('^[a-fA-F0-9-*]+$')) {
setWaitingSearch('');
socket.send(
JSON.stringify({ command: 'serial_number_search', serial_prefix: value.toLowerCase() }),
);
if (socket) {
if (socket.readyState === WebSocket.OPEN) {
if (value.length > 1 && value.match('^[a-fA-F0-9-*]+$')) {
setWaitingSearch('');
socket.send(
JSON.stringify({ command: 'serial_number_search', serial_prefix: value.toLowerCase() }),
);
} else {
setResults([]);
}
} else if (socket.readyState !== WebSocket.CONNECTING && endpoints?.owgw !== undefined) {
setWaitingSearch(value);
setSocket(new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`));
} else {
setResults([]);
setWaitingSearch(value);
}
} else if (socket.readyState !== WebSocket.CONNECTING) {
setWaitingSearch(value);
setSocket(new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`));
} else {
setWaitingSearch(value);
}
};
@@ -59,12 +60,20 @@ const DeviceSearchBar = ({ action }) => {
}, [socket]);
useEffect(() => {
if (socket === null && endpoints?.owgw) {
if (socket === null && endpoints?.owgw !== undefined) {
setSocket(new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`));
}
}, []);
return <SearchBar t={t} search={search} results={results} history={history} action={action} />;
return (
<DeviceSearchBarInput
search={search}
results={results}
history={history}
action={action}
isDisabled={endpoints.owgw === undefined}
/>
);
};
DeviceSearchBar.propTypes = {

View File

@@ -34,12 +34,17 @@ const EventQueueModal = ({ show, toggle }) => {
setResult(response.data);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('commands.unable_queue', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
})
.finally(() => {
setLoading(false);

View File

@@ -18,7 +18,7 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import 'react-widgets/styles.css';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth, useDevice, useToast } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
@@ -26,6 +26,7 @@ const ConfigureModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [hadSuccess, setHadSuccess] = useState(false);
const [hadFailure, setHadFailure] = useState(false);
const [doingNow, setDoingNow] = useState(false);
@@ -74,7 +75,18 @@ const ConfigureModal = ({ show, toggleModal }) => {
.then(() => {
setHadSuccess(true);
})
.catch(() => {
.catch((e) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setResponseBody(t('commands.error'));
setHadFailure(true);
})

View File

@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CPopover } from '@coreui/react';
import { formatDaysAgo, prettyDate } from 'utils/helper';
const FormattedDate = ({ date, size }) => {
if (size === 'lg') {
return (
<CPopover content={prettyDate(date)} advancedOptions={{ animation: false }}>
<h2 className="d-inline-block">{date === 0 ? '-' : formatDaysAgo(date)}</h2>
</CPopover>
);
}
return (
<CPopover content={prettyDate(date)} advancedOptions={{ animation: false }}>
<span className="d-inline-block">{date === 0 ? '-' : formatDaysAgo(date)}</span>
</CPopover>
);
};
FormattedDate.propTypes = {
date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
size: PropTypes.string,
};
FormattedDate.defaultProps = {
date: 0,
size: 'md',
};
export default FormattedDate;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
@@ -32,6 +32,17 @@ const LatestStatisticsModal = ({ show, toggle }) => {
.catch(() => {});
};
const latestStatsString = useMemo(() => {
if (latestStats) {
try {
return JSON.stringify(latestStats, null, 2);
} catch (e) {
return '';
}
}
return '';
}, [latestStats]);
useEffect(() => {
if (show) {
getLatestStats();
@@ -52,13 +63,9 @@ const LatestStatisticsModal = ({ show, toggle }) => {
</CModalHeader>
<CModalBody>
<div style={{ textAlign: 'right' }}>
<CopyToClipboardButton
t={t}
size="lg"
content={JSON.stringify(latestStats ?? {}, null, 4)}
/>
<CopyToClipboardButton t={t} size="lg" content={latestStatsString} />
</div>
<pre className="ignore">{JSON.stringify(latestStats, null, 2)}</pre>
<pre className="ignore">{latestStatsString}</pre>
</CModalBody>
</CModal>
);

View File

@@ -1,10 +1,10 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { CSpinner } from '@coreui/react';
import { CSpinner, CAlert } from '@coreui/react';
import { useTranslation } from 'react-i18next';
import { v4 as createUuid } from 'uuid';
import axiosInstance from 'utils/axiosInstance';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth } from 'ucentral-libs';
import {
capitalizeFirstLetter,
datesSameDay,
@@ -14,244 +14,267 @@ import {
} from 'utils/helper';
import DeviceStatisticsChart from './DeviceStatisticsChart';
const StatisticsChartList = ({ setOptions, section, setStart, setEnd, time }) => {
const StatisticsChartList = ({ deviceSerialNumber, setOptions, section, time }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const [statOptions, setStatOptions] = useState({
interfaceList: [],
memory: [],
settings: {},
});
const [error, setError] = useState(false);
const transformIntoDataset = (data) => {
let sortedData = data.sort((a, b) => {
if (a.recorded > b.recorded) return 1;
if (b.recorded > a.recorded) return -1;
return 0;
});
try {
let sortedData = data.sort((a, b) => {
if (a.recorded > b.recorded) return 1;
if (b.recorded > a.recorded) return -1;
return 0;
});
const dataLength = sortedData.length;
if (dataLength > 1000 && dataLength < 3000) {
sortedData = sortedData.filter((dat, index) => index % 4 === 0);
} else if (dataLength >= 3000 && dataLength < 5000) {
sortedData = sortedData.filter((dat, index) => index % 8 === 0);
} else if (dataLength >= 5000 && dataLength < 7000) {
sortedData = sortedData.filter((dat, index) => index % 12 === 0);
} else if (dataLength > 7000) {
sortedData = sortedData.filter((dat, index) => index % 20 === 0);
}
const dataLength = sortedData.length;
if (dataLength > 1000 && dataLength < 3000) {
sortedData = sortedData.filter((dat, index) => index % 4 === 0);
} else if (dataLength >= 3000 && dataLength < 5000) {
sortedData = sortedData.filter((dat, index) => index % 8 === 0);
} else if (dataLength >= 5000 && dataLength < 7000) {
sortedData = sortedData.filter((dat, index) => index % 12 === 0);
} else if (dataLength > 7000) {
sortedData = sortedData.filter((dat, index) => index % 20 === 0);
}
// Looping through data to build our memory graph data
const memoryUsed = [
{
titleName: t('statistics.memory'),
name: 'Used',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
{
titleName: t('statistics.memory'),
name: 'Buffered',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
{
titleName: t('statistics.memory'),
name: 'Cached',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
];
// Looping through data to build our memory graph data
const memoryUsed = [
{
titleName: t('statistics.memory'),
name: 'Used',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
{
titleName: t('statistics.memory'),
name: 'Buffered',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
{
titleName: t('statistics.memory'),
name: 'Cached',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: true,
},
];
for (const log of sortedData) {
memoryUsed[0].data.push(
Math.floor((log.data.unit.memory.total - log.data.unit.memory.free) / 1024 / 1024),
for (const log of sortedData) {
memoryUsed[0].data.push(
Math.floor((log.data.unit.memory.total - log.data.unit.memory.free) / 1024 / 1024),
);
memoryUsed[1].data.push(Math.floor(log.data.unit.memory.buffered / 1024 / 1024));
memoryUsed[2].data.push(Math.floor(log.data.unit.memory.cached / 1024 / 1024));
}
const newUsed = memoryUsed[0].data;
if (newUsed.length > 0) newUsed.shift();
memoryUsed[0].data = newUsed;
const newBuff = memoryUsed[1].data;
if (newBuff.length > 0) newBuff.shift();
memoryUsed[1].data = newBuff;
const newCached = memoryUsed[2].data;
if (newCached.length > 0) newCached.shift();
memoryUsed[2].data = newCached;
// This dictionary will have a key that is the interface name and a value of it's index in the final array
const interfaceTypes = {};
const interfaceList = [];
const categories = [];
let i = 0;
const areSameDay = datesSameDay(
new Date(sortedData[0].recorded * 1000),
new Date(sortedData[sortedData.length - 1].recorded * 1000),
);
memoryUsed[1].data.push(Math.floor(log.data.unit.memory.buffered / 1024 / 1024));
memoryUsed[2].data.push(Math.floor(log.data.unit.memory.cached / 1024 / 1024));
}
const newUsed = memoryUsed[0].data;
if (newUsed.length > 0) newUsed.shift();
memoryUsed[0].data = newUsed;
const newBuff = memoryUsed[1].data;
if (newBuff.length > 0) newBuff.shift();
memoryUsed[1].data = newBuff;
const newCached = memoryUsed[2].data;
if (newCached.length > 0) newCached.shift();
memoryUsed[2].data = newCached;
// This dictionary will have a key that is the interface name and a value of it's index in the final array
const interfaceTypes = {};
const interfaceList = [];
const categories = [];
let i = 0;
const areSameDay = datesSameDay(
new Date(sortedData[0].recorded * 1000),
new Date(sortedData[sortedData.length - 1].recorded * 1000),
);
// Just building the array for all the interfaces
for (const log of sortedData) {
categories.push(areSameDay ? unixToTime(log.recorded) : prettyDate(log.recorded));
for (const logInterface of log.data.interfaces) {
if (interfaceTypes[logInterface.name] === undefined) {
interfaceTypes[logInterface.name] = i;
interfaceList.push([
{
titleName: logInterface.name,
name: 'Tx',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: false,
},
{
titleName: logInterface.name,
name: 'Rx',
backgroundColor: 'rgb(0,216,255,0.9)',
data: [],
fill: false,
},
]);
i += 1;
// Just building the array for all the interfaces
for (const log of sortedData) {
categories.push(areSameDay ? unixToTime(log.recorded) : prettyDate(log.recorded));
for (const logInterface of log.data.interfaces) {
if (interfaceTypes[logInterface.name] === undefined) {
interfaceTypes[logInterface.name] = i;
interfaceList.push([
{
titleName: logInterface.name,
name: 'Tx',
backgroundColor: 'rgb(228,102,81,0.9)',
data: [],
fill: false,
},
{
titleName: logInterface.name,
name: 'Rx',
backgroundColor: 'rgb(0,216,255,0.9)',
data: [],
fill: false,
},
]);
i += 1;
}
}
}
}
// Looping through all the data
const prevTxObj = {};
const prevRxObj = {};
for (const log of sortedData) {
// Looping through the interfaces of the log
const version = log.data.version ?? 0;
for (const inter of log.data.interfaces) {
if (version > 0) {
const prevTx = prevTxObj[inter.name] !== undefined ? prevTxObj[inter.name] : 0;
const prevRx = prevTxObj[inter.name] !== undefined ? prevRxObj[inter.name] : 0;
const tx = inter.counters ? Math.floor(inter.counters.tx_bytes / 1024) : 0;
const rx = inter.counters ? Math.floor(inter.counters.rx_bytes / 1024) : 0;
interfaceList[interfaceTypes[inter.name]][0].data.push(Math.max(0, tx - prevTx));
interfaceList[interfaceTypes[inter.name]][1].data.push(Math.max(0, rx - prevRx));
prevTxObj[inter.name] = tx;
prevRxObj[inter.name] = rx;
} else {
interfaceList[interfaceTypes[inter.name]][0].data.push(
inter.counters ? Math.floor(inter.counters.tx_bytes / 1024) : 0,
);
interfaceList[interfaceTypes[inter.name]][1].data.push(
inter.counters ? Math.floor(inter.counters.rx_bytes / 1024) : 0,
);
// Looping through all the data
const prevTxObj = {};
const prevRxObj = {};
for (const log of sortedData) {
// Looping through the interfaces of the log
const version = log.data.version ?? 0;
for (const inter of log.data.interfaces) {
if (version > 0) {
const prevTx = prevTxObj[inter.name] !== undefined ? prevTxObj[inter.name] : 0;
const prevRx = prevTxObj[inter.name] !== undefined ? prevRxObj[inter.name] : 0;
const tx = inter.counters ? Math.floor(inter.counters.tx_bytes / 1024) : 0;
const rx = inter.counters ? Math.floor(inter.counters.rx_bytes / 1024) : 0;
interfaceList[interfaceTypes[inter.name]][0].data.push(Math.max(0, tx - prevTx));
interfaceList[interfaceTypes[inter.name]][1].data.push(Math.max(0, rx - prevRx));
prevTxObj[inter.name] = tx;
prevRxObj[inter.name] = rx;
} else {
interfaceList[interfaceTypes[inter.name]][0].data.push(
inter.counters ? Math.floor(inter.counters.tx_bytes / 1024) : 0,
);
interfaceList[interfaceTypes[inter.name]][1].data.push(
inter.counters ? Math.floor(inter.counters.rx_bytes / 1024) : 0,
);
}
}
}
}
for (let y = 0; y < interfaceList.length; y += 1) {
for (let z = 0; z < interfaceList[y].length; z += 1) {
const newArray = interfaceList[y][z].data;
if (newArray.length > 0) newArray.shift();
interfaceList[y][z].data = newArray;
for (let y = 0; y < interfaceList.length; y += 1) {
for (let z = 0; z < interfaceList[y].length; z += 1) {
const newArray = interfaceList[y][z].data;
if (newArray.length > 0) newArray.shift();
interfaceList[y][z].data = newArray;
}
}
}
const newCategories = categories;
if (newCategories.length > 0) newCategories.shift();
const interfaceOptions = {
chart: {
id: 'chart',
},
stroke: {
curve: 'smooth',
},
xaxis: {
title: {
text: 'Time',
style: {
fontSize: '15px',
const newCategories = categories;
if (newCategories.length > 0) newCategories.shift();
const interfaceOptions = {
chart: {
id: 'chart',
},
stroke: {
curve: 'smooth',
},
xaxis: {
title: {
text: 'Time',
style: {
fontSize: '15px',
},
},
categories: newCategories,
tickAmount: areSameDay ? 15 : 10,
},
yaxis: {
labels: {
minWidth: 40,
},
title: {
text: t('statistics.data'),
style: {
fontSize: '15px',
},
},
},
categories: newCategories,
tickAmount: areSameDay ? 15 : 10,
},
yaxis: {
labels: {
minWidth: 40,
legend: {
position: 'top',
horizontalAlign: 'right',
float: true,
},
title: {
text: t('statistics.data'),
style: {
fontSize: '15px',
};
const memoryOptions = {
chart: {
id: 'chart',
},
stroke: {
curve: 'smooth',
},
xaxis: {
tickAmount: areSameDay ? 15 : 10,
title: {
text: 'Time',
style: {
fontSize: '15px',
},
},
categories,
},
yaxis: {
tickAmount: 5,
title: {
text: t('statistics.data_mb'),
style: {
fontSize: '15px',
},
},
},
},
legend: {
position: 'top',
horizontalAlign: 'right',
float: true,
},
};
const memoryOptions = {
chart: {
id: 'chart',
},
stroke: {
curve: 'smooth',
},
xaxis: {
tickAmount: areSameDay ? 15 : 10,
title: {
text: 'Time',
style: {
fontSize: '15px',
},
legend: {
position: 'top',
horizontalAlign: 'right',
float: true,
},
categories,
},
yaxis: {
tickAmount: 5,
title: {
text: t('statistics.data_mb'),
style: {
fontSize: '15px',
},
},
},
legend: {
position: 'top',
horizontalAlign: 'right',
float: true,
},
};
};
const newOptions = {
interfaceList,
memory: [memoryUsed],
interfaceOptions,
memoryOptions,
start: new Date(sortedData[0].recorded * 1000).toISOString(),
end: new Date(sortedData[sortedData.length - 1].recorded * 1000).toISOString(),
};
const newOptions = {
interfaceList,
memory: [memoryUsed],
interfaceOptions,
memoryOptions,
start: new Date(sortedData[0].recorded * 1000).toISOString(),
end: new Date(sortedData[sortedData.length - 1].recorded * 1000).toISOString(),
};
if (statOptions !== newOptions) {
const sectionOptions = newOptions.interfaceList.map((opt) => ({
value: opt[0].titleName,
label: opt[0].titleName,
}));
setOptions([...sectionOptions, { value: 'memory', label: t('statistics.memory') }]);
setStatOptions({ ...newOptions });
if (sortedData.length > 0) {
setStart(new Date(sortedData[0].recorded * 1000));
setEnd(new Date(sortedData[sortedData.length - 1].recorded * 1000));
if (statOptions !== newOptions) {
const sectionOptions = newOptions.interfaceList.map((opt) => ({
value: opt[0].titleName,
label: opt[0].titleName,
}));
setOptions([...sectionOptions, { value: 'memory', label: t('statistics.memory') }]);
setStatOptions({ ...newOptions });
}
setError(undefined);
} catch (e) {
if (data?.length === 0) {
setError('nodata');
} else {
setError('error');
}
}
};
const getInterface = useCallback(() => {
if (error === 'error') {
return (
<div style={{ textAlign: 'center' }}>
<CAlert color="danger" style={{ width: '240px', margin: 'auto' }}>
Error while parsing statistics
</CAlert>
</div>
);
}
if (error === 'nodata') {
return (
<div style={{ textAlign: 'center' }}>
<CAlert color="danger" style={{ width: '340px', margin: 'auto' }}>
No available statistics during this time period
</CAlert>
</div>
);
}
if (statOptions.interfaceList.length === 0) return <p>N/A</p>;
const interfaceToShow = statOptions.interfaceList.find(
@@ -278,8 +301,9 @@ const StatisticsChartList = ({ setOptions, section, setStart, setEnd, time }) =>
</div>
);
}
return <p>N/A</p>;
}, [statOptions, section]);
}, [statOptions, section, error]);
const getStatistics = () => {
setLoading(true);
@@ -358,11 +382,10 @@ const StatisticsChartList = ({ setOptions, section, setStart, setEnd, time }) =>
};
StatisticsChartList.propTypes = {
deviceSerialNumber: PropTypes.string.isRequired,
setOptions: PropTypes.func.isRequired,
section: PropTypes.string.isRequired,
time: PropTypes.instanceOf(Object).isRequired,
setStart: PropTypes.func.isRequired,
setEnd: PropTypes.func.isRequired,
};
export default React.memo(StatisticsChartList);

View File

@@ -12,55 +12,98 @@ import {
} from '@coreui/react';
import DatePicker from 'react-widgets/DatePicker';
import { cilSync } from '@coreui/icons';
import { useDevice } from 'ucentral-libs';
import CIcon from '@coreui/icons-react';
import { useGlobalWebSocket } from 'contexts/WebSocketProvider';
import StatisticsChartList from './StatisticsChartList';
import LatestStatisticsmodal from './LatestStatisticsModal';
const getStart = () => {
const date = new Date();
date.setHours(date.getHours() - 1);
return date;
};
const DeviceStatisticsCard = () => {
const { t } = useTranslation();
const [showLatestModal, setShowLatestModal] = useState(false);
const [options, setOptions] = useState([]);
const [section, setSection] = useState('');
const [start, setStart] = useState(null);
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState(null);
const [endError, setEndError] = useState(false);
const [time, setTime] = useState({ refreshId: '0', start: null, end: null });
const { deviceSerialNumber } = useDevice();
const [nextUpdate, setNextUpdate] = useState(undefined);
const { addDeviceListener, removeDeviceListener } = useGlobalWebSocket();
const [time, setTime] = useState({
refreshId: '0',
start: getStart(),
end: new Date().toISOString(),
});
const toggleLatestModal = () => {
setShowLatestModal(!showLatestModal);
};
const modifyStart = (value) => {
const modifyStart = (value, refresh = true) => {
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
if (refresh) setTime({ ...time, refreshId: createUuid(), start: value, isChosen: true });
else setTime({ ...time, start: value, isChosen: true });
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
const modifyEnd = (value, refresh = true) => {
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
if (refresh) setTime({ ...time, refreshId: createUuid(), end: value, isChosen: true });
else setTime({ ...time, end: value, isChosen: true });
} catch (e) {
setEnd('');
setEndError(true);
}
};
const refresh = () => {
setTime({ refreshId: createUuid(), start, end });
setTime({ refreshId: createUuid(), start: getStart(), end: new Date().toISOString() });
};
const handleRefreshClick = () => {
refresh();
};
useEffect(() => {
if (section === '' && options.length > 0) setSection(options[0].value);
}, [options]);
useEffect(() => {
if (nextUpdate && !time.isChosen) {
setTime({ refreshId: createUuid(), start: getStart(), end: new Date().toISOString() });
setNextUpdate(undefined);
}
}, [nextUpdate, time]);
useEffect(() => {
setNextUpdate(undefined);
if (deviceSerialNumber) {
addDeviceListener({
serialNumber: deviceSerialNumber,
types: ['device_statistics'],
onTrigger: () => setNextUpdate(1),
});
refresh();
}
return () => {
if (deviceSerialNumber) {
removeDeviceListener({
serialNumber: deviceSerialNumber,
});
}
};
}, [deviceSerialNumber]);
return (
<div>
<CCard className="m-0">
@@ -68,7 +111,12 @@ const DeviceStatisticsCard = () => {
<div className="d-flex flex-row-reverse align-items-center">
<div className="pl-2">
<CPopover content={t('common.refresh')}>
<CButton size="sm" color="info" onClick={refresh} disabled={startError || endError}>
<CButton
size="sm"
color="info"
onClick={handleRefreshClick}
disabled={startError || endError}
>
<CIcon content={cilSync} />
</CButton>
</CPopover>
@@ -77,7 +125,7 @@ const DeviceStatisticsCard = () => {
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
value={time.end ? new Date(time.end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
@@ -88,7 +136,7 @@ const DeviceStatisticsCard = () => {
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
value={time.start ? new Date(time.start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
@@ -118,11 +166,10 @@ const DeviceStatisticsCard = () => {
</CCardHeader>
<CCardBody className="p-1">
<StatisticsChartList
deviceSerialNumber={deviceSerialNumber}
setOptions={setOptions}
section={section}
time={time}
setStart={setStart}
setEnd={setEnd}
/>
</CCardBody>
</CCard>

View File

@@ -25,6 +25,7 @@ const NetworkDiagram = ({ show, elements, setElements }) => {
onElementsRemove={onElementsRemove}
onLoad={onLoad}
snapToGrid
minZoom={0.1}
snapGrid={[20, 20]}
>
<MiniMap

View File

@@ -47,7 +47,7 @@ const associationNode = (associationInfo) => (
<div>
<CRow>
<CCol className="text-center">
<h6>{associationInfo.bssid}</h6>
<h6>{associationInfo.station}</h6>
</CCol>
</CRow>
<CRow>
@@ -92,7 +92,6 @@ const NetworkDiagram = ({ show, radios, associations }) => {
// Creating the association nodes and their edges
for (let i = 0; i < associations.length; i += 1) {
const assoc = associations[i];
// If the radio has not been added, we create a new unknown radio based on its index
if (radiosAdded[assoc.radio.radioIndex] === undefined) {
newElements.push({
@@ -107,7 +106,7 @@ const NetworkDiagram = ({ show, radios, associations }) => {
// Adding the association
newElements.push({
id: `a-${assoc.bssid}`,
id: `a-${assoc.station}`,
data: { label: associationNode(assoc) },
position: {
x: getX(radiosAdded[assoc.radio.radioIndex]),
@@ -120,9 +119,9 @@ const NetworkDiagram = ({ show, radios, associations }) => {
// Creating the edge
newElements.push({
id: `e-${assoc.radio.radioIndex}-${assoc.bssid}`,
id: `e-${assoc.radio.radioIndex}-${assoc.station}`,
source: `r-${assoc.radio.radioIndex}`,
target: `a-${assoc.bssid}`,
target: `a-${assoc.station}`,
arrowHeadType: 'arrowclosed',
});
}

View File

@@ -89,7 +89,18 @@ const ActionModal = ({ show, toggleModal }) => {
});
toggleModal();
})
.catch(() => {
.catch((e) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setResult('error');
})
.finally(() => {

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import Select from 'react-select';
@@ -12,6 +12,9 @@ import {
CRow,
CCol,
CInput,
CFormGroup,
CInputRadio,
CLabel,
CSpinner,
CAlert,
} from '@coreui/react';
@@ -36,11 +39,14 @@ const TelemetryModal = ({ show, toggle }) => {
const [lastMessage, setLastMessage] = useState({});
const [receivedMessages, setReceivedMessages] = useState(0);
const [types, setTypes] = useState([]);
const [chosenMethod, setChosenMethod] = useState('false');
const [lifetime, setLifetime] = useState(5);
const [interval, setInterval] = useState(3);
const [loading, setLoading] = useState(false);
const [lastUpdate, setLastUpdate] = useState('');
const onIntervalChange = (e) => setInterval(e.target.value);
const onLifetimeChange = (e) => setLifetime(e.target.value);
const closeSocket = () => {
if (socket !== null) {
@@ -49,6 +55,17 @@ const TelemetryModal = ({ show, toggle }) => {
}
};
const msgToDisplay = useMemo(() => {
const display = {};
if (lastMessage) {
for (const type of types) {
display[type.value] = lastMessage[type.value];
}
}
return display;
}, [lastMessage, types]);
const getUrl = () => {
setLastUpdate('');
setLastMessage({});
@@ -57,6 +74,8 @@ const TelemetryModal = ({ show, toggle }) => {
const parameters = {
serialNumber: deviceSerialNumber,
interval: parseInt(interval, 10),
lifetime: parseInt(lifetime * 60, 10),
kafka: chosenMethod,
types: types.map((type) => type.value),
};
@@ -72,18 +91,31 @@ const TelemetryModal = ({ show, toggle }) => {
{ headers },
)
.then((response) => {
if (response.data.uri && response.data.uri !== '') {
if (chosenMethod === 'true') {
addToast({
title: t('common.success'),
body: t('commands.command_success'),
color: 'success',
autohide: true,
});
toggle();
} else if (response.data.uri && response.data.uri !== '') {
setReceivedMessages(0);
setSocket(new WebSocket(response.data.uri));
}
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('telemetry.connection_failed', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
})
.finally(() => setLoading(false));
};
@@ -146,6 +178,50 @@ const TelemetryModal = ({ show, toggle }) => {
/>
</CCol>
</CRow>
<CRow>
<CCol>{`${t('telemetry.lifetime')}: ${lifetime} ${t('common.minutes')}`}</CCol>
</CRow>
<CRow>
<CCol>
<CInput
type="range"
min="1"
max="120"
step="1"
onChange={onLifetimeChange}
value={lifetime}
/>
</CCol>
</CRow>
<CFormGroup row className="mb-0">
<CCol md="3">
<CLabel>{t('telemetry.outputmode')}</CLabel>
</CCol>
<CCol>
<CFormGroup variant="checkbox" onClick={() => setChosenMethod('false')} inline>
<CInputRadio
defaultChecked={chosenMethod === 'false'}
id="traceRadio1"
name="radios"
value="traceOption1"
/>
<CLabel variant="checkbox" htmlFor="traceRadio1">
Websocket
</CLabel>
</CFormGroup>
<CFormGroup variant="checkbox" onClick={() => setChosenMethod('true')} inline>
<CInputRadio
defaultChecked={chosenMethod === 'true'}
id="traceRadio2"
name="radios"
value="traceOption2"
/>
<CLabel variant="checkbox" htmlFor="traceRadio2">
Kafka
</CLabel>
</CFormGroup>
</CCol>
</CFormGroup>
<CRow>
<CCol sm="2" className="pt-2">
{t('telemetry.types')}:
@@ -178,6 +254,11 @@ const TelemetryModal = ({ show, toggle }) => {
{t('telemetry.interval')}: {interval} {t('common.seconds')}
</CCol>
</CRow>
<CRow>
<CCol>
{t('telemetry.lifetime')}: {lifetime} {t('common.minutes')}
</CCol>
</CRow>
<CRow>
<CCol>
{t('telemetry.types')}: {types.map((type) => type.label).join(', ')}
@@ -193,7 +274,7 @@ const TelemetryModal = ({ show, toggle }) => {
</CRow>
<CRow>
<CCol>
<pre>{JSON.stringify(lastMessage, null, 2)}</pre>
<pre>{JSON.stringify(msgToDisplay, null, 2)}</pre>
</CCol>
</CRow>
<CRow>

View File

@@ -23,13 +23,14 @@ import PropTypes from 'prop-types';
import 'react-widgets/styles.css';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import { LoadingButton, useAuth, useDevice, useToast } from 'ucentral-libs';
import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
import WaitingForTraceBody from './WaitingForTraceBody';
const TraceModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const { deviceSerialNumber, getDeviceConnection } = useDevice();
const [hadSuccess, setHadSuccess] = useState(false);
const [hadFailure, setHadFailure] = useState(false);
@@ -94,7 +95,18 @@ const TraceModal = ({ show, toggleModal }) => {
setWaitingForTrace(true);
}
})
.catch(() => {
.catch((e) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setResponseBody(t('commands.error'));
setHadFailure(true);
})

View File

@@ -49,7 +49,7 @@ const WifiAnalysis = () => {
const secondsToLabel = (seconds) =>
compactSecondsToDetailed(seconds, t('common.day'), t('common.days'), t('common.seconds'));
const extractIp = (json, bssid) => {
const extractIp = (json, station) => {
const ips = {
ipV4: [],
ipV6: [],
@@ -57,7 +57,7 @@ const WifiAnalysis = () => {
for (const obj of json.interfaces) {
if ('clients' in obj) {
for (const client of obj.clients) {
if (client.mac === bssid) {
if (client.mac === station) {
ips.ipV4 = ips.ipV4.concat(client.ipv4_addresses ?? []);
ips.ipV6 = ips.ipV6.concat(client.ipv6_addresses ?? []);
}
@@ -151,7 +151,7 @@ const WifiAnalysis = () => {
const data = {
radio: radioInfo,
...extractIp(stat.data, association.bssid),
...extractIp(stat.data, association.station),
station: association.station,
ssid: ssid.ssid,
rssi: association.rssi ? parseDbm(association.rssi) : '-',
@@ -238,7 +238,7 @@ const WifiAnalysis = () => {
return (
<div>
<CCard>
<CCard className="mb-0">
<CCardHeader className="dark-header d-flex flex-row-reverse align-items-center">
<div className="pl-2">
<CPopover content={t('common.refresh')}>
@@ -254,35 +254,47 @@ const WifiAnalysis = () => {
</div>
</CCardHeader>
<CCardBody>
<CRow className="mb-4">
<CCol className="text-center">
<input
type="range"
style={{ width: '80%' }}
className="form-range"
min="0"
max={range}
step="1"
onChange={(e) => updateSelectedStats(e.target.value)}
defaultValue={range}
disabled={!selectedRadioStats}
/>
<h5>
{t('common.timestamp')}: {tableTime}
</h5>
</CCol>
</CRow>
<div className="overflow-auto" style={{ height: 'calc(100vh - 300px)' }}>
<h5 className="pb-3 text-center">{t('wifi_analysis.radios')}</h5>
<RadioAnalysisTable data={selectedRadioStats ?? []} loading={loading} range={range} />
<h5 className="pt-5 pb-3 text-center">{t('wifi_analysis.associations')}</h5>
<WifiAnalysisTable
t={t}
data={selectedAssociationStats ?? []}
loading={loading}
range={range}
/>
</div>
{!loading && parsedAssociationStats.length === 0 ? (
<div className="text-center">
<h3>{t('wifi_analysis.waiting_for_data')}</h3>
</div>
) : (
<>
<CRow className="mb-4">
<CCol className="text-center">
<input
type="range"
style={{ width: '80%' }}
className="form-range"
min="0"
max={range}
step="1"
onChange={(e) => updateSelectedStats(e.target.value)}
defaultValue={range}
disabled={!selectedRadioStats}
/>
<h5>
{t('common.timestamp')}: {tableTime}
</h5>
</CCol>
</CRow>
<div className="overflow-auto" style={{ height: 'calc(100vh - 300px)' }}>
<h5 className="pb-3 text-center">{t('wifi_analysis.radios')}</h5>
<RadioAnalysisTable
data={selectedRadioStats ?? []}
loading={loading}
range={range}
/>
<h5 className="pt-5 pb-3 text-center">{t('wifi_analysis.associations')}</h5>
<WifiAnalysisTable
t={t}
data={selectedAssociationStats ?? []}
loading={loading}
range={range}
/>
</div>
</>
)}
</CCardBody>
</CCard>
<CModal size="xl" show={showModal} onClose={toggleModal}>

View File

@@ -0,0 +1,187 @@
{
"SSID": 0,
"SUPP_RATES": 1,
"FH_PARAMS": 2,
"DS_PARAMS": 3,
"CF_PARAMS": 4,
"TIM": 5,
"IBSS_PARAMS": 6,
"COUNTRY": 7,
"REQUEST": 10,
"QBSS_LOAD": 11,
"EDCA_PARAM_SET": 12,
"TSPEC": 13,
"TCLAS": 14,
"SCHEDULE": 15,
"CHALLENGE": 16,
"PWR_CONSTRAINT": 32,
"PWR_CAPABILITY": 33,
"TPC_REQUEST": 34,
"TPC_REPORT": 35,
"SUPPORTED_CHANNELS": 36,
"CHANNEL_SWITCH": 37,
"MEASURE_REQUEST": 38,
"MEASURE_REPORT": 39,
"QUIET": 40,
"IBSS_DFS": 41,
"ERP_INFO": 42,
"TS_DELAY": 43,
"TCLAS_PROCESSING": 44,
"HT_CAPABILITY": 45,
"QOS_CAPA": 46,
"RSN": 48,
"802_15_COEX": 49,
"EXT_SUPP_RATES": 50,
"AP_CHAN_REPORT": 51,
"NEIGHBOR_REPORT": 52,
"RCPI": 53,
"MOBILITY_DOMAIN": 54,
"FAST_BSS_TRANSITION": 55,
"TIMEOUT_INTERVAL": 56,
"RIC_DATA": 57,
"DSE_REGISTERED_LOCATION": 58,
"SUPPORTED_REGULATORY_CLASSES": 59,
"EXT_CHANSWITCH_ANN": 60,
"HT_OPERATION": 61,
"SECONDARY_CHANNEL_OFFSET": 62,
"BSS_AVG_ACCESS_DELAY": 63,
"ANTENNA_INFO": 64,
"RSNI": 65,
"MEASUREMENT_PILOT_TX_INFO": 66,
"BSS_AVAILABLE_CAPACITY": 67,
"BSS_AC_ACCESS_DELAY": 68,
"TIME_ADVERTISEMENT": 69,
"RRM_ENABLED_CAPABILITIES": 70,
"MULTIPLE_BSSID": 71,
"BSS_COEX_2040": 72,
"BSS_INTOLERANT_CHL_REPORT": 73,
"OVERLAP_BSS_SCAN_PARAM": 74,
"RIC_DESCRIPTOR": 75,
"MMIE": 76,
"ASSOC_COMEBACK_TIME": 77,
"EVENT_REQUEST": 78,
"EVENT_REPORT": 79,
"DIAGNOSTIC_REQUEST": 80,
"DIAGNOSTIC_REPORT": 81,
"LOCATION_PARAMS": 82,
"NON_TX_BSSID_CAP": 83,
"SSID_LIST": 84,
"MULTI_BSSID_IDX": 85,
"FMS_DESCRIPTOR": 86,
"FMS_REQUEST": 87,
"FMS_RESPONSE": 88,
"QOS_TRAFFIC_CAPA": 89,
"BSS_MAX_IDLE_PERIOD": 90,
"TSF_REQUEST": 91,
"TSF_RESPOSNE": 92,
"WNM_SLEEP_MODE": 93,
"TIM_BCAST_REQ": 94,
"TIM_BCAST_RESP": 95,
"COLL_IF_REPORT": 96,
"CHANNEL_USAGE": 97,
"TIME_ZONE": 98,
"DMS_REQUEST": 99,
"DMS_RESPONSE": 100,
"LINK_ID": 101,
"WAKEUP_SCHEDUL": 102,
"CHAN_SWITCH_TIMING": 104,
"PTI_CONTROL": 105,
"PU_BUFFER_STATUS": 106,
"INTERWORKING": 107,
"ADVERTISEMENT_PROTOCOL": 108,
"EXPEDITED_BW_REQ": 109,
"QOS_MAP_SET": 110,
"ROAMING_CONSORTIUM": 111,
"EMERGENCY_ALERT": 112,
"MESH_CONFIG": 113,
"MESH_ID": 114,
"LINK_METRIC_REPORT": 115,
"CONGESTION_NOTIFICATION": 116,
"PEER_MGMT": 117,
"CHAN_SWITCH_PARAM": 118,
"MESH_AWAKE_WINDOW": 119,
"BEACON_TIMING": 120,
"MCCAOP_SETUP_REQ": 121,
"MCCAOP_SETUP_RESP": 122,
"MCCAOP_ADVERT": 123,
"MCCAOP_TEARDOWN": 124,
"GANN": 125,
"RANN": 126,
"EXT_CAPABILITY": 127,
"PREQ": 130,
"PREP": 131,
"PERR": 132,
"PXU": 137,
"PXUC": 138,
"AUTH_MESH_PEER_EXCH": 139,
"MIC": 140,
"DESTINATION_URI": 141,
"UAPSD_COEX": 142,
"WAKEUP_SCHEDULE": 143,
"EXT_SCHEDULE": 144,
"STA_AVAILABILITY": 145,
"DMG_TSPEC": 146,
"DMG_AT": 147,
"DMG_CAP": 148,
"CISCO_VENDOR_SPECIFIC": 150,
"DMG_OPERATION": 151,
"DMG_BSS_PARAM_CHANGE": 152,
"DMG_BEAM_REFINEMENT": 153,
"CHANNEL_MEASURE_FEEDBACK": 154,
"AWAKE_WINDOW": 157,
"MULTI_BAND": 158,
"ADDBA_EXT": 159,
"NEXT_PCP_LIST": 160,
"PCP_HANDOVER": 161,
"DMG_LINK_MARGIN": 162,
"SWITCHING_STREAM": 163,
"SESSION_TRANSITION": 164,
"DYN_TONE_PAIRING_REPORT": 165,
"CLUSTER_REPORT": 166,
"RELAY_CAP": 167,
"RELAY_XFER_PARAM_SET": 168,
"BEAM_LINK_MAINT": 169,
"MULTIPLE_MAC_ADDR": 170,
"U_PID": 171,
"DMG_LINK_ADAPT_ACK": 172,
"MCCAOP_ADV_OVERVIEW": 174,
"QUIET_PERIOD_REQ": 175,
"QUIET_PERIOD_RESP": 177,
"EPAC_POLICY": 182,
"CLISTER_TIME_OFF": 183,
"INTER_AC_PRIO": 184,
"SCS_DESCRIPTOR": 185,
"QLOAD_REPORT": 186,
"HCCA_TXOP_UPDATE_COUNT": 187,
"HL_STREAM_ID": 188,
"GCR_GROUP_ADDR": 189,
"ANTENNA_SECTOR_ID_PATTERN": 190,
"VHT_CAPABILITY": 191,
"VHT_OPERATION": 192,
"EXTENDED_BSS_LOAD": 193,
"WIDE_BW_CHANNEL_SWITCH": 194,
"TX_POWER_ENVELOPE": 195,
"CHANNEL_SWITCH_WRAPPER": 196,
"AID": 197,
"QUIET_CHANNEL": 198,
"OPMODE_NOTIF": 199,
"REDUCED_NEIGHBOR_REPORT": 201,
"AID_REQUEST": 210,
"AID_RESPONSE": 211,
"S1G_BCN_COMPAT": 213,
"S1G_SHORT_BCN_INTERVAL": 214,
"S1G_TWT": 216,
"S1G_CAPABILITIES": 217,
"VENDOR_SPECIFIC": 221,
"QOS_PARAMETER": 222,
"S1G_OPERATION": 232,
"CAG_NUMBER": 237,
"AP_CSN": 239,
"FILS_INDICATION": 240,
"DILS": 241,
"FRAGMENT": 242,
"RSNX": 244,
"EXTENSION": 255
}

View File

@@ -6,7 +6,6 @@ import {
CModalTitle,
CModalBody,
CRow,
CForm,
CSwitch,
CCol,
CSpinner,
@@ -21,16 +20,22 @@ import PropTypes from 'prop-types';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import { prettyDateForFile } from 'utils/helper';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth, useDevice, useToast } from 'ucentral-libs';
import WifiChannelTable from 'components/WifiScanResultModal/WifiChannelTable';
import 'react-widgets/styles.css';
import { CSVLink } from 'react-csv';
import IeDisplay from 'components/WifiScanResultModal/IeDisplay';
import IE_OPTIONS from './IE_OPTIONS.json';
const allIes = Object.entries(IE_OPTIONS).map(([, value]) => value);
const WifiScanModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [hadSuccess, setHadSuccess] = useState(false);
const [selectedIes, setSelectedIes] = useState(undefined);
const [hadFailure, setHadFailure] = useState(false);
const [errorCode, setErrorCode] = useState(0);
const [waiting, setWaiting] = useState(false);
@@ -60,6 +65,7 @@ const WifiScanModal = ({ show, toggleModal }) => {
setActiveScan(false);
setHideOptions(false);
setErrorCode(0);
setSelectedIes(undefined);
}, [show]);
const parseThroughList = (scanList) => {
@@ -87,6 +93,7 @@ const WifiScanModal = ({ show, toggleModal }) => {
deviceToAdd.SSID = device.meshid && device.meshid.length > 0 ? device.meshid : 'N/A';
}
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
deviceToAdd.ies = device.ies;
channel.devices.push(deviceToAdd);
}
});
@@ -123,7 +130,7 @@ const WifiScanModal = ({ show, toggleModal }) => {
}
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
channel.devices.push(deviceToAdd);
listCsv.push({ ...deviceToAdd, ...device });
listCsv.push({ ...device, ...deviceToAdd, ies: JSON.stringify(device.ies, null, 4) });
}
});
});
@@ -140,6 +147,7 @@ const WifiScanModal = ({ show, toggleModal }) => {
override_dfs: dfs,
bandwidth: bandwidth !== '' ? bandwidth : undefined,
activeScan,
ies: allIes,
};
const headers = {
Accept: 'application/json',
@@ -165,7 +173,18 @@ const WifiScanModal = ({ show, toggleModal }) => {
setHadFailure(true);
}
})
.catch(() => {
.catch((e) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setHadFailure(true);
})
.finally(() => {
@@ -223,15 +242,13 @@ const WifiScanModal = ({ show, toggleModal }) => {
<p className="pl-2">{t('wifi_analysis.override_dfs')}:</p>
</CCol>
<CCol>
<CForm className="pl-4">
<CSwitch
color="primary"
defaultChecked={dfs}
onClick={toggleDfs}
labelOn={t('common.on')}
labelOff={t('common.off')}
/>
</CForm>
<CSwitch
color="primary"
defaultChecked={dfs}
onClick={toggleDfs}
labelOn={t('common.on')}
labelOff={t('common.off')}
/>
</CCol>
</CRow>
<CRow className="mt-3">
@@ -239,15 +256,13 @@ const WifiScanModal = ({ show, toggleModal }) => {
<p className="pl-2">{t('scan.active')}:</p>
</CCol>
<CCol>
<CForm className="pl-4">
<CSwitch
color="primary"
defaultChecked={activeScan}
onClick={toggleActiveScan}
labelOn={t('common.on')}
labelOff={t('common.off')}
/>
</CForm>
<CSwitch
color="primary"
defaultChecked={activeScan}
onClick={toggleActiveScan}
labelOn={t('common.on')}
labelOff={t('common.off')}
/>
</CCol>
</CRow>
<CRow className="mt-3">
@@ -255,19 +270,17 @@ const WifiScanModal = ({ show, toggleModal }) => {
<p className="pl-2">Bandwidth:</p>
</CCol>
<CCol>
<CForm className="pl-4">
<CSelect
custom
value={bandwidth}
onChange={(e) => setBandwidth(e.target.value)}
style={{ width: '100px' }}
>
<option value="">Default</option>
<option value="20">20 MHz</option>
<option value="40">40 MHz</option>
<option value="80">80 MHz</option>
</CSelect>
</CForm>
<CSelect
custom
value={bandwidth}
onChange={(e) => setBandwidth(e.target.value)}
style={{ width: '100px' }}
>
<option value="">Default</option>
<option value="20">20 MHz</option>
<option value="40">40 MHz</option>
<option value="80">80 MHz</option>
</CSelect>
</CCol>
</CRow>
</div>
@@ -296,7 +309,10 @@ const WifiScanModal = ({ show, toggleModal }) => {
</CCol>
</CRow>
)}
<WifiChannelTable channels={channelList} />
{selectedIes || channelList === null ? null : (
<WifiChannelTable channels={channelList} setIes={setSelectedIes} />
)}
{selectedIes && <IeDisplay ies={selectedIes} setIes={setSelectedIes} />}
</div>
</CModalBody>
</CModal>

View File

@@ -0,0 +1,90 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { CButton, CRow, CCol } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilArrowLeft } from '@coreui/icons';
const IeDisplay = ({ ies, setIes }) => {
const handleClick = () => {
setIes(undefined);
};
const display = useMemo(
() =>
ies.map((ie) => {
if (ie.byteArr) {
return (
<CCol sm="6">
<h5
style={{
textDecoration: 'underline',
}}
>
{ie.name}:{' '}
</h5>
<pre>
{ie.byteArr.map((arr, i) => {
const offset = (i * 8).toString(16);
return (
<pre
className="mb-0"
style={{
overflowY: 'auto',
maxHeight: '200px',
}}
>
{offset.length === 1 ? `0${offset}` : offset}: {arr.join(' ')}
</pre>
);
})}
</pre>
</CCol>
);
}
return (
<CCol sm="6">
<h5
style={{
textDecoration: 'underline',
}}
>
{ie.name}:{' '}
</h5>
<pre
style={{
overflowY: 'auto',
maxHeight: '200px',
}}
>
{JSON.stringify(ie.data, null, 4)}
</pre>
</CCol>
);
}),
[ies],
);
return (
<>
<CRow>
<CCol>
<h4>Information Elements</h4>
</CCol>
<CCol className="text-right">
<CButton color="primary" variant="outline" className="ml-2" onClick={handleClick}>
Go Back
<CIcon className="ml-2" content={cilArrowLeft} />
</CButton>
</CCol>
</CRow>
<CRow>{display}</CRow>
</>
);
};
IeDisplay.propTypes = {
ies: PropTypes.instanceOf(Array).isRequired,
setIes: PropTypes.func.isRequired,
};
export default IeDisplay;

View File

@@ -1,8 +1,18 @@
import { CCard, CCardTitle, CCardBody, CDataTable, CCardHeader } from '@coreui/react';
import {
CCard,
CCardTitle,
CCardBody,
CDataTable,
CButton,
CPopover,
CCardHeader,
} from '@coreui/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import 'react-widgets/styles.css';
import { Buffer } from 'buffer';
import IE_OPTIONS from '../WifiScanModal/IE_OPTIONS.json';
const parseDbm = (value) => {
if (!value) return '-';
@@ -10,9 +20,66 @@ const parseDbm = (value) => {
return 4294967295 + value;
};
const WifiChannelCard = ({ channel }) => {
const getIeName = (type) => {
for (const [key, value] of Object.entries(IE_OPTIONS)) {
if (value === type) return `${key} (${type})`;
}
return type;
};
const parseIe = ({ name, type, data, content }) => {
try {
if (data) {
const ie = Buffer.from(data, 'base64');
const arr = new Uint16Array(ie);
const finalArr = [];
for (let i = 0; i < arr.length; i += 8) {
const slice = arr.slice(i, i + 8);
finalArr.push(
Object.keys(slice).map((k) => {
const num = slice[k].toString(16);
return num.length === 1 ? `0${num}` : num;
}),
);
}
const finalName = name ? `${name} (${type})` : getIeName(type);
return { name: finalName, byteArr: finalArr };
}
if (content) {
return { name: name ? `${name} (${type})` : getIeName(type), data: content };
}
return { name: name ? `${name} (${type})` : getIeName(type), data: content ?? data };
} catch {
return { name: name ? `${name} (${type}, error while parsing)` : getIeName(type), data };
}
};
const WifiChannelCard = ({ channel, setIes }) => {
const { t } = useTranslation();
const columns = [{ key: 'SSID', _style: { width: '70%' } }, { key: 'Signal' }];
const columns = [{ key: 'SSID', _style: { width: '70%' } }, { key: 'Signal' }, { key: 'IE' }];
const displayIe = (ies) => {
const parsedIes = ies.map((ie) => parseIe(ie));
const str = ies.map(({ type, data }) => `${type}: ${data}\n`);
return (
<td className="ignore-overflow text-center align-middle">
{str.length > 0 ? (
<CPopover content="View IEs">
<CButton color="primary" size="sm" onClick={() => setIes(parsedIes)}>
{ies.length}
</CButton>
</CPopover>
) : (
ies.length
)}
</td>
);
};
return (
<CCard>
@@ -29,7 +96,8 @@ const WifiChannelCard = ({ channel }) => {
fields={columns}
className="text-white"
scopedSlots={{
Signal: (item) => <td>{parseDbm(item.Signal)}</td>,
Signal: (item) => <td className="align-middle">{parseDbm(item.Signal)}</td>,
IE: (item) => displayIe(item.ies),
}}
/>
</div>
@@ -40,6 +108,7 @@ const WifiChannelCard = ({ channel }) => {
WifiChannelCard.propTypes = {
channel: PropTypes.instanceOf(Object).isRequired,
setIes: PropTypes.func.isRequired,
};
export default WifiChannelCard;

View File

@@ -4,7 +4,7 @@ import { v4 as createUuid } from 'uuid';
import PropTypes from 'prop-types';
import WifiChannelCard from './WifiChannelCard';
const WifiChannelTable = ({ channels }) => {
const WifiChannelTable = ({ channels, setIes }) => {
const sortChannels = () => {
channels.sort((a, b) => (a.channel > b.channel ? 1 : -1));
};
@@ -17,13 +17,15 @@ const WifiChannelTable = ({ channels }) => {
<CRow>
<CCol>
{channels.map((channel, index) => {
if (index % 2 === 0) return <WifiChannelCard key={createUuid()} channel={channel} />;
if (index % 2 === 0)
return <WifiChannelCard key={createUuid()} channel={channel} setIes={setIes} />;
return <div key={createUuid()} />;
})}
</CCol>
<CCol>
{channels.map((channel, index) => {
if (index % 2 === 1) return <WifiChannelCard key={createUuid()} channel={channel} />;
if (index % 2 === 1)
return <WifiChannelCard key={createUuid()} channel={channel} setIes={setIes} />;
return <div key={createUuid()} />;
})}
</CCol>
@@ -33,6 +35,7 @@ const WifiChannelTable = ({ channels }) => {
WifiChannelTable.propTypes = {
channels: PropTypes.instanceOf(Array),
setIes: PropTypes.func.isRequired,
};
WifiChannelTable.defaultProps = {

View File

@@ -1,5 +1,5 @@
/* eslint-disable-rule prefer-destructuring */
import React, { useCallback } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
@@ -9,12 +9,14 @@ import { prettyDate, prettyDateForFile } from 'utils/helper';
import { useDevice } from 'ucentral-libs';
import { CSVLink } from 'react-csv';
import WifiChannelTable from './WifiChannelTable';
import IeDisplay from './IeDisplay';
const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
const { t } = useTranslation();
const { deviceSerialNumber } = useDevice();
const [selectedIes, setSelectedIes] = useState(undefined);
const getData = useCallback(() => {
const getData = useMemo(() => {
if (scanResults === null || scanResults.length === 0) return [];
const dbmNumber = 4294967295;
const listOfChannels = [];
@@ -31,7 +33,6 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
channel: channelNumber,
devices: [],
};
scanResults.forEach((device) => {
if (device.channel === channelNumber) {
const deviceToAdd = {};
@@ -41,14 +42,15 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
}
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
channel.devices.push(deviceToAdd);
listCsv.push({ ...deviceToAdd, ...device });
listCsv.push({ ...deviceToAdd, ...device, ies: JSON.stringify(device.ies, null, 4) });
}
});
});
return listCsv;
}, [scanResults]);
const parseThroughList = useCallback(() => {
const parseThroughList = useMemo(() => {
if (!scanResults) return null;
const dbmNumber = 4294967295;
const listOfChannels = [];
@@ -73,6 +75,7 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
deviceToAdd.SSID = device.meshid && device.meshid.length > 0 ? device.meshid : 'N/A';
}
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
deviceToAdd.ies = device.ies;
channel.devices.push(deviceToAdd);
}
});
@@ -82,6 +85,10 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
return finalList;
}, [scanResults]);
useEffect(() => {
setSelectedIes(undefined);
}, [show]);
return (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader>
@@ -94,7 +101,7 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
filename={`wifi_scan_${deviceSerialNumber}_${
date !== '' ? prettyDateForFile(date) : ''
}.csv`}
data={getData()}
data={getData}
>
<CButton color="primary" variant="outline" className="ml-2">
<CIcon content={cilCloudDownload} />
@@ -109,7 +116,10 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
</div>
</CModalHeader>
<CModalBody>
{scanResults === null ? null : <WifiChannelTable channels={parseThroughList()} />}
{selectedIes && <IeDisplay ies={selectedIes} setIes={setSelectedIes} />}
{selectedIes || scanResults === null ? null : (
<WifiChannelTable channels={parseThroughList} setIes={setSelectedIes} />
)}
</CModalBody>
</CModal>
);

4
src/constants.js Normal file
View File

@@ -0,0 +1,4 @@
export const AUTH_EXPIRED_TOKEN_CODE = 9;
export const AUTH_INVALID_TOKEN_CODE = 8;
export const LOGOUT_ON_SEC_ERROR_CODES = [AUTH_EXPIRED_TOKEN_CODE, AUTH_INVALID_TOKEN_CODE];

View File

@@ -11,7 +11,7 @@ const WebSocketContext = React.createContext({
addDeviceListener: () => {},
});
export const WebSocketProvider = ({ children }) => {
export const WebSocketProvider = ({ children, setNewConnectionData }) => {
const { currentToken, endpoints } = useAuth();
const [isOpen, setIsOpen] = useState(false);
const ws = useRef(undefined);
@@ -20,6 +20,9 @@ export const WebSocketProvider = ({ children }) => {
const onMessage = useCallback((message) => {
const result = extractWebSocketResponse(message);
if (result?.type === 'device_connections_statistics') {
setNewConnectionData(result.content);
}
if (result?.type === 'NOTIFICATION') {
dispatch({ type: 'NEW_NOTIFICATION', notification: result.notification });
pushNotification(result.notification);
@@ -36,23 +39,29 @@ export const WebSocketProvider = ({ children }) => {
}
}, []);
// useEffect for created the WebSocket and 'storing' it in useRef
useEffect(() => {
ws.current = new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`);
const onStartWebSocket = () => {
ws.current = new WebSocket(`${endpoints.owgw?.replace('https', 'wss')}/api/v1/ws`);
ws.current.onopen = () => {
setIsOpen(true);
ws.current?.send(`token:${currentToken}`);
};
ws.current.onclose = () => {
setIsOpen(false);
setTimeout(onStartWebSocket, 3000);
};
ws.current.onerror = () => {
setIsOpen(false);
};
};
// useEffect for created the WebSocket and 'storing' it in useRef
useEffect(() => {
if (endpoints?.owgw !== undefined) {
onStartWebSocket();
}
const wsCurrent = ws?.current;
return () => wsCurrent?.close();
}, []);
}, [endpoints]);
// useEffect for generating global notifications
useEffect(() => {
@@ -65,6 +74,7 @@ export const WebSocketProvider = ({ children }) => {
if (wsCurrent) wsCurrent.removeEventListener('message', onMessage);
};
}, [ws?.current]);
const values = useMemo(
() => ({
lastMessage,
@@ -83,6 +93,7 @@ export const WebSocketProvider = ({ children }) => {
WebSocketProvider.propTypes = {
children: PropTypes.node.isRequired,
setNewConnectionData: PropTypes.func.isRequired,
};
export const useGlobalWebSocket = () => React.useContext(WebSocketContext);

View File

@@ -26,30 +26,11 @@ export const extractWebSocketResponse = (message) => {
if (data.command_response_id) {
return { data, type: 'COMMAND' };
}
if (data.notification.type === 'device_connections_statistics') {
return { content: data.notification.content, type: 'device_connections_statistics' };
}
} catch {
return undefined;
}
return undefined;
};
export const getStatusFromNotification = (notification) => {
let status = 'success';
if (notification.content.warning?.length > 0) status = 'warning';
if (notification.content.error?.length > 0) status = 'error';
return status;
};
export const getNotificationDescription = (t, notification) => {
if (
notification.content.type === 'venue_configuration_update' ||
notification.content.type === 'entity_configuration_update'
) {
return t('configurations.notification_details', {
success: notification.content.success.length,
warning: notification.content.warning.length,
error: notification.content.error.length,
});
}
return notification.content.details;
};

View File

@@ -0,0 +1,17 @@
import { useState } from 'react';
const useToggle = (initialState) => {
const [value, setValue] = useState(initialState);
return [
value,
() => {
setValue(!value);
},
(newValue) => {
setValue(newValue);
},
];
};
export default useToggle;

120
src/layout/Devices.js Normal file
View File

@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import axiosInstance from 'utils/axiosInstance';
import { useAuth } from 'ucentral-libs';
import { CPopover } from '@coreui/react';
import { extraCompactSecondsToDetailed, secondsToDetailed } from 'utils/helper';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
const propTypes = {
newData: PropTypes.instanceOf(Object),
};
const defaultProps = {
newData: undefined,
};
const SidebarDevices = ({ newData }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [stats, setStats] = useState();
const [lastUpdate, setLastUpdate] = useState();
const [lastTime, setLastTime] = useState();
const getInitialStats = async () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/devices?connectionStatistics=true`, options)
.then(({ data }) => {
setStats(data);
setLastUpdate(new Date());
})
.catch(() => {});
};
const getTime = () => {
if (lastTime === undefined || lastUpdate === undefined) return null;
const seconds = lastTime.getTime() - lastUpdate.getTime();
return Math.max(0, Math.floor(seconds / 1000));
};
useEffect(() => {
if (newData !== undefined && Object.keys(newData).length > 0) {
setStats({ ...newData });
setLastUpdate(new Date());
}
}, [newData]);
useEffect(() => {
getInitialStats();
}, []);
useEffect(() => {
const interval = setInterval(() => {
setLastTime(new Date());
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
if (!stats) {
return null;
}
return (
<div
style={{
position: 'absolute',
bottom: '0px',
width: '100%',
background: '#2f3d54 !important',
backgroundColor: '#2f3d54 !important',
borderTop: '3px solid #d8dbe0',
color: 'white',
textAlign: 'center',
paddingTop: '15px',
paddingBottom: '25px',
}}
>
<h3 style={{ marginBottom: '0px' }}>{stats?.connectedDevices ?? stats?.numberOfDevices}</h3>
<h6>Connected Devices</h6>
<CPopover
content={secondsToDetailed(
stats?.averageConnectionTime ?? stats?.averageConnectedTime,
t('common.day'),
t('common.days'),
t('common.hour'),
t('common.hours'),
t('common.minute'),
t('common.minutes'),
t('common.second'),
t('common.seconds'),
)}
>
<h3 style={{ marginBottom: '0px' }}>
{extraCompactSecondsToDetailed(
stats?.averageConnectionTime ?? stats?.averageConnectedTime,
t('common.day'),
t('common.days'),
t('common.seconds'),
)}
</h3>
</CPopover>
<h6>Avg. Connection Time</h6>
<h7 style={{ color: '#ebedef', fontStyle: 'italic' }}>{getTime()} seconds ago</h7>
</div>
);
};
SidebarDevices.propTypes = propTypes;
SidebarDevices.defaultProps = defaultProps;
export default React.memo(SidebarDevices);

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { CSidebar, CSidebarBrand, CSidebarNav } from '@coreui/react';
import PropTypes from 'prop-types';
import styles from './index.module.scss';
const Sidebar = ({
showSidebar,
setShowSidebar,
logo,
options,
redirectTo,
logoHeight,
logoWidth,
}) => (
<CSidebar show={showSidebar} onShowChange={(val) => setShowSidebar(val)}>
<CSidebarBrand className="d-md-down-none" to={redirectTo}>
<img
className={[styles.sidebarImgFull, 'c-sidebar-brand-full'].join(' ')}
style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }}
src={logo}
alt="OpenWifi"
/>
<img
className={[styles.sidebarImgMinimized, 'c-sidebar-brand-minimized'].join(' ')}
style={{ height: logoHeight ?? undefined, width: logoWidth ?? undefined }}
src={logo}
alt="OpenWifi"
/>
</CSidebarBrand>
<CSidebarNav>{options}</CSidebarNav>
</CSidebar>
);
Sidebar.propTypes = {
showSidebar: PropTypes.string.isRequired,
setShowSidebar: PropTypes.func.isRequired,
logo: PropTypes.string.isRequired,
options: PropTypes.node.isRequired,
redirectTo: PropTypes.string.isRequired,
logoHeight: PropTypes.string,
logoWidth: PropTypes.string,
};
Sidebar.defaultProps = {
logoHeight: null,
logoWidth: null,
};
export default React.memo(Sidebar);

View File

@@ -0,0 +1,9 @@
.sidebarImgFull {
height: 75px;
width: 175px;
}
.sidebarImgMinimized {
height: 75px;
width: 75px;
}

View File

@@ -4,13 +4,20 @@ import routes from 'routes';
import { CSidebarNavItem } from '@coreui/react';
import { cilBarcode, cilRouter, cilSave, cilSettings, cilPeople } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { Header, Sidebar, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs';
import { Header, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs';
import { WebSocketProvider } from 'contexts/WebSocketProvider';
import Sidebar from './Sidebar';
import SidebarDevices from './Devices';
const TheLayout = () => {
const [showSidebar, setShowSidebar] = useState('responsive');
const { endpoints, currentToken, user, avatar, logout } = useAuth();
const { t, i18n } = useTranslation();
const [newConnectionData, setNewConnectionData] = useState();
const onConnectionDataChange = React.useCallback((newData) => {
setNewConnectionData({ ...newData });
}, []);
return (
<div className="c-app c-default-layout">
@@ -50,6 +57,7 @@ const TheLayout = () => {
to="/system"
icon={<CIcon content={cilSettings} size="xl" className="mr-3" />}
/>
<SidebarDevices newData={newConnectionData} />
</>
}
redirectTo="/devices"
@@ -71,7 +79,7 @@ const TheLayout = () => {
/>
<div className="c-body">
<ToastProvider>
<WebSocketProvider>
<WebSocketProvider setNewConnectionData={onConnectionDataChange}>
<PageContainer t={t} routes={routes} redirectTo="/devices" />
</WebSocketProvider>
</ToastProvider>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import { CRow, CCol, CCard, CCardBody, CNav, CNavLink, CTabPane, CTabContent } from '@coreui/react';
import DeviceHealth from 'components/DeviceHealth';
import CommandHistory from 'components/CommandHistory';
@@ -7,7 +7,7 @@ import DeviceLogs from 'components/DeviceLogs';
import DeviceStatisticsCard from 'components/InterfaceStatistics';
import DeviceActionCard from 'components/DeviceActionCard';
import axiosInstance from 'utils/axiosInstance';
import { DeviceProvider, useAuth } from 'ucentral-libs';
import { DeviceProvider, useAuth, useToast } from 'ucentral-libs';
import { useTranslation } from 'react-i18next';
import ConfigurationDisplay from 'components/ConfigurationDisplay';
import WifiAnalysis from 'components/WifiAnalysis';
@@ -23,7 +23,9 @@ const DevicePage = () => {
const [index, setIndex] = useState(0);
const { currentToken, endpoints } = useAuth();
const [lastStats, setLastStats] = useState(null);
const { addToast } = useToast();
const [status, setStatus] = useState(null);
const history = useHistory();
const [deviceConfig, setDeviceConfig] = useState(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
@@ -64,7 +66,16 @@ const DevicePage = () => {
.then((response) => {
if (response) setDeviceConfig({ ...deviceInfo, extendedInfo: response.data.extendedInfo });
})
.catch(() => {
.catch((e) => {
if (e.response?.status === 404 || e.response?.status === 400) {
addToast({
title: t('common.error'),
body: t('device.mac_not_found'),
color: 'danger',
autohide: true,
});
history.push('/devices');
}
setDeviceConfig(deviceInfo);
});
};

View File

@@ -0,0 +1,189 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
CCard,
CCardBody,
CCardHeader,
CRow,
CCol,
CButton,
CPopover,
CModal,
CModalBody,
CModalHeader,
CModalTitle,
CDataTable,
} from '@coreui/react';
import Select from 'react-select';
import CIcon from '@coreui/icons-react';
import { cilSync, cilX } from '@coreui/icons';
import { prettyDate } from 'utils/helper';
import useToggle from 'hooks/useToggle';
import FormattedDate from 'components/FormattedDate';
const ApiStatusCard = ({ t, info, reload }) => {
const [types, setTypes] = useState([]);
const [showCerts, toggleCerts] = useToggle();
const submit = () => {
reload(
types.map((v) => v.value),
info.endpoint,
);
};
return (
<CCard>
<CCardHeader className="dark-header">
<div style={{ fontWeight: '600' }} className=" text-value-lg float-left">
{info.title}
</div>
</CCardHeader>
<CCardBody>
<CRow>
<CCol sm="4">
<div block="true">{t('common.endpoint')}:</div>
</CCol>
<CCol>
<div block="true">{info.endpoint}</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4">
<div block="true">{t('system.hostname')}:</div>
</CCol>
<CCol>
<div block="true">{info.hostname}</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4">
<div block="true">{t('system.os')}:</div>
</CCol>
<CCol>
<div block="true">{info.os}</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4">
<div block="true">{t('system.processors')}:</div>
</CCol>
<CCol>
<div block="true">{info.processors}</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4">
<div block="true">{t('common.start')}:</div>
</CCol>
<CCol>
<div block="true">
{info.start ? <FormattedDate date={info.start} /> : t('common.unknown')}
</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4">
<div block="true">{t('status.uptime')}:</div>
</CCol>
<CCol>
<div block="true">{info.uptime}</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4">
<div block="true">{t('footer.version')}:</div>
</CCol>
<CCol>
<div block="true">{info.version}</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4">
<div block="true">{t('common.certificates')}:</div>
</CCol>
<CCol>
<div block="true">
{info.certificates?.length > 0 ? (
<CButton className="ml-0 pl-0 py-0" color="link" onClick={toggleCerts}>
{t('common.details')} ({info.certificates.length})
</CButton>
) : (
<div>{t('common.unknown')}</div>
)}
</div>
</CCol>
</CRow>
<CRow>
<CCol sm="4" className="pt-1">
<div block="true">{t('system.reload_subsystems')}:</div>
</CCol>
<CCol>
<div block="true">
{info.subsystems.length === 0 ? (
t('common.unknown')
) : (
<div>
<div className="float-left" style={{ width: '85%' }}>
<Select
isMulti
closeMenuOnSelect={false}
name="Subsystems"
options={info.subsystems.map((sys) => ({ value: sys, label: sys }))}
onChange={setTypes}
value={types}
className="basic-multi-select"
classNamePrefix="select"
/>
</div>
<div className="float-left text-right" style={{ width: '15%' }}>
<CPopover content={t('system.reload')}>
<CButton
color="primary"
variant="outline"
onClick={submit}
disabled={types.length === 0}
>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
</div>
)}
</div>
</CCol>
</CRow>
</CCardBody>
<CModal size="lg" show={showCerts} onClose={toggleCerts}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('common.certificates')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleCerts}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<CDataTable
addTableClasses="table-sm"
border
items={info?.certificates.map((cert) => ({
...cert,
expiresOn: prettyDate(cert.expiresOn),
}))}
/>
</CModalBody>
</CModal>
</CCard>
);
};
ApiStatusCard.propTypes = {
t: PropTypes.func.isRequired,
info: PropTypes.instanceOf(Object).isRequired,
reload: PropTypes.func.isRequired,
};
export default React.memo(ApiStatusCard);

View File

@@ -1,22 +1,135 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { v4 as createUuid } from 'uuid';
import { CRow, CCol } from '@coreui/react';
import { useAuth, useToast } from 'ucentral-libs';
import { secondsToDetailed } from 'utils/helper';
import { useTranslation } from 'react-i18next';
import { SystemPage as Page, useToast, useAuth } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import ApiStatusCard from './ApiStatusCard';
const SystemPage = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [endpointsInfo, setEndpointsInfo] = useState([]);
const getSystemInfo = async (key, endpoint) => {
let systemInfo = {
title: key,
endpoint,
hostname: t('common.unknown'),
os: t('common.unknown'),
processors: t('common.unknown'),
uptime: t('common.unknown'),
version: t('common.unknown'),
certificates: [],
subsystems: [],
};
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
await axiosInstance
.get(`${endpoint}/api/v1/system?command=info`, options)
.then((newInfo) => {
systemInfo = { ...systemInfo, ...newInfo.data };
systemInfo.uptime = secondsToDetailed(
newInfo.data.uptime,
t('common.day'),
t('common.days'),
t('common.hour'),
t('common.hours'),
t('common.minute'),
t('common.minutes'),
t('common.second'),
t('common.seconds'),
);
systemInfo.start = newInfo.data.start;
})
.catch(() => {});
await axiosInstance
.post(`${endpoint}/api/v1/system`, { command: 'getsubsystemnames' }, options)
.then((newSubs) => {
systemInfo.subsystems = newSubs.data.list.sort((a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
})
.catch(() => {});
return systemInfo;
};
const reload = (subsystems, endpoint) => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
const parameters = {
command: 'reload',
subsystems,
};
axiosInstance
.post(`${endpoint}/api/v1/system?command=info`, parameters, options)
.then(() => {
addToast({
title: t('common.success'),
body: t('system.success_reload'),
color: 'success',
autohide: true,
});
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('system.error_reloading', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
});
};
const getAllInfo = async () => {
const promises = [];
for (const [key, value] of Object.entries(endpoints)) {
promises.push(getSystemInfo(key, value));
}
try {
const results = await Promise.all(promises);
setEndpointsInfo(results);
} catch (e) {
addToast({
title: t('common.error'),
body: t('system.error_fetching'),
color: 'danger',
autohide: true,
});
}
};
useEffect(() => {
getAllInfo();
}, []);
return (
<Page
t={t}
currentToken={currentToken}
endpoints={endpoints}
addToast={addToast}
axiosInstance={axiosInstance}
/>
<CRow>
{endpointsInfo.map((info) => (
<CCol sm="12" lg="6" xxl="4" key={createUuid()}>
<ApiStatusCard t={t} info={info} reload={reload} />
</CCol>
))}
</CRow>
);
};
export default SystemPage;

View File

@@ -13,7 +13,7 @@ const Routes = () => {
path="/"
name="Devices"
render={(props) =>
currentToken !== '' && Object.keys(endpoints).length !== 0 ? (
currentToken !== '' && endpoints && Object.keys(endpoints).length !== 0 ? (
<TheLayout {...props} />
) : (
<ToastProvider>

View File

@@ -1,5 +1,6 @@
import * as axios from 'axios';
import axiosRetry from 'axios-retry';
import { LOGOUT_ON_SEC_ERROR_CODES } from 'constants';
const axiosInstance = axios.create();
@@ -27,7 +28,7 @@ axiosInstance.interceptors.response.use(
retries += 1;
localStorage.setItem('sec_retries', retries);
}
if (error.response.data?.ErrorCode === 9) {
if (LOGOUT_ON_SEC_ERROR_CODES.includes(error.response.data?.ErrorCode)) {
localStorage.removeItem('access_token');
localStorage.removeItem('gateway_endpoints');
sessionStorage.clear();

View File

@@ -1,5 +1,3 @@
export const cleanTimestamp = (timestamp) => timestamp.replace('T', ' ').replace('Z', ' ');
export const cleanBytesString = (bytes, decimals = 2) => {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (!bytes || bytes === 0) {
@@ -132,6 +130,25 @@ export const compactSecondsToDetailed = (seconds, dayLabel, daysLabel, secondsLa
return finalString;
};
export const extraCompactSecondsToDetailed = (seconds) => {
let secondsLeft = seconds;
const days = Math.floor(secondsLeft / (3600 * 24));
secondsLeft -= days * (3600 * 24);
const hours = Math.floor(secondsLeft / 3600);
secondsLeft -= hours * 3600;
const minutes = Math.floor(secondsLeft / 60);
secondsLeft -= minutes * 60;
let finalString = '';
finalString = `${finalString}${prettyNumber(days)}:`;
finalString = `${finalString}${prettyNumber(hours)}:`;
finalString = `${finalString}${prettyNumber(minutes)}:`;
finalString = `${finalString}${prettyNumber(secondsLeft)}`;
return finalString;
};
export const validateEmail = (email) => {
const regex = /\S+@\S+\.\S+/;
return regex.test(email);
@@ -143,3 +160,25 @@ export const testRegex = (value, regexString) => {
};
export const datesSameDay = (first, second) => first.getDate() === second.getDate();
const units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
};
const rtf = new Intl.RelativeTimeFormat('en', { localeMatcher: 'best fit', style: 'long' });
export const formatDaysAgo = (d1, d2 = new Date()) => {
const convertedTimestamp = unixToDateString(d1);
const date = new Date(convertedTimestamp);
const elapsed = date - d2;
for (const [key] of Object.entries(units))
if (Math.abs(elapsed) > units[key] || key === 'second')
return rtf.format(Math.round(elapsed / units[key]), key);
return prettyDate(date);
};