Compare commits

...

47 Commits

Author SHA1 Message Date
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
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
43 changed files with 1471 additions and 597 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-RC2
pullPolicy: Always
services:

377
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.7.0",
"version": "2.7.0(8)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.7.0",
"version": "2.7.0(8)",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -35,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": {
@@ -2069,6 +2069,64 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -7320,60 +7378,6 @@
"node": ">=12"
}
},
"node_modules/html-minifier-terser/node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/html-minifier-terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/html-minifier-terser/node_modules/terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"acorn": "^8.5.0"
},
"peerDependenciesMeta": {
"acorn": {
"optional": true
}
}
},
"node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -9201,9 +9205,9 @@
}
},
"node_modules/moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
@@ -11795,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"
@@ -13073,6 +13077,24 @@
"node": ">=6"
}
},
"node_modules/terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz",
@@ -13107,26 +13129,6 @@
}
}
},
"node_modules/terser-webpack-plugin/node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/terser-webpack-plugin/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/terser-webpack-plugin/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -13192,39 +13194,23 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/terser-webpack-plugin/node_modules/terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"node_modules/terser/node_modules/acorn": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"dev": true,
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
"acorn": "bin/acorn"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"acorn": "^8.5.0"
},
"peerDependenciesMeta": {
"acorn": {
"optional": true
}
"node": ">=0.4.0"
}
},
"node_modules/terser-webpack-plugin/node_modules/terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true,
"engines": {
"node": ">= 8"
}
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/text-table": {
"version": "0.2.0",
@@ -13420,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",
@@ -14269,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",
@@ -16414,6 +16400,55 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -20424,41 +20459,6 @@
"param-case": "^3.0.4",
"relateurl": "^0.2.7",
"terser": "^5.10.0"
},
"dependencies": {
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
}
}
}
}
},
"html-parse-stringify": {
@@ -21815,9 +21815,9 @@
}
},
"moment": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"mrmime": {
"version": "1.0.0",
@@ -23690,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": {
@@ -24722,6 +24722,32 @@
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true
},
"terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dev": true,
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"dependencies": {
"acorn": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"dev": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
}
}
},
"terser-webpack-plugin": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz",
@@ -24735,20 +24761,6 @@
"terser": "^5.7.2"
},
"dependencies": {
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -24791,25 +24803,6 @@
"requires": {
"has-flag": "^4.0.0"
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
}
}
}
}
},
@@ -24975,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",
@@ -25660,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.7.0",
"version": "2.7.0(8)",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
@@ -29,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

@@ -326,6 +326,7 @@
"device": {
"add_to_blacklist": "Gerät zur Blacklist hinzufügen",
"all_devices": "Alle Geräte",
"already_running_command": "Gerät führt bereits einen Befehl aus, bitte versuchen Sie es später erneut",
"blacklisted_on": "Datum",
"capabilities": "Fähigkeiten",
"certificate_explanation": "Zertifikate der angeschlossenen Geräte",
@@ -337,6 +338,7 @@
"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}",
"firmware_count_explanation": "Dies ist die Gesamtzahl der Geräte, die diesem Firmware-Server hinzugefügt wurden, einschließlich der Geräte, die derzeit nicht auf den zugehörigen Gateway-Server verweisen.",
"health_explanation": "Zustand der verbundenen Geräte ((Geräte = 100 % * 100 + Geräte > 90 % * 95 + Geräte > 60 % * 75 + Geräte < 60 % * 35) / Verbundene Geräte)",
"mac_not_found": "Seriennummer nicht gefunden, Sie werden zur Seite „Geräte“ weitergeleitet",
"memory_explanation": "Anzahl verbundener Geräte mit entsprechendem belegtem Speicher %",
"remove_from_blacklist": "Von der schwarzen Liste entfernen",
"success_added_blacklist": "Gerät erfolgreich zur Blacklist hinzugefügt!",
@@ -720,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": {
@@ -814,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

@@ -326,6 +326,7 @@
"device": {
"add_to_blacklist": "Add Device To Blacklist",
"all_devices": "All Devices",
"already_running_command": "Device is already executing a command, please try later",
"blacklisted_on": "Date",
"capabilities": "Capabilities",
"certificate_explanation": "Certificates of connected devices",
@@ -337,6 +338,7 @@
"error_fetching_devices": "Error while fetching devices: {{error}}",
"firmware_count_explanation": "This is the total amount of devices that were added to this firmware server, including devices not currently pointing at the related gateway server.",
"health_explanation": "Health of connected devices ((Devices=100% * 100 + Devices>90% * 95 + Devices>60% * 75 + Devices<60% * 35) / ConnectedDevices)",
"mac_not_found": "Serial number not found, redirecting you to the Devices page",
"memory_explanation": "Amount of connected devices with corresponding memory used percentage",
"remove_from_blacklist": "Remove from blacklist",
"success_added_blacklist": "Device successfully added to blacklist!",
@@ -720,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": {
@@ -814,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

@@ -326,6 +326,7 @@
"device": {
"add_to_blacklist": "Agregar dispositivo a la lista negra",
"all_devices": "Todos los dispositivos",
"already_running_command": "El dispositivo ya está ejecutando un comando, intente más tarde",
"blacklisted_on": "Fecha",
"capabilities": "capacidades",
"certificate_explanation": "Certificados de dispositivos conectados",
@@ -337,6 +338,7 @@
"error_fetching_devices": "Error al recuperar dispositivos: {{error}}",
"firmware_count_explanation": "Esta es la cantidad total de dispositivos que se agregaron a este servidor de firmware, incluidos los dispositivos que actualmente no apuntan al servidor de puerta de enlace relacionado.",
"health_explanation": "Estado de los dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos conectados)",
"mac_not_found": "Número de serie no encontrado, lo redirige a la página Dispositivos",
"memory_explanation": "Cantidad de dispositivos conectados con la memoria correspondiente utilizada%",
"remove_from_blacklist": "ELIMINAR DE LA LISTA NEGRA",
"success_added_blacklist": "¡Dispositivo agregado exitosamente a la lista negra!",
@@ -720,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": {
@@ -814,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

@@ -326,6 +326,7 @@
"device": {
"add_to_blacklist": "Ajouter un appareil à la liste noire",
"all_devices": "Tous les dispositifs",
"already_running_command": "L'appareil exécute déjà une commande, veuillez réessayer plus tard",
"blacklisted_on": "Rendez-vous amoureux",
"capabilities": "Capacités",
"certificate_explanation": "Certificats des appareils connectés",
@@ -337,6 +338,7 @@
"error_fetching_devices": "Erreur lors de la récupération des appareils : {{error}}",
"firmware_count_explanation": "Il s'agit du nombre total d'appareils qui ont été ajoutés à ce serveur de micrologiciel, y compris les appareils qui ne pointent pas actuellement vers le serveur de passerelle associé.",
"health_explanation": "Santé des appareils connectés ((Appareils = 100 % * 100 + Appareils> 90 % * 95 + Appareils> 60 % * 75 + Appareils < 60 % * 35) / Appareils connectés)",
"mac_not_found": "Numéro de série introuvable, vous redirigeant vers la page Appareils",
"memory_explanation": "Nombre d'appareils connectés avec la mémoire correspondante utilisée %",
"remove_from_blacklist": "Supprimer de la liste noire",
"success_added_blacklist": "Appareil ajouté avec succès à la liste noire !",
@@ -720,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": {
@@ -814,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

@@ -326,6 +326,7 @@
"device": {
"add_to_blacklist": "Adicionar dispositivo à lista negra",
"all_devices": "Todos os dispositivos",
"already_running_command": "O dispositivo já está executando um comando, tente mais tarde",
"blacklisted_on": "Encontro",
"capabilities": "Recursos",
"certificate_explanation": "Certificados de dispositivos conectados",
@@ -337,6 +338,7 @@
"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}",
"firmware_count_explanation": "Esta é a quantidade total de dispositivos que foram adicionados a este servidor de firmware, incluindo dispositivos que não estão apontando para o servidor de gateway relacionado.",
"health_explanation": "Integridade dos dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos Conectados)",
"mac_not_found": "Número de série não encontrado, redirecionando você para a página Dispositivos",
"memory_explanation": "Quantidade de dispositivos conectados com a memória correspondente usada%",
"remove_from_blacklist": "Remover da lista negra",
"success_added_blacklist": "Dispositivo adicionado à lista negra com sucesso!",
@@ -720,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": {
@@ -814,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>
),

View File

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

View File

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

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

@@ -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,6 +13,7 @@ const DeviceSearchBar = ({ action }) => {
const [waitingSearch, setWaitingSearch] = useState('');
const search = (value) => {
if (socket) {
if (socket.readyState === WebSocket.OPEN) {
if (value.length > 1 && value.match('^[a-fA-F0-9-*]+$')) {
setWaitingSearch('');
@@ -23,12 +23,13 @@ const DeviceSearchBar = ({ action }) => {
} else {
setResults([]);
}
} else if (socket.readyState !== WebSocket.CONNECTING) {
} else if (socket.readyState !== WebSocket.CONNECTING && endpoints?.owgw !== undefined) {
setWaitingSearch(value);
setSocket(new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`));
} else {
setWaitingSearch(value);
}
}
};
const closeSocket = () => {
@@ -59,12 +60,20 @@ const DeviceSearchBar = ({ action }) => {
}, [socket]);
useEffect(() => {
if (socket === null && endpoints?.owgw) {
if (socket === null && endpoints?.owgw !== undefined) {
setSocket(new WebSocket(`${endpoints.owgw.replace('https', 'wss')}/api/v1/ws`));
}
}, []);
return <SearchBar t={t} search={search} results={results} history={history} action={action} />;
return (
<DeviceSearchBarInput
search={search}
results={results}
history={history}
action={action}
isDisabled={endpoints.owgw === undefined}
/>
);
};
DeviceSearchBar.propTypes = {

View File

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

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,6 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { CSpinner } from '@coreui/react';
import { CSpinner, CAlert } from '@coreui/react';
import { useTranslation } from 'react-i18next';
import { v4 as createUuid } from 'uuid';
import axiosInstance from 'utils/axiosInstance';
@@ -23,8 +23,10 @@ const StatisticsChartList = ({ deviceSerialNumber, setOptions, section, time })
memory: [],
settings: {},
});
const [error, setError] = useState(false);
const transformIntoDataset = (data) => {
try {
let sortedData = data.sort((a, b) => {
if (a.recorded > b.recorded) return 1;
if (b.recorded > a.recorded) return -1;
@@ -244,9 +246,35 @@ const StatisticsChartList = ({ deviceSerialNumber, setOptions, section, time })
setOptions([...sectionOptions, { value: 'memory', label: t('statistics.memory') }]);
setStatOptions({ ...newOptions });
}
setError(undefined);
} catch (e) {
if (data?.length === 0) {
setError('nodata');
} else {
setError('error');
}
}
};
const getInterface = useCallback(() => {
if (error === 'error') {
return (
<div style={{ textAlign: 'center' }}>
<CAlert color="danger" style={{ width: '240px', margin: 'auto' }}>
Error while parsing statistics
</CAlert>
</div>
);
}
if (error === 'nodata') {
return (
<div style={{ textAlign: 'center' }}>
<CAlert color="danger" style={{ width: '340px', margin: 'auto' }}>
No available statistics during this time period
</CAlert>
</div>
);
}
if (statOptions.interfaceList.length === 0) return <p>N/A</p>;
const interfaceToShow = statOptions.interfaceList.find(
@@ -273,8 +301,9 @@ const StatisticsChartList = ({ deviceSerialNumber, setOptions, section, time })
</div>
);
}
return <p>N/A</p>;
}, [statOptions, section]);
}, [statOptions, section, error]);
const getStatistics = () => {
setLoading(true);

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) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: t('telemetry.connection_failed', { error: e.response?.data?.ErrorDescription }),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
})
.finally(() => setLoading(false));
};
@@ -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

@@ -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,6 +254,12 @@ const WifiAnalysis = () => {
</div>
</CCardHeader>
<CCardBody>
{!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
@@ -274,7 +280,11 @@ const WifiAnalysis = () => {
</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} />
<RadioAnalysisTable
data={selectedRadioStats ?? []}
loading={loading}
range={range}
/>
<h5 className="pt-5 pb-3 text-center">{t('wifi_analysis.associations')}</h5>
<WifiAnalysisTable
t={t}
@@ -283,6 +293,8 @@ const WifiAnalysis = () => {
range={range}
/>
</div>
</>
)}
</CCardBody>
</CCard>
<CModal size="xl" show={showModal} onClose={toggleModal}>

View File

@@ -20,31 +20,22 @@ import PropTypes from 'prop-types';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import { prettyDateForFile } from 'utils/helper';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth, useDevice, useToast } from 'ucentral-libs';
import WifiChannelTable from 'components/WifiScanResultModal/WifiChannelTable';
import 'react-widgets/styles.css';
import { CSVLink } from 'react-csv';
import Select from 'react-select';
import IeDisplay from 'components/WifiScanResultModal/IeDisplay';
import IE_OPTIONS from './IE_OPTIONS.json';
const getIeOptions = () => {
const arr = [];
for (const [key, value] of Object.entries(IE_OPTIONS)) {
arr.push({
label: `${key} (${value})`,
value,
});
}
return arr;
};
const allIes = Object.entries(IE_OPTIONS).map(([, value]) => value);
const WifiScanModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [hadSuccess, setHadSuccess] = useState(false);
const [selectedIes, setSelectedIes] = useState(undefined);
const [ies, setIes] = useState([]);
const [hadFailure, setHadFailure] = useState(false);
const [errorCode, setErrorCode] = useState(0);
const [waiting, setWaiting] = useState(false);
@@ -63,14 +54,6 @@ const WifiScanModal = ({ show, toggleModal }) => {
setActiveScan(!activeScan);
};
const onIesChange = (v) => {
if (v.find(({ value }) => value === '*')) {
setIes(getIeOptions());
} else {
setIes(v);
}
};
useEffect(() => {
setHadSuccess(false);
setHadFailure(false);
@@ -82,7 +65,7 @@ const WifiScanModal = ({ show, toggleModal }) => {
setActiveScan(false);
setHideOptions(false);
setErrorCode(0);
setIes(undefined);
setSelectedIes(undefined);
}, [show]);
const parseThroughList = (scanList) => {
@@ -164,7 +147,7 @@ const WifiScanModal = ({ show, toggleModal }) => {
override_dfs: dfs,
bandwidth: bandwidth !== '' ? bandwidth : undefined,
activeScan,
ies: ies.length > 0 ? ies.map(({ value }) => value) : undefined,
ies: allIes,
};
const headers = {
Accept: 'application/json',
@@ -190,7 +173,18 @@ const WifiScanModal = ({ show, toggleModal }) => {
setHadFailure(true);
}
})
.catch(() => {
.catch((e) => {
if (e.response?.data?.ErrorDescription !== undefined) {
const split = e.response?.data?.ErrorDescription.split(':');
if (split !== undefined && split.length >= 2) {
addToast({
title: t('common.error'),
body: split[1],
color: 'danger',
autohide: true,
});
}
}
setHadFailure(true);
})
.finally(() => {
@@ -289,23 +283,6 @@ const WifiScanModal = ({ show, toggleModal }) => {
</CSelect>
</CCol>
</CRow>
<CRow className="mt-3">
<CCol md="3">
<p className="pl-2">{t('actions.request_ie')}:</p>
</CCol>
<CCol>
<Select
isMulti
closeMenuOnSelect={false}
name="request_ie"
options={[{ label: 'All', value: '*' }, ...getIeOptions()]}
onChange={onIesChange}
value={ies}
className="basic-multi-select"
classNamePrefix="select"
/>
</CCol>
</CRow>
</div>
<div hidden={!waiting}>
<CRow>
@@ -332,10 +309,10 @@ const WifiScanModal = ({ show, toggleModal }) => {
</CCol>
</CRow>
)}
{selectedIes && <IeDisplay ies={selectedIes} setIes={setSelectedIes} />}
{selectedIes || channelList === null ? null : (
<WifiChannelTable channels={channelList} setIes={setSelectedIes} />
)}
{selectedIes && <IeDisplay ies={selectedIes} setIes={setSelectedIes} />}
</div>
</CModalBody>
</CModal>

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

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);
};