Compare commits

...

190 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
Charles
c3574d96d7 2.7.0 2022-06-01 16:44:07 +01:00
Dmitry Dunaev
ebd2419634 [WIFI-7555] Fix: helm path
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-05-23 15:21:59 +03:00
Johann Hoffmann
133c256543 Enable CI for pull requests in release branches
Signed-off-by: Johann Hoffmann <johann.hoffmann@mailbox.org>
2022-05-23 13:14:42 +02:00
Charles
98a2a72f33 2.6.25: device dashboard pie charts now have both absolute and percentage values, added device total explanations 2022-05-18 16:48:04 +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
Charles
f008fd082e 2.6.23: websocket memory leak fix, fixes for device page refresh on websocket notification 2022-05-05 20:34:58 +01:00
Charles
d2fd895582 2.6.20: reboot/blink/trace UI fixes, now using global websocket to update UI and notify user on device connection/disconnection 2022-05-04 21:28:47 +01:00
Dmitry Dunaev
746a812ae8 Merge pull request #89 from Telecominfraproject/feature/wifi-7825--use-clusterip-for-service
[WIFI-7825] Chg: use ClusterIP for service by default
2022-05-03 12:14:18 +03:00
Dmitry Dunaev
b67c69b88b [WIFI-7825] Chg: use ClusterIP for service by default
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-05-03 12:07:49 +03:00
Charles
f6ee20730a Merge pull request #87 from stephb9959/main
Device Search fix
2022-04-28 21:23:20 +01:00
Charles
2829a96c84 Merge pull request #186 from stephb9959/dev
Device search fix
2022-04-28 21:21:56 +01:00
Charles
37e1a92a89 Device search fix 2022-04-28 21:10:01 +01:00
Charles
81c4717472 Merge pull request #86 from stephb9959/main
2.6.14
2022-04-19 18:20:10 +01:00
Charles
94aac686c9 Merge pull request #181 from stephb9959/dev
2.6.14: statistics fix for devices with more than one interface
2022-04-19 18:09:43 +01:00
Charles
b75848515b 2.6.14: statistics fix for devices with more than one interface 2022-04-19 15:51:00 +01:00
Dmitry Dunaev
a26cf9a3ff [WIFI-7555] Add: Helm packaging and GitHub release step
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-04-18 11:19:27 +03:00
Dmitry Dunaev
a7e4f728d2 [WIFI-7461] Add: trigger-deploy-to-dev step in CI
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-04-13 13:01:59 +03:00
Charles
921d234972 Merge pull request #179 from stephb9959/dev
2.6.13
2022-04-05 19:41:41 +01:00
Charles
6bec9f977f 2.6.13 2022-04-05 19:41:09 +01:00
Charles
6eaa9f8af1 Merge pull request #177 from stephb9959/dev
2.6.12: statistics fix for negative values
2022-04-05 18:28:34 +01:00
Charles
5ef189b445 2.6.12: statistics fix for negative values 2022-04-05 18:27:06 +01:00
Charles
9f8283892e Merge pull request #175 from stephb9959/dev
2.6.11: fix for negative interface deltas
2022-04-05 18:02:19 +01:00
Charles
6ba2dc9601 2.6.11: fix for negative interface deltas 2022-04-05 18:01:40 +01:00
Dmitry Dunaev
e23512c860 [WIFI-4884] Add: more clear slack message on failure
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-03-28 12:58:58 +03:00
Dmitry Dunaev
32b6fe1625 Merge pull request #85 from Telecominfraproject/feature/wifi-4884--add-slack-failure-notify
[WIFI-4884] Add: notification on CI failure in Slack
2022-03-24 14:51:08 +03:00
Dmitry Dunaev
8663b6d108 [WIFI-4884] Add: notification on CI failure in Slack
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-03-24 14:50:37 +03:00
Charles
7794aa4c99 Merge pull request #84 from stephb9959/main
2.6.10
2022-03-18 18:47:57 +00:00
Charles
ba90ea59f4 Merge pull request #171 from stephb9959/dev
2.6.9
2022-03-18 18:47:26 +00:00
Charles
aadb4c44a1 2.6.10 2022-03-18 17:00:19 +00:00
Charles
467ad39873 2.6.9: wifi analysis vendor value fix 2022-03-18 16:29:57 +00:00
Charles
0a92b2db48 Merge pull request #83 from stephb9959/main
2.6.8
2022-03-18 13:33:40 +00:00
Charles
60a8f1ea61 Merge pull request #169 from stephb9959/dev
2.6.8
2022-03-18 13:31:53 +00:00
Charles
1063061b47 2.6.8: now using station instead of bssid within the wifi analysis table 2022-03-18 13:09:43 +00:00
Charles
54186575e0 2.6.7: displaying entity/venue/subscriber wth buttons to go to provisioning directly within device details 2022-03-15 18:17:55 +00:00
Dmitry Dunaev
114005d572 Merge pull request #82 from Telecominfraproject/feature/wifi-1998--ingress-deprecation
[WIFI-1998] Add: gracefull ingress deprecationush
2022-03-01 16:22:44 +03:00
Dmitry Dunaev
cde59a5ab1 [WIFI-1998] Add: gracefull ingress deprecationush
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-03-01 16:17:26 +03:00
Charles
e6bb26ce12 2.6.6: added entity venue and mac to device details 2022-02-25 15:59:36 +02:00
Charles
0cde953d58 2.6.5: presentation fix for locale in device details 2022-02-24 22:35:10 +02:00
Charles
ce7a804a70 2.6.4: added more details such as locale in device details, added locale info and flag to device table 2022-02-24 22:19:35 +02:00
Charles
67716aedde 2.6.2: fixed configuration display copy to clipboard format, added copy to clipboard button to latest statistics modal 2022-02-22 21:39:58 +02:00
Charles
54a98cd6e5 Merge pull request #81 from stephb9959/main
2.6.1
2022-02-21 21:04:47 +02:00
Charles
31a901bea9 Merge pull request #161 from stephb9959/dev
2.6.1
2022-02-21 21:04:18 +02:00
Charles
f0fdc90226 2.6.1: added bandwidth option to wifi scan 2022-02-21 20:03:31 +02:00
Charles
e14f892bc6 2.6.0: configure command feedback more clear 2022-02-11 13:52:25 +00:00
Charles
39158b0d1e Merge pull request #80 from stephb9959/main
2.5.44
2022-02-10 18:59:48 +00:00
Charles
acc264534e Merge pull request #160 from stephb9959/dev
2.5.44: allowing for the * symbol to be used on the device search bar
2022-02-10 18:58:38 +00:00
Charles
48dcb4acbf 2.5.44: allowing for the * symbol to be used on the device search bar 2022-02-10 15:11:04 +00:00
Charles
1c40f9eb4c Merge pull request #158 from stephb9959/dev
2.5.43
2022-02-09 18:18:19 +00:00
Charles
9e418eb423 New wifiscan icon 2022-02-09 18:08:33 +00:00
Charles
e4ff3a87a7 2.5.43: download wifi scan button added to wifi scan modal 2022-02-09 13:59:07 +00:00
Charles
e82551c97f Extension fix for wifi scan download 2022-02-08 19:12:24 +00:00
Charles
d877a4aecf 2.5.42: wifi scan download filename fix 2022-02-08 19:06:40 +00:00
Charles
5c50a40bdb 2.5.41: download wifi scan button 2022-02-08 18:45:52 +00:00
Charles
9caf0f375c 2.5.40: interface statistics negative deltas are now displayed as 0, fix on device page for when provisioning cannot be contacted 2022-02-08 17:06:28 +00:00
Johann Hoffmann
1ed8285452 Use docker-image-build composite action
Signed-off-by: Johann Hoffmann <johann.hoffmann@mailbox.org>
2022-02-08 17:45:14 +01:00
Charles
16170e613c Merge pull request #151 from stephb9959/dev
2.5.39
2022-02-07 22:16:07 +00:00
Charles
b3fb45dd36 2.5.39: statistics for v1 interfaces fix 2022-02-07 22:05:05 +00:00
Charles
54f5912da6 2.5.38: fix for v1 interface stats 2022-02-07 15:53:02 +00:00
Charles
8542ded488 2.5.37: memory graph fix, wifi scan results now contain the meshid when applicable 2022-02-07 15:35:59 +00:00
Charles
e6561faf8c Merge pull request #79 from stephb9959/main
2.5.36
2022-02-03 20:49:45 +01:00
Charles
f291b7b0fc Merge pull request #144 from stephb9959/dev
2.5.36
2022-02-03 20:45:02 +01:00
Charles
d9ea2abf1a 2.5.36: fix for device statistics version 1 2022-02-03 20:30:02 +01:00
Charles
60a072809b 2.5.35: now displaying 'waiting for update' when lastStats arent fetched yet on device status card 2022-01-28 14:58:01 +01:00
Dmitry Dunaev
9828d6457d [WIFI-6837] Chg: helm service type to NodePort
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-01-28 16:19:58 +03:00
Charles
d6f390d7d4 2.5.34: wifi scan can now use the override_dfs option, shows alert if error code = 1 2022-01-27 20:28:12 +01:00
Charles
6ddafc8de0 Merge pull request #78 from stephb9959/main
2.5.33
2022-01-25 16:31:00 +01:00
Charles
ed5c83cf66 Merge pull request #137 from stephb9959/dev
2.5.33: login mfa fix, new copy button
2022-01-25 16:02:57 +01:00
Charles
bf227b5e6f 2.5.33: login mfa fix, new copy button 2022-01-25 15:11:33 +01:00
Charles
8bc1350e2e Merge pull request #77 from stephb9959/main
2.5.32
2022-01-21 19:38:04 +01:00
Charles
3aa0dd2f51 Merge pull request #136 from stephb9959/dev
2.5.32
2022-01-21 19:35:49 +01:00
Charles
6c1f1e1db7 2.5.32: Dependencies fix 2022-01-21 18:32:42 +01:00
Charles
0c615fcb3b 2.5.31: added security retries display on error code 13 2022-01-21 17:56:41 +01:00
Charles
3ca900af6c 2.5.30: fix for device statistics 2022-01-18 19:42:08 +01:00
Charles
1481626b1b 2.5.29: fixes for statistics graphs 2022-01-18 14:36:42 +01:00
Charles
a0ba5aeca4 2.5.28: added deltas as possible source of tx/rx from associations 2022-01-18 11:10:13 +01:00
Charles
f48a922b4c Merge pull request #76 from stephb9959/main
Version 2.5.27
2022-01-17 22:08:46 +01:00
Charles
2418273191 Merge pull request #129 from stephb9959/dev
Version 2.5.25
2022-01-17 22:04:47 +01:00
Charles
09a10d7838 2.5.27: standardized command history, health and logs, wifi analysis table fixes 2022-01-17 19:16:36 +01:00
Charles
40ed1dd612 2.5.26: fixing for empty vendors in wifi analysis 2022-01-17 15:38:48 +01:00
Charles
2aa38f1117 Version 2.5.25: added refresh to logs/health/wifi analysis, added vendors to wifi analysis, changed configure feedback to use toast 2022-01-17 15:30:45 +01:00
Charles
5d81ad9830 Merge pull request #75 from stephb9959/main
Version 2.5.24
2022-01-14 20:12:16 +01:00
Charles
dffb45e233 Merge pull request #122 from stephb9959/dev
Version 2.5.24
2022-01-14 19:57:12 +01:00
Charles
5606c7b29a Version 2.5.24 2022-01-14 15:58:54 +01:00
Charles
9d5b4f63d3 Custom time on device stats 2022-01-14 15:17:10 +01:00
Charles
2c353023ab Merge pull request #74 from stephb9959/main
Version 2.5.18
2022-01-14 14:32:48 +01:00
Charles
6992cdbaa4 2.5.21: datepicker fix and added datepicker to device stats 2022-01-13 16:47:38 +01:00
Charles
9576079bfa 2.5.20: memory graph addded and can choose between interfaces 2022-01-13 15:53:29 +01:00
Charles
1e08ccaae3 Version 2.5.19: new memory display 2022-01-12 15:44:07 +01:00
Charles
80e07eb53a Merge pull request #73 from Telecominfraproject/release/v2.5.0
Version 2.5.18
2022-01-12 15:06:20 +01:00
Charles
54b7a27e65 Version 2.5.18 2022-01-12 14:49:09 +01:00
bourquecharles
5dc6100e8e Version 2.5.18 2022-01-12 14:20:11 +01:00
bourquecharles
a5c1a7122d Version 2.5.18: user fixes, blink only now, fix for connect loading 2022-01-10 09:48:54 +01:00
bourquecharles
61442462c7 Version 2.5.17 2021-12-28 14:38:03 -05:00
Dmitry Dunaev
917c31bef4 Merge pull request #71 from Telecominfraproject/feature/wifi-4977--introduce-revisionHistoryLimit
[WIFI-4977] Add: helm add revisionHistoryLimit support
2021-12-23 16:27:19 +03:00
Dmitry Dunaev
989439587f [WIFI-4977] Add: helm add revisionHistoryLimit support
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2021-12-23 16:10:27 +03:00
bourquecharles
e74130733e Version 2.5.16 2021-12-22 09:48:09 -05:00
bourquecharles
2cce9a4f4c Added all device images 2021-12-09 12:46:41 -05:00
bourquecharles
b721dfeb71 New labels 2021-12-07 17:18:30 -05:00
bourquecharles
645b9b2c37 New labels 2021-12-07 15:23:49 -05:00
bourquecharles
b419ebfe5d Version 2.5.14 2021-12-07 09:58:48 -05:00
bourquecharles
96d40a2946 Version 2.5.13 2021-12-03 09:07:34 -05:00
bourquecharles
e9b40573c7 Version 2.5.12 2021-12-02 16:37:05 -05:00
bourquecharles
ad316dfeac Image for devices 2021-12-02 16:16:43 -05:00
bourquecharles
62cbaf3c04 New labels 2021-12-01 14:29:27 -05:00
bourquecharles
cdb7eb3da9 Version 2.5.10 2021-12-01 12:07:50 -05:00
bourquecharles
671e0bbf71 Version 2.5.9 2021-11-25 17:07:38 -05:00
bourquecharles
50704b7b6a Version 2.5.8 2021-11-25 16:55:47 -05:00
bourquecharles
c198d1f593 Device page bugfix 2021-11-25 11:00:15 -05:00
bourquecharles
c91cd2eecf Dashboard fixes, other bugfixes 2021-11-25 09:59:03 -05:00
Charles
31e47f4a04 New labels 2021-11-22 17:02:17 -05:00
Charles
470a9c4afa Device list fix 2021-11-22 09:28:21 -05:00
Charles
98692be3ba Merge pull request #69 from Telecominfraproject/revert-68-main
Revert "Version 2.4.2"
2021-11-19 16:31:50 -05:00
Charles
5c5077d7ec Revert "Version 2.4.2" 2021-11-19 16:31:41 -05:00
Charles
c13cae9ab3 Version 2.5.3 2021-11-19 15:01:07 -05:00
Charles
74de687b90 Label fix 2021-11-19 14:44:47 -05:00
Charles
ed3aca7d0c Version 2.5.2 2021-11-19 14:41:04 -05:00
Charles
c9467f31c8 Version 2.5.1 2021-11-19 12:20:02 -05:00
Charles
ad08632809 Merge pull request #68 from stephb9959/main
Version 2.4.2
2021-11-19 11:15:18 -05:00
Charles
bea47b2640 Login page fix 2021-11-19 09:34:48 -05:00
Charles
242078ec15 Dependency fix 2021-11-19 09:12:41 -05:00
Charles
5ca140df46 Reverting ucentral-libs version 2021-11-19 09:06:37 -05:00
Charles
1259212cb2 Merge branch 'main' into dev 2021-11-19 09:00:00 -05:00
Charles
299c43e10d Version 2.5 2021-11-19 08:42:54 -05:00
Dmitry Dunaev
969450cad3 [WIFI-4860] Chg: apply enforce-jira-issue-key only to PRs to release branches 2021-11-19 16:25:41 +03:00
Dmitry Dunaev
3da330b637 Merge pull request #67 from Telecominfraproject/feature/wifi-4860--add-ensure-jira-issue-key-workflow
[WIFI-4860] Add: enforce-jira-issue-key workflow
2021-11-19 15:49:57 +03:00
Dmitry Dunaev
86bd64e887 [WIFI-4860] Add: enforce-jira-issue-key workflow
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2021-11-19 13:19:48 +03:00
Charles
e02f939cb8 Merge pull request #66 from stephb9959/main
Version 2.4.1
2021-11-15 17:27:26 -05:00
Charles
4bde6e2d1f Merge pull request #65 from stephb9959/main
Version 2.4.0
2021-11-15 16:58:40 -05:00
Charles
ee69783a66 Merge pull request #64 from stephb9959/main
Version 2.3.20
2021-11-13 07:43:45 -05:00
Charles
cddb0e94fa Merge pull request #63 from stephb9959/main
Version 2.3.18
2021-11-12 16:41:07 -05:00
Charles
b1277ff2ac Merge pull request #62 from stephb9959/main
Version 2.3.16
2021-11-09 11:42:40 -05:00
Max
262c1fe1e2 allow to set pod annotations (#61) 2021-11-09 13:29:16 +01:00
Charles
2d2603ff27 Merge pull request #60 from stephb9959/main
Version 2.3.12
2021-11-02 16:42:57 -04:00
Charles
d349e43523 Merge pull request #59 from stephb9959/main
Version 2.3.11
2021-11-01 17:29:31 -04:00
Charles
ebf2d7d5c6 Merge pull request #58 from stephb9959/main
Version 2.3.9
2021-10-28 14:27:24 -04:00
Charles
411c618be1 Merge pull request #57 from stephb9959/main
Version 2.3.2
2021-10-26 17:20:45 -04:00
Charles
6151dcb8ff Merge pull request #56 from stephb9959/main
Version 2.2.12
2021-10-20 13:55:26 -04:00
Charles
3564abfa29 Merge pull request #55 from stephb9959/main
Version 2.2.11
2021-10-19 11:50:48 -04:00
Dmitry Dunaev
955becdb46 Merge pull request #54 from Telecominfraproject/fix/wifi-4923--helm-git-readme
[WIFI-4923] Fix: helm-git link in chart README
2021-10-19 11:50:52 +03:00
Dmitry Dunaev
daba3a3f28 [WIFI-4923] Fix: helm-git link in chart README 2021-10-19 11:40:47 +03:00
Charles
33b8d1a1f5 Merge pull request #53 from stephb9959/main
Version 2.2.8
2021-10-14 14:29:45 -04:00
Charles
5e35c23883 Merge pull request #52 from stephb9959/main
Version 2.2.5
2021-10-12 12:02:18 -04:00
Charles
601c369f2d Merge pull request #51 from stephb9959/main
Version 2.2.2
2021-09-28 14:59:12 -04:00
Charles
d48925f9ba Merge pull request #50 from stephb9959/main
2.2.1
2021-09-28 11:45:53 -04:00
146 changed files with 12829 additions and 9529 deletions

View File

@@ -12,6 +12,7 @@ on:
pull_request:
branches:
- main
- 'release/*'
defaults:
run:
@@ -24,45 +25,48 @@ jobs:
DOCKER_REGISTRY_URL: tip-tip-wlan-cloud-ucentral.jfrog.io
DOCKER_REGISTRY_USERNAME: ucentral
steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: docker build -t owgw-ui:${{ github.sha }} .
- name: Tag Docker image
run: |
TAGS="${{ github.sha }}"
if [[ ${GITHUB_REF} == "refs/heads/"* ]]
then
CURRENT_TAG=$(echo ${GITHUB_REF#refs/heads/} | tr '/' '-')
TAGS="$TAGS $CURRENT_TAG"
else
if [[ ${GITHUB_REF} == "refs/tags/"* ]]
then
CURRENT_TAG=$(echo ${GITHUB_REF#refs/tags/} | tr '/' '-')
TAGS="$TAGS $CURRENT_TAG"
else # PR build
CURRENT_TAG=$(echo ${GITHUB_HEAD_REF#refs/heads/} | tr '/' '-')
TAGS="$TAGS $CURRENT_TAG"
fi
fi
echo "Result tags: $TAGS"
for tag in $TAGS; do
docker tag owgw-ui:${{ github.sha }} ${{ env.DOCKER_REGISTRY_URL }}/owgw-ui:$tag
done
- name: Log into Docker registry
if: startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/pull/') || github.ref == 'refs/heads/main'
uses: docker/login-action@v1
- name: Checkout actions repo
uses: actions/checkout@v2
with:
registry: ${{ env.DOCKER_REGISTRY_URL }}
username: ${{ env.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
repository: Telecominfraproject/.github
path: github
- name: Push Docker images
if: startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/pull/') || github.ref == 'refs/heads/main'
run: |
docker images | grep ${{ env.DOCKER_REGISTRY_URL }}/owgw-ui | awk -F ' ' '{print $1":"$2}' | xargs -I {} docker push {}
- name: Build and push Docker image
uses: ./github/composite-actions/docker-image-build
with:
image_name: owgw-ui
registry: tip-tip-wlan-cloud-ucentral.jfrog.io
registry_user: ucentral
registry_password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Notify on failure via Slack
if: failure() && github.ref == 'refs/heads/main'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_USERNAME: GitHub Actions failure notifier
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_COLOR: "${{ job.status }}"
SLACK_ICON: https://raw.githubusercontent.com/quintessence/slack-icons/master/images/github-logo-slack-icon.png
SLACK_TITLE: Docker build failed for OWGW-UI service
trigger-deploy-to-dev:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs:
- docker
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
with:
repository: Telecominfraproject/.github
path: github
- name: Trigger deployment of the latest version to dev instance and wait for result
uses: ./github/composite-actions/trigger-workflow-and-wait
with:
owner: Telecominfraproject
repo: wlan-testing
workflow: ucentralgw-dev-deployment.yaml
token: ${{ secrets.WLAN_TESTING_PAT }}
ref: master
inputs: '{"force_latest": "true"}'

View File

@@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
- 'release/*'
types: [ closed ]
defaults:
@@ -16,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

@@ -0,0 +1,24 @@
name: Ensure Jira issue is linked
on:
pull_request:
types: [opened, edited, reopened, synchronize]
branches:
- 'release/*'
jobs:
check_for_issue_key:
runs-on: ubuntu-latest
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
with:
repository: Telecominfraproject/.github
path: github
- name: Run JIRA check
uses: ./github/composite-actions/enforce-jira-issue-key
with:
jira_base_url: ${{ secrets.TIP_JIRA_URL }}
jira_user_email: ${{ secrets.TIP_JIRA_USER_EMAIL }}
jira_api_token: ${{ secrets.TIP_JIRA_API_TOKEN }}

46
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Release chart package
on:
push:
tags:
- 'v*'
defaults:
run:
shell: bash
jobs:
helm-package:
runs-on: ubuntu-20.04
env:
HELM_REPO_URL: https://tip.jfrog.io/artifactory/tip-wlan-cloud-ucentral-helm/
HELM_REPO_USERNAME: ucentral
steps:
- name: Checkout uCentral assembly chart repo
uses: actions/checkout@v2
with:
path: wlan-cloud-ucentralgw-ui
- name: Build package
working-directory: wlan-cloud-ucentralgw-ui/helm
run: |
helm plugin install https://github.com/aslafy-z/helm-git --version 0.10.0
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm dependency update
mkdir dist
helm package . -d dist
- name: Generate GitHub release body
working-directory: wlan-cloud-ucentralgw-ui/helm
run: |
pip3 install yq -q
echo "Docker image - tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui:$GITHUB_REF_NAME" > release.txt
echo "Helm charted may be attached to this release" >> release.txt
echo "Deployment artifacts may be found in https://github.com/Telecominfraproject/wlan-cloud-ucentral-deploy/tree/$GITHUB_REF_NAME" >> release.txt
- name: Create GitHub release
uses: softprops/action-gh-release@v1
with:
body_path: wlan-cloud-ucentralgw-ui/helm/release.txt
files: wlan-cloud-ucentralgw-ui/helm/dist/*

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

@@ -20,7 +20,7 @@ Currently this chart is not assembled in charts archives, so [helm-git](https://
To install the chart with the release name `my-release`:
```bash
$ helm install --name my-release git+https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui@helm?ref=main
$ helm install --name my-release git+https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui@helm/owgwui-0.1.0.tgz?ref=main
```
The command deploys the Web UI on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.

View File

@@ -30,3 +30,13 @@ Create chart name and version as used by the chart label.
{{- define "owgwui.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "owgwui.ingress.apiVersion" -}}
{{- if .Capabilities.APIVersions.Has "networking.k8s.io/v1" -}}
{{- print "networking.k8s.io/v1" -}}
{{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}}
{{- print "networking.k8s.io/v1beta1" -}}
{{- else -}}
{{- print "extensions/v1beta1" -}}
{{- end -}}
{{- end -}}

View File

@@ -11,6 +11,7 @@ metadata:
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "owgwui.name" . }}
@@ -26,6 +27,12 @@ spec:
{{- with .Values.services.owgwui.labels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.podAnnotations }}
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
spec:
containers:

View File

@@ -2,7 +2,7 @@
{{- range $ingress, $ingressValue := .Values.ingresses }}
{{- if $ingressValue.enabled }}
---
apiVersion: extensions/v1beta1
apiVersion: {{ include "owgwui.ingress.apiVersion" $root }}
kind: Ingress
metadata:
name: {{ include "owgwui.fullname" $root }}-{{ $ingress }}
@@ -36,11 +36,25 @@ spec:
paths:
{{- range $ingressValue.paths }}
- path: {{ .path }}
{{- if $root.Capabilities.APIVersions.Has "networking.k8s.io/v1" }}
pathType: {{ .pathType | default "ImplementationSpecific" }}
{{- end }}
backend:
{{- if $root.Capabilities.APIVersions.Has "networking.k8s.io/v1" }}
service:
name: {{ include "owgwui.fullname" $root }}-{{ .serviceName }}
port:
{{- if kindIs "string" .servicePort }}
name: {{ .servicePort }}
{{- else }}
number: {{ .servicePort }}
{{- end }}
{{- else }}
serviceName: {{ include "owgwui.fullname" $root }}-{{ .serviceName }}
servicePort: {{ .servicePort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -1,5 +1,6 @@
# System
replicaCount: 1
revisionHistoryLimit: 2
nameOverride: ""
fullnameOverride: ""
@@ -7,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:
@@ -48,6 +49,7 @@ ingresses:
- chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
serviceName: owgwui
servicePort: http
@@ -69,6 +71,8 @@ tolerations: []
affinity: {}
podAnnotations: {}
# Application
public_env_variables:
DEFAULT_UCENTRALSEC_URL: https://ucentral.dpaas.arilia.com:16001

10754
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.4.3",
"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",
@@ -17,6 +18,8 @@
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-apexcharts": "^1.3.9",
"react-country-flag": "^3.0.2",
"react-csv": "^2.2.2",
"react-dom": "^17.0.2",
"react-flow-renderer": "^9.6.6",
"react-i18next": "^11.11.0",
@@ -26,7 +29,7 @@
"react-tooltip": "^4.2.21",
"react-widgets": "^5.1.1",
"sass": "^1.35.1",
"ucentral-libs": "^1.0.37",
"ucentral-libs": "^1.0.61",
"uuid": "^8.3.2"
},
"main": "index.js",
@@ -82,7 +85,6 @@
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"mini-css-extract-plugin": "^1.6.1",
"node-sass": "^5.0.0",
"path": "^0.12.7",
"prettier": "^2.3.2",
"react-refresh": "^0.9.0",

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",
@@ -17,6 +18,7 @@
"blink": "LEDs Blinken",
"device_leds": "LEDs",
"execute_now": "Möchten Sie dieses Muster jetzt einstellen?",
"explanation": "Welches Muster möchten Sie auf diesem Gerät für 30 Sekunden einstellen?",
"pattern": "Wählen Sie das Muster, das Sie verwenden möchten:",
"set_leds": "LEDs einstellen",
"when_blink_leds": "Wann möchten Sie die LEDs blinken lassen?"
@@ -26,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}}"
@@ -37,13 +40,16 @@
"add_note": "Notiz hinzufügen",
"add_note_explanation": "Schreiben Sie unten Ihre neue Notiz und klicken Sie auf die Schaltfläche \"+\", wo Sie fertig sind",
"adding_ellipsis": "Hinzufügen ...",
"all": "Alles",
"are_you_sure": "Bist du sicher?",
"back_to_login": "Zurück zur Anmeldung",
"back_to_start": "Zurück zum Start",
"blacklist": "Schwarze Liste",
"by": "Durch",
"cancel": "Abbrechen",
"certificate": "Zertifikat",
"certificates": "Zertifikate",
"claim": "Anspruch",
"clear": "Löschen",
"close": "Schließen",
"code": "Code",
@@ -58,16 +64,19 @@
"confirm_stop_editing": "Möchten Sie die Bearbeitung wirklich beenden? Dadurch werden alle nicht gespeicherten Änderungen, die Sie vorgenommen haben, verworfen.",
"connected": "Verbindung wurde hergestellt",
"copied": "kopiert!",
"copied_to_clipboard": "In die Zwischenablage kopiert!",
"copy_to_clipboard": "In die Zwischenablage kopieren",
"create": "Erstellen",
"created": "Erstellt",
"created_by": "Erstellt von",
"creator": "Schöpfer",
"current": "Aktuell",
"custom_date": "Benutzerdefiniertes Datum",
"dashboard": "Instrumententafel",
"date": "Datum",
"day": "tag",
"days": "tage",
"default_map": "Standardkarte",
"delete": "Löschen",
"delete_device": "Gerät löschen",
"details": "Einzelheiten",
@@ -85,6 +94,7 @@
"dismiss": "entlassen",
"do_now": "Sofort",
"download": "Herunterladen",
"duplicate": "Duplikat",
"duration": "Dauer",
"edit": "Bearbeiten",
"edit_user": "Bearbeiten",
@@ -94,6 +104,7 @@
"error": "Fehler",
"error_adding_note": "Fehler beim Hinzufügen einer Notiz",
"error_code": "Fehlercode",
"errors": "Fehler",
"execute_now": "Möchten Sie diesen Befehl jetzt ausführen?",
"executed": "Ausgeführt",
"exit": "Ausgang",
@@ -110,6 +121,7 @@
"hours": "std",
"id": "ID",
"invalid_credentials": "Ungültiger Benutzername und / oder Passwort",
"invalid_date_explanation": "Ungültiges Datum, bitte verwenden Sie den Kalender, auf den Sie über die Schaltfläche rechts zugreifen können",
"invalid_file": "Die ausgewählte Datei war ungültig, bitte lesen Sie die Anweisungen und passen Sie Ihre Datei entsprechend an",
"invalid_password": "Dieses Passwort entspricht nicht den grundlegenden Passwortregeln. Bitte besuchen Sie unsere Seite Passwortrichtlinien, um mehr zu erfahren",
"invalid_pem": "Ihre PEM-Datei ist ungültig. Es sollte mit '-----BEGIN CERTIFICATE-----' ODER '-----BEGIN PRIVATE KEY-----' beginnen und mit '-----END CERTIFICATE--- enden. --' ODER '-----END PRIVATSCHLÜSSEL-----'",
@@ -138,21 +150,25 @@
"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",
"not_connected": "Nicht verbunden",
"of_connected": "% der Geräte",
"of_connected": "% der verbundenen Geräte",
"off": "Aus",
"on": "An",
"optional": "Wahlweise",
"overall_health": "Allgemeine Gesundheit",
"password_policy": "Kennwortrichtlinie",
"preferences": "Einstellungen",
"preview": "Vorschau",
"program": "Programm",
"reason": "Grund",
"recorded": "Verzeichnet",
"refresh": "Aktualisierung",
"refresh_device": "Gerät aktualisieren",
"remove_claim": "Anspruch entfernen",
"required": "Erforderlich",
"result": "Ergebnis",
"save": "Sparen",
@@ -170,12 +186,14 @@
"show_all": "Zeige alles",
"socket_connection_closed": "Verbindung geschlossen!",
"start": "Start",
"status": "Status",
"stop_editing": "Stoppen Sie die Bearbeitung",
"submit": "Absenden",
"submitted": "Eingereicht",
"success": "Erfolg",
"system": "System",
"table": "Tabelle",
"time_per_device": "Gerät/Sekunde",
"timestamp": "Zeit",
"to": "zu",
"type": "Art",
@@ -190,6 +208,8 @@
"uuid": "UUID",
"vendors": "Anbieter",
"view_more": "Mehr anzeigen",
"visibility": "Sichtweite",
"waiting_for_update": "Warten auf Aktualisierung",
"yes": "Ja"
},
"configuration": {
@@ -210,6 +230,8 @@
"creation_success": "Konfiguration erfolgreich erstellt!",
"currently_associated": "Aktuell zugeordnete Konfiguration: {{config}}",
"currently_selected_config": "Derzeit ausgewählte Konfiguration: {{config}}",
"default_configs": "Standardkonfigurationen",
"default_configurations": "Standardkonfigurationen",
"delete_config": "Konfiguration löschen",
"details": "Gerätedetails",
"device_password": "Passwort",
@@ -218,6 +240,7 @@
"devices_affected": "Von dieser Konfiguration betroffene Geräte:",
"edit_configuration": "Konfiguration bearbeiten",
"error_delete": "Fehler beim Versuch zu löschen: {{error}}",
"error_delete_blacklist": "Fehler beim Löschen aus der schwarzen Liste: {{error}}",
"error_fetching_config": "Fehler beim Abrufen der Konfiguration",
"error_trying_delete": "Fehler beim Versuch zu löschen: {{error}}",
"error_update": "Fehler: {{error}}",
@@ -261,6 +284,7 @@
"contact": {
"access_pin": "Zugangs-PIN",
"add_contact": "Kontakt hinzufügen",
"contact": "Kontakt",
"create_contact": "Kontakt erstellen",
"currently_selected_contact": "Aktuell ausgewählter Kontakt: {{contact}}",
"delete": "Kontakt löschen?",
@@ -300,12 +324,27 @@
"healthchecks_title": "Healthchecks löschen"
},
"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}}",
"health_explanation": "Zustand der angeschlossenen Geräte",
"memory_explanation": "Von angeschlossenen Geräten belegter Speicher",
"uptimes_explanation": "Zeit, zu der verbundene Geräte aktiv und verbunden waren"
"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!",
"success_edit_blacklist": "Blacklist erfolgreich bearbeitet!",
"success_removed_blacklist": "Gerät erfolgreich von Blacklist entfernt!",
"uptimes_explanation": "Anzahl der verbundenen Geräte basierend auf ihrer Betriebszeit"
},
"device_logs": {
"log": "Protokoll",
@@ -320,23 +359,32 @@
"add_success": "Entität erfolgreich erstellt!",
"assigned_inventory": "Zugewiesenes Inventar",
"cannot_delete": "Entitäten mit untergeordneten Elementen können nicht gelöscht werden. Löschen Sie die untergeordneten Elemente dieser Entität, um sie löschen zu können.",
"confirm_map_delete": "Möchten Sie die Karte {{name}}wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden",
"currently_selected_entity": "Derzeit ausgewähltes Unternehmen: {{config}}",
"currently_selected_venue": "Aktuell ausgewählter Veranstaltungsort: {{config}}",
"delete_success": "Entität erfolgreich gelöscht",
"delete_warning": "Achtung: Dieser Vorgang kann nicht rückgängig gemacht werden",
"duplicate_from_node": "Mit einem bestimmten Root-Knoten duplizieren",
"duplicate_map": "Karte duplizieren",
"duplicate_with_node": "Dupliziere {{mapName}} mit {{rootName}} als Root-Knoten",
"edit_failure": "Aktualisierung fehlgeschlagen : {{error}}",
"enter_here": "Geben Sie hier die IP(s) ein, die Sie hinzufügen möchten",
"entire_tree": "Seitenverzeichnis",
"entire_tree": "Netzwerkkarte",
"entities": "Entitäten",
"entity": "Entität",
"error_deleting_map": "Fehler beim Löschen der Karte: {{error}}",
"error_fetch_entity": "Fehler beim Abrufen von Entitätsinformationen",
"error_fetching": "Fehler beim Abrufen von Entitäten",
"error_fetching_map": "Fehler beim Abrufen der Karte: {{error}}",
"error_fetching_tree": "Fehler beim Abrufen des Baums: {{error}}",
"error_saving": "Fehler beim Speichern der Entität",
"error_saving_map": "Fehler beim Speichern der Karte: {{error}}",
"higher_priority": "Stellen Sie eine höhere Priorität ein",
"ip_detection": "IP-Erkennung",
"ip_formats": "Sie können IPv4- oder IPv6-Adressen in den folgenden Formaten hinzufügen:",
"lower_priority": "Niedrigere Priorität setzen",
"map": "Karte",
"map_delete_success": "Karte erfolgreich gelöscht!",
"need_select_entity": "sSie müssen eine Entität aus der folgenden Tabelle auswählen",
"no_ips": "Keine IPs ausgewählt",
"not_assigned": "Nicht zugeordnet",
@@ -344,6 +392,7 @@
"select_entity": "Wählen Sie diese Entität aus",
"selected_entity": "Ausgewählte Einheit",
"selected_map": "Ausgewählte Karte",
"tree_saved": "Karte erfolgreich gespeichert!",
"update_failure_error": "Fehler beim Versuch, die Entität zu aktualisieren: {{error}}",
"valid_serial": "Muss eine gültige Seriennummer sein (12 HEX-Zeichen)",
"venues": "Veranstaltungsorte"
@@ -382,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..."
@@ -541,6 +590,9 @@
"verification_code": "Geben Sie hier Ihre Bestätigung ein",
"wrong_code": "Der eingegebene Bestätigungscode ist ungültig."
},
"preferences": {
"provisioning": "Bereitstellung"
},
"reboot": {
"directions": "Wann möchten Sie dieses Gerät neu starten?",
"now": "Möchten Sie dieses Gerät jetzt neu starten?",
@@ -551,7 +603,7 @@
"channel": "Kanal",
"directions": "Starten Sie einen WiFi-Scan dieses Geräts, der ungefähr 25 Sekunden dauern sollte.",
"re_scan": "Erneut scannen",
"result_directions": "Bitte klicken Sie auf die Schaltfläche '$t(scan.re_scan)', wenn Sie einen Scan mit derselben Konfiguration wie beim letzten Scan durchführen möchten.",
"result_directions": "Sie können oben rechts auf die Schaltfläche „Scannen“ klicken, um $t(scan.re_scan)",
"results": "Ergebnisse des WiFi-Scans",
"scan": "Scan",
"scanning": "Scannen... ",
@@ -584,7 +636,7 @@
"mac_prefix": "MAC-Präfix",
"max_associations": "max. Verbände",
"max_clients": "Max. Kunden",
"messages_transmitted": "Gesendete Nachrichten",
"messages_transmitted": "Nachricht TX",
"min_associations": "Mindest. Verbände",
"min_clients": "Mindest. Kunden",
"pause": "Pause",
@@ -592,7 +644,7 @@
"prefix_length": "Erforderlich, muss eine Länge von 6 Zeichen haben",
"previous_runs": "Vorherige Läufe",
"received": "empfangen",
"received_messages": "Erhaltene Nachrichten",
"received_messages": "Nachricht RX",
"reconnect_interval": "Wiederverbindungsintervall",
"resume": "Fortsetzen",
"resume_success": "Lauf wieder aufgenommen!",
@@ -615,11 +667,14 @@
},
"statistics": {
"data": "Daten (KB)",
"data_mb": "Daten (MB)",
"latest_statistics": "Neueste Statistiken",
"lifetime_stats": "Lifetime-Statistik",
"memory": "Erinnerung",
"no_interfaces": "Keine Statistiken zur Schnittstellenlebensdauer verfügbar",
"show_latest": "Letzte Statistik",
"title": "Statistiken"
"title": "Statistiken",
"used": "Verwendeter Speicher %"
},
"status": {
"connection_status": "Status",
@@ -631,9 +686,27 @@
"percentage_free": "{{percentage}}% von {{total}} kostenlos",
"percentage_used": "{{percentage}}% von {{total}} verwendet",
"title": "#{{serialNumber}} Status",
"total_memory": "Gesamtspeicher",
"uptime": "Betriebszeit",
"used_total_memory": "{{used}} verwendet / {{total}} insgesamt"
},
"subscriber": {
"add_device_subscriber_explanation": "Um andere Geräte zu reklamieren, kannst du unsere Suchleiste verwenden oder direkt aus der Tabelle reklamieren. Wenn ein Gerät bereits von einem Benutzer beansprucht wurde, müssen Sie zu dessen Details gehen und die Zuweisung aufheben, bevor Sie es beanspruchen.",
"create": "Abonnenten erstellen",
"devices_one": "{{count}} Gerät",
"devices_other": "{{count}} Geräte",
"edit": "Abonnent bearbeiten",
"error_create": "Fehler beim Erstellen des Abonnenten: {{error}}",
"error_delete": "Fehler beim Löschen des Abonnenten: {{error}}",
"error_fetching": "Fehler beim Abrufen von Abonnenten: {{error}}",
"error_fetching_single": "Fehler beim Abrufen des Abonnenten: {{error}}",
"error_update": "Fehler beim Aktualisieren des Abonnenten: {{error}}",
"is_already_claimed": "wird bereits beansprucht von",
"subscribers": "Abonnenten",
"success_create": "Abonnent erfolgreich erstellt!",
"success_delete": "Abonnent erfolgreich gelöscht!",
"success_update": "Abonnent erfolgreich aktualisiert!"
},
"system": {
"error_fetching": "Fehler beim Abrufen von Systeminformationen",
"error_reloading": "Fehler beim Neuladen: {{error}}",
@@ -649,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": {
@@ -659,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"
},
@@ -723,6 +798,7 @@
"send_code_again": "Code nochmal senden",
"show_hide_password": "Passwort anzeigen/verbergen",
"successful_validation": "Telefonnummer bestätigt! Klicken Sie auf die Schaltfläche Speichern, um es mit Ihrem Profil zu verknüpfen",
"table_title": "Admin-Benutzer",
"update_failure": "Fehler beim Aktualisieren: {{error}}",
"update_failure_title": "Update fehlgeschlagen",
"update_success": "Benutzer erfolgreich aktualisiert",
@@ -738,7 +814,11 @@
"associations": "Verbände",
"mode": "Modus",
"network_diagram": "Netzwerkdiagramm",
"override_dfs": "DFS überschreiben",
"radios": "Radios",
"title": "WLAN-Analyse"
"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",
"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",
@@ -17,6 +18,7 @@
"blink": "Blink",
"device_leds": "Device LEDs",
"execute_now": "Would you like to set this pattern now?",
"explanation": "What pattern would you like to set on this device for 30 seconds?",
"pattern": "LEDs pattern: ",
"set_leds": "Set LEDs",
"when_blink_leds": "When would you like to make the device LEDs blink?"
@@ -26,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}}"
@@ -37,13 +40,16 @@
"add_note": "Add Note",
"add_note_explanation": "Write your new note below and click the '+' button where you are done",
"adding_ellipsis": "Adding...",
"all": "All",
"are_you_sure": "Are you sure?",
"back_to_login": "Back to Login",
"back_to_start": "Back to start",
"blacklist": "Blacklist",
"by": "By",
"cancel": "Cancel",
"certificate": "Certificate",
"certificates": "Certificates",
"claim": "Claim",
"clear": "Clear",
"close": "Close",
"code": "Code",
@@ -58,16 +64,19 @@
"confirm_stop_editing": "Are you sure you want to stop editing? This will cancel any unsaved changes you have made.",
"connected": "Connected",
"copied": "Copied!",
"copied_to_clipboard": "Copied to Clipboard!",
"copy_to_clipboard": "Copy to clipboard",
"create": "Create",
"created": "Created",
"created_by": "Created By",
"creator": "Creator",
"current": "Current ",
"custom_date": "Custom Date",
"dashboard": "Dashboard",
"date": "Date",
"day": "day",
"days": "days",
"default_map": "Default Map",
"delete": "Delete",
"delete_device": "Delete Device",
"details": "Details",
@@ -85,6 +94,7 @@
"dismiss": "Dismiss",
"do_now": "Do Now!",
"download": "Download",
"duplicate": "Duplicate",
"duration": "Duration",
"edit": "Edit",
"edit_user": "Edit",
@@ -94,6 +104,7 @@
"error": "Error",
"error_adding_note": "Error while adding note",
"error_code": "Error Code",
"errors": "Errors",
"execute_now": "Would you like to execute this command now?",
"executed": "Executed",
"exit": "Exit",
@@ -110,6 +121,7 @@
"hours": "hours",
"id": "Id",
"invalid_credentials": "Invalid username and/or password",
"invalid_date_explanation": "Invalid Date, please use the calendar accessible with the button on the right ",
"invalid_file": "The chosen file was invalid, please read the instructions and adjust your file accordingly",
"invalid_password": "This password does not confirm to basic password rules. Please visit our Password Policy page to learn more",
"invalid_pem": "Your .pem file is invalid. It should start with '-----BEGIN CERTIFICATE-----' OR '-----BEGIN PRIVATE KEY-----' and it should end with '-----END CERTIFICATE-----' OR '-----END PRIVATE KEY-----'",
@@ -138,21 +150,25 @@
"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",
"not_connected": "Not Connected",
"of_connected": "% of devices",
"of_connected": "% of connected devices",
"off": "Off",
"on": "On",
"optional": "Optional",
"overall_health": "Overall Health",
"password_policy": "Password Policy",
"preferences": "Preferences",
"preview": "Preview",
"program": "Program",
"reason": "Reason",
"recorded": "Recorded",
"refresh": "Refresh",
"refresh_device": "Refresh Device",
"remove_claim": "Remove Claim",
"required": "Required",
"result": "Result",
"save": "Save",
@@ -170,12 +186,14 @@
"show_all": "Show All",
"socket_connection_closed": "Connection closed!",
"start": "Start",
"status": "Status",
"stop_editing": "Stop Editing",
"submit": "Submit",
"submitted": "Submitted",
"success": "Success",
"system": "System",
"table": "Table",
"time_per_device": "Devices/Second",
"timestamp": "Time",
"to": "To",
"type": "Type",
@@ -190,6 +208,8 @@
"uuid": "UUID",
"vendors": "Vendors",
"view_more": "View more",
"visibility": "Visibility",
"waiting_for_update": "Waiting for Update",
"yes": "Yes"
},
"configuration": {
@@ -210,6 +230,8 @@
"creation_success": "Configuration successfully created!",
"currently_associated": "Currently Associated Configuration: {{config}}",
"currently_selected_config": "Currently Selected Configuration: {{config}}",
"default_configs": "Default Configs",
"default_configurations": "Default Configurations",
"delete_config": "Delete Config",
"details": "Details",
"device_password": "Password",
@@ -218,6 +240,7 @@
"devices_affected": "Devices affected by this configuration: ",
"edit_configuration": "Edit Configuration",
"error_delete": "Error while trying to delete: {{error}}",
"error_delete_blacklist": "Error deleting from blacklist: {{error}}",
"error_fetching_config": "Error while fetching configuration",
"error_trying_delete": "Error while trying to delete: {{error}}",
"error_update": "Error: {{error}}",
@@ -261,6 +284,7 @@
"contact": {
"access_pin": "Access PIN",
"add_contact": "Add Contact",
"contact": "Contact",
"create_contact": "Create Contact",
"currently_selected_contact": "Currently Selected Contact: {{contact}}",
"delete": "Delete Contact?",
@@ -300,12 +324,27 @@
"healthchecks_title": "Delete Healthchecks"
},
"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}}",
"health_explanation": "Health of connected devices",
"memory_explanation": "Memory used by connected devices",
"uptimes_explanation": "Time connected devices have been up and connected"
"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!",
"success_edit_blacklist": "Successfully edited blacklist!",
"success_removed_blacklist": "Successfully removed device from blacklist!",
"uptimes_explanation": "Amount of devices connected based on their uptime"
},
"device_logs": {
"log": "Log",
@@ -320,23 +359,32 @@
"add_success": "Entity Successfully Created!",
"assigned_inventory": "Assigned Inventory",
"cannot_delete": "You cannot delete entities which have children. Delete this entity's children to be able to delete it.",
"confirm_map_delete": "Are you sure you want to delete the map {{name}}? This action cannot be reverted",
"currently_selected_entity": "Currently Selected Entity: {{config}}",
"currently_selected_venue": "Currently Selected Venue: {{config}}",
"delete_success": "Entity Successfully Deleted",
"delete_warning": "Warning: this operation cannot be reverted",
"duplicate_from_node": "Duplicate with specific Root Node",
"duplicate_map": "Duplicate Map",
"duplicate_with_node": "Duplicate {{mapName}} with {{rootName}} as root node",
"edit_failure": "Update unsuccessful : {{error}}",
"enter_here": "Enter the IP(s) you'd like to add here",
"entire_tree": "Site Map",
"entire_tree": "Network Map",
"entities": "Entities",
"entity": "Entity",
"error_deleting_map": "Error deleting map: {{error}}",
"error_fetch_entity": "Error while fetching entity information",
"error_fetching": "Error while fetching entities",
"error_fetching_map": "Error fetching map: {{error}}",
"error_fetching_tree": "Error while fetching tree: {{error}}",
"error_saving": "Error while saving entity",
"error_saving_map": "Error saving map: {{error}}",
"higher_priority": "Make Higher Priority",
"ip_detection": "IP Detection",
"ip_formats": "You can add IPv4 or IPv6 addresses in the following formats:",
"lower_priority": "Make Lower Priority",
"map": "Map",
"map_delete_success": "Map Successfully Deleted!",
"need_select_entity": "You need to select an entity from the table below",
"no_ips": "No IPs selected",
"not_assigned": "Not Assigned",
@@ -344,6 +392,7 @@
"select_entity": "Select this Entity",
"selected_entity": "Selected Entity",
"selected_map": "Selected Map",
"tree_saved": "Map Successfully Saved!",
"update_failure_error": "Error while trying to update entity: {{error}}",
"valid_serial": "Needs to be a valid serial number (12 HEX characters)",
"venues": "Venues"
@@ -382,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..."
@@ -541,6 +590,9 @@
"verification_code": "Enter your verification here",
"wrong_code": "The verification code that was entered is not valid. "
},
"preferences": {
"provisioning": "Provisioning"
},
"reboot": {
"directions": "When would you like to reboot this device?",
"now": "Would you like to reboot this device now?",
@@ -551,7 +603,7 @@
"channel": "Channel",
"directions": "Launch a wifi scan of this device, which should take approximately 25 seconds.",
"re_scan": "Re-Scan",
"result_directions": "Please click the '$t(scan.re_scan)' button if you would like to do a scan with the same configuration as the last.",
"result_directions": "You can click the 'Scan' button at the top right to $t(scan.re_scan)",
"results": "Wi-Fi Scan Results",
"scan": "Scan",
"scanning": "Scanning... ",
@@ -584,7 +636,7 @@
"mac_prefix": "MAC Prefix",
"max_associations": "Max. Associations",
"max_clients": "Max. Clients",
"messages_transmitted": "Messages Transmitted",
"messages_transmitted": "Msgs TX",
"min_associations": "Min. Associations",
"min_clients": "Min. Clients",
"pause": "Pause",
@@ -592,7 +644,7 @@
"prefix_length": "Required, needs to be of a length of 6 characters",
"previous_runs": "Previous Runs",
"received": "Received",
"received_messages": "Messages Received",
"received_messages": "Msgs RX",
"reconnect_interval": "Reconnect Interval",
"resume": "Resume",
"resume_success": "Run Resumed!",
@@ -615,11 +667,14 @@
},
"statistics": {
"data": "Data (KB)",
"data_mb": "Data (MB)",
"latest_statistics": "Latest Statistics",
"lifetime_stats": "Lifetime Statistics",
"memory": "Memory",
"no_interfaces": "No interface lifetime statistics available",
"show_latest": "Last Statistics",
"title": "Statistics"
"title": "Statistics",
"used": "Used Memory %"
},
"status": {
"connection_status": "Status",
@@ -631,9 +686,27 @@
"percentage_free": "{{percentage}}% of {{total}} free",
"percentage_used": "{{percentage}}% of {{total}} used",
"title": "#{{serialNumber}} Status",
"total_memory": "Total Memory",
"uptime": "Uptime",
"used_total_memory": "{{used}} used / {{total}} total "
},
"subscriber": {
"add_device_subscriber_explanation": "To claim devices, you can use our search bar or claim directly from the table. If a device was already claimed by a user, you will need to go to to their details and unassign it before claiming it.",
"create": "Create Subscriber",
"devices_one": "{{count}} Device",
"devices_other": "{{count}} Devices",
"edit": "Edit Subscriber",
"error_create": "Error creating subscriber: {{error}}",
"error_delete": "Error deleting subscriber: {{error}}",
"error_fetching": "Error fetching subscribers: {{error}}",
"error_fetching_single": "Error fetching subscriber: {{error}}",
"error_update": "Error updating subscriber: {{error}}",
"is_already_claimed": "is already claimed by ",
"subscribers": "Subscribers",
"success_create": "Subscriber successfully created!",
"success_delete": "Subscriber successfully deleted!",
"success_update": "Successfully updated subscriber!"
},
"system": {
"error_fetching": "Error while fetching system information",
"error_reloading": "Error while reloading: {{error}}",
@@ -649,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": {
@@ -659,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"
},
@@ -723,6 +798,7 @@
"send_code_again": "Send Code Again",
"show_hide_password": "Show/Hide Password",
"successful_validation": "Phone Number Validated! Click the save button to link it to your profile",
"table_title": "Admin Users",
"update_failure": "Error while trying to update: {{error}}",
"update_failure_title": "Update Failed",
"update_success": "User Updated Successfully",
@@ -738,7 +814,11 @@
"associations": "Associations",
"mode": "Mode",
"network_diagram": "Network Diagram",
"override_dfs": "Override DFS",
"radios": "Radios",
"title": "Wi-Fi Analysis"
"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",
"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",
@@ -17,6 +18,7 @@
"blink": "Parpadeo",
"device_leds": "LED de dispositivo",
"execute_now": "¿Le gustaría establecer este patrón ahora?",
"explanation": "¿Qué patrón le gustaría establecer en este dispositivo durante 30 segundos?",
"pattern": "Elija el patrón que le gustaría usar:",
"set_leds": "Establecer LED",
"when_blink_leds": "¿Cuándo desea que los LED del dispositivo parpadeen?"
@@ -26,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}}"
@@ -37,13 +40,16 @@
"add_note": "Añadir la nota",
"add_note_explanation": "Escriba su nueva nota a continuación y haga clic en el botón '+' donde haya terminado",
"adding_ellipsis": "Añadiendo ...",
"all": "TODOS",
"are_you_sure": "¿Estás seguro?",
"back_to_login": "Atrás para iniciar sesión",
"back_to_start": "volver a empezar",
"blacklist": "Lista negra",
"by": "Por",
"cancel": "Cancelar",
"certificate": "Certificado",
"certificates": "Certificados",
"claim": "Reclamación",
"clear": "Claro",
"close": "Cerrar",
"code": "Código",
@@ -58,16 +64,19 @@
"confirm_stop_editing": "¿Estás seguro de que quieres dejar de editar? Esto cancelará cualquier cambio no guardado que haya realizado.",
"connected": "Conectado",
"copied": "Copiado!",
"copied_to_clipboard": "¡Copiado al portapapeles!",
"copy_to_clipboard": "Copiar al portapapeles",
"create": "Crear",
"created": "creado",
"created_by": "Creado por",
"creator": "Creador",
"current": "Corriente",
"custom_date": "Fecha personalizada",
"dashboard": "Tablero",
"date": "Fecha",
"day": "día",
"days": "días",
"default_map": "Mapa predeterminado",
"delete": "Borrar",
"delete_device": "Eliminar dispositivo",
"details": "Detalles",
@@ -85,6 +94,7 @@
"dismiss": "Despedir",
"do_now": "¡Hagan ahora!",
"download": "Descargar",
"duplicate": "Duplicar",
"duration": "Duración",
"edit": "Editar",
"edit_user": "Editar",
@@ -94,6 +104,7 @@
"error": "Error",
"error_adding_note": "Error al agregar una nota",
"error_code": "código de error",
"errors": "Los errores",
"execute_now": "¿Le gustaría ejecutar este comando ahora?",
"executed": "ejecutado",
"exit": "salida",
@@ -110,6 +121,7 @@
"hours": "horas",
"id": "Carné de identidad",
"invalid_credentials": "Nombre de usuario y / o contraseña inválido",
"invalid_date_explanation": "Fecha no válida, utilice el calendario accesible con el botón de la derecha",
"invalid_file": "El archivo elegido no es válido, lea las instrucciones y ajuste su archivo en consecuencia",
"invalid_password": "Esta contraseña no confirma las reglas básicas de contraseña. Visite nuestra página de Política de contraseñas para obtener más información.",
"invalid_pem": "Su archivo .pem no es válido. Debe comenzar con '----- BEGIN CERTIFICATE -----' O '----- BEGIN PRIVATE KEY -----' y debe terminar con '----- END CERTIFICATE --- - 'O' ----- FIN DE CLAVE PRIVADA ----- '",
@@ -138,21 +150,25 @@
"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",
"not_connected": "No conectado",
"of_connected": "% de dispositivos",
"of_connected": "% de dispositivos conectados",
"off": "Apagado",
"on": "en",
"optional": "Opcional",
"overall_health": "Salud en general",
"password_policy": "Política de contraseñas",
"preferences": "Preferencias",
"preview": "Avance",
"program": "Programa",
"reason": "Razón",
"recorded": "Grabado",
"refresh": "Refrescar",
"refresh_device": "Actualizar dispositivo",
"remove_claim": "Quitar reclamo",
"required": "Necesario",
"result": "Resultado",
"save": "Salvar",
@@ -170,12 +186,14 @@
"show_all": "Mostrar todo",
"socket_connection_closed": "¡Conexión cerrada!",
"start": "comienzo",
"status": "Estado",
"stop_editing": "Dejar de editar",
"submit": "Enviar",
"submitted": "Presentado",
"success": "Éxito",
"system": "Sistema",
"table": "Mesa",
"time_per_device": "Dispositivo / segundo",
"timestamp": "hora",
"to": "a",
"type": "Tipo",
@@ -190,6 +208,8 @@
"uuid": "UUID",
"vendors": "Vendedores",
"view_more": "Ver más",
"visibility": "Visibilidad",
"waiting_for_update": "Esperando actualización",
"yes": "Sí"
},
"configuration": {
@@ -210,6 +230,8 @@
"creation_success": "¡Configuración creada con éxito!",
"currently_associated": "Configuración asociada actual: {{config}}",
"currently_selected_config": "Configuración seleccionada actualmente: {{config}}",
"default_configs": "Configuraciones predeterminadas",
"default_configurations": "Configuraciones predeterminadas",
"delete_config": "Eliminar Configuración",
"details": "Detalles",
"device_password": "Contraseña",
@@ -218,6 +240,7 @@
"devices_affected": "Dispositivos afectados por esta configuración:",
"edit_configuration": "Editar configuración",
"error_delete": "Error al intentar eliminar: {{error}}",
"error_delete_blacklist": "Error al eliminar de la lista negra: {{error}}",
"error_fetching_config": "Error al obtener la configuración",
"error_trying_delete": "Error al intentar eliminar: {{error}}",
"error_update": "Error: {{error}}",
@@ -261,6 +284,7 @@
"contact": {
"access_pin": "PIN de acceso",
"add_contact": "Agregar contacto",
"contact": "Contacto",
"create_contact": "Crear contacto",
"currently_selected_contact": "Contacto seleccionado actualmente: {{contact}}",
"delete": "¿Borrar contacto?",
@@ -300,12 +324,27 @@
"healthchecks_title": "Eliminar comprobaciones de estado"
},
"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}}",
"health_explanation": "Salud de los dispositivos conectados",
"memory_explanation": "Memoria utilizada por dispositivos conectados",
"uptimes_explanation": "Tiempo que los dispositivos conectados han estado en funcionamiento y conectados"
"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!",
"success_edit_blacklist": "Lista negra editada con éxito!",
"success_removed_blacklist": "¡Dispositivo eliminado con éxito de la lista negra!",
"uptimes_explanation": "Cantidad de dispositivos conectados según su tiempo de actividad"
},
"device_logs": {
"log": "Iniciar sesión",
@@ -320,23 +359,32 @@
"add_success": "¡Entidad creada con éxito!",
"assigned_inventory": "Inventario asignado",
"cannot_delete": "No puede eliminar entidades que tienen hijos. Elimina los hijos de esta entidad para poder eliminarla.",
"confirm_map_delete": "¿Está seguro de que desea eliminar el mapa {{name}}? Esta acción no se puede revertir",
"currently_selected_entity": "Entidad seleccionada actualmente: {{config}}",
"currently_selected_venue": "Lugar seleccionado actualmente: {{config}}",
"delete_success": "Entidad eliminada correctamente",
"delete_warning": "Advertencia: esta operación no se puede revertir",
"duplicate_from_node": "Duplicar con un nodo raíz específico",
"duplicate_map": "Mapa duplicado",
"duplicate_with_node": "Duplicar {{mapName}} con {{rootName}} como nodo raíz",
"edit_failure": "Actualización fallida: {{error}}",
"enter_here": "Ingrese las IP que desea agregar aquí",
"entire_tree": "Site MAp",
"entire_tree": "Mapa de red",
"entities": "entidades",
"entity": "Entidad",
"error_deleting_map": "Error al eliminar el mapa: {{error}}",
"error_fetch_entity": "Error al obtener la información de la entidad",
"error_fetching": "Error al recuperar entidades",
"error_fetching_map": "Error al obtener el mapa: {{error}}",
"error_fetching_tree": "Error al obtener el árbol: {{error}}",
"error_saving": "Error al guardar la entidad",
"error_saving_map": "Error al guardar el mapa: {{error}}",
"higher_priority": "Dar mayor prioridad",
"ip_detection": "Detección de IP",
"ip_formats": "Puede agregar direcciones IPv4 o IPv6 en los siguientes formatos:",
"lower_priority": "Hacer una prioridad más baja",
"map": "Mapa",
"map_delete_success": "¡Mapa eliminado correctamente!",
"need_select_entity": "Debe seleccionar una entidad de la siguiente tabla",
"no_ips": "No se seleccionaron direcciones IP",
"not_assigned": "No asignado",
@@ -344,6 +392,7 @@
"select_entity": "Seleccione esta entidad",
"selected_entity": "Entidad seleccionada",
"selected_map": "Mapa seleccionado",
"tree_saved": "¡Mapa guardado con éxito!",
"update_failure_error": "Error al intentar actualizar la entidad: {{error}}",
"valid_serial": "Debe ser un número de serie válido (12 caracteres HEX)",
"venues": "Sedes"
@@ -382,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 ..."
@@ -541,6 +590,9 @@
"verification_code": "Ingrese su verificación aquí",
"wrong_code": "El código de verificación que se ingresó no es válido."
},
"preferences": {
"provisioning": "Aprovisionamiento"
},
"reboot": {
"directions": "¿Cuándo le gustaría reiniciar este dispositivo?",
"now": "¿Le gustaría reiniciar este dispositivo ahora?",
@@ -551,7 +603,7 @@
"channel": "Canal",
"directions": "Ejecute un escaneo wifi de este dispositivo, que debería tomar aproximadamente 25 segundos.",
"re_scan": "Vuelva a escanear",
"result_directions": "Haga clic en el botón '$ t (scan.re_scan)' si desea realizar un escaneo con la misma configuración que el anterior.",
"result_directions": "Puede hacer clic en el botón 'Escanear' en la parte superior derecha para $t(scan.re_scan)",
"results": "Resultados de escaneo Wi-Fi",
"scan": "Escanear",
"scanning": "Exploración... ",
@@ -584,7 +636,7 @@
"mac_prefix": "Prefijo MAC",
"max_associations": "Max. Asociaciones",
"max_clients": "Max. Clientela",
"messages_transmitted": "Mensajes transmitidos",
"messages_transmitted": "Mensajes TX",
"min_associations": "Min. Asociaciones",
"min_clients": "Min. Clientela",
"pause": "pausa",
@@ -592,7 +644,7 @@
"prefix_length": "Obligatorio, debe tener una longitud de 6 caracteres",
"previous_runs": "Ejecuciones anteriores",
"received": "recibido",
"received_messages": "Mensajes recibidos",
"received_messages": "Msgs RX",
"reconnect_interval": "Intervalo de reconexión",
"resume": "Currículum",
"resume_success": "¡Ejecutar reanudado!",
@@ -615,11 +667,14 @@
},
"statistics": {
"data": "Datos (KB)",
"data_mb": "Datos (MB)",
"latest_statistics": "Últimas estadísticas",
"lifetime_stats": "Estadísticas de por vida",
"memory": "Memoria",
"no_interfaces": "No hay estadísticas de vida útil de la interfaz disponibles",
"show_latest": "Últimas estadísticas",
"title": "estadística"
"title": "estadística",
"used": "Memoria usada %"
},
"status": {
"connection_status": "Estado",
@@ -631,9 +686,27 @@
"percentage_free": "{{percentage}}% de {{total}} gratis",
"percentage_used": "{{percentage}}% de {{total}} utilizado",
"title": "#{{serialNumber}} Estado",
"total_memory": "Memoria total",
"uptime": "Tiempo de actividad",
"used_total_memory": "{{used}} usado / {{total}} total"
},
"subscriber": {
"add_device_subscriber_explanation": "Para reclamar otros dispositivos, puede usar nuestra barra de búsqueda o reclamar directamente desde la tabla. Si un dispositivo ya fue reclamado por un usuario, deberá ir a sus detalles y anular la asignación antes de reclamarlo.",
"create": "Crear suscriptor",
"devices_one": "{{count}} dispositivo",
"devices_other": "{{count}} dispositivos",
"edit": "Editar suscriptor",
"error_create": "Error al crear el suscriptor: {{error}}",
"error_delete": "Error al eliminar el suscriptor: {{error}}",
"error_fetching": "Error al obtener suscriptores: {{error}}",
"error_fetching_single": "Error al obtener el suscriptor: {{error}}",
"error_update": "Error al actualizar el suscriptor: {{error}}",
"is_already_claimed": "ya es reclamado por",
"subscribers": "Suscriptores",
"success_create": "¡Suscriptor creado correctamente!",
"success_delete": "¡Suscriptor eliminado correctamente!",
"success_update": "Suscriptor actualizado con éxito!"
},
"system": {
"error_fetching": "Error al obtener información del sistema",
"error_reloading": "Error al recargar: {{error}}",
@@ -649,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": {
@@ -659,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"
},
@@ -723,6 +798,7 @@
"send_code_again": "Enviar Código De nuevo",
"show_hide_password": "Mostrar / Ocultar contraseña",
"successful_validation": "¡Número de teléfono validado! Haga clic en el botón guardar para vincularlo a su perfil",
"table_title": "Usuarios administrativos",
"update_failure": "Error al intentar actualizar: {{error}}",
"update_failure_title": "Actualización fallida",
"update_success": "Usuario actualizado con éxito",
@@ -738,7 +814,11 @@
"associations": "Asociaciones",
"mode": "Modo",
"network_diagram": "Diagrama de Red",
"override_dfs": "Anular DFS",
"radios": "Radios",
"title": "Análisis de Wi-Fi"
"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",
"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",
@@ -17,6 +18,7 @@
"blink": "Cligner",
"device_leds": "LED de l'appareil",
"execute_now": "Souhaitez-vous définir ce modèle maintenant ?",
"explanation": "Quel modèle souhaitez-vous définir sur cet appareil pendant 30 secondes ?",
"pattern": "Choisissez le modèle que vous souhaitez utiliser :",
"set_leds": "Définir les LED",
"when_blink_leds": "Quand souhaitez-vous faire clignoter les LED de l'appareil ?"
@@ -26,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}}"
@@ -37,13 +40,16 @@
"add_note": "Ajouter une note",
"add_note_explanation": "Écrivez votre nouvelle note ci-dessous et cliquez sur le bouton '+' où vous avez terminé",
"adding_ellipsis": "Ajouter...",
"all": "Tout",
"are_you_sure": "Êtes-vous sûr?",
"back_to_login": "Retour connexion",
"back_to_start": "Retour au début",
"blacklist": "Liste noire",
"by": "Par",
"cancel": "annuler",
"certificate": "Certificat",
"certificates": "Certificats",
"claim": "Prétendre",
"clear": "Clair",
"close": "Fermer",
"code": "Code",
@@ -58,16 +64,19 @@
"confirm_stop_editing": "Voulez-vous vraiment arrêter la modification ? Cela annulera toutes les modifications non enregistrées que vous avez apportées.",
"connected": "Connecté",
"copied": "Copié!",
"copied_to_clipboard": "Copié dans le presse-papier!",
"copy_to_clipboard": "Copier dans le presse-papier",
"create": "Créer",
"created": "Créé",
"created_by": "Créé par",
"creator": "Créateur",
"current": "Actuel",
"custom_date": "Date personnalisée",
"dashboard": "Tableau de bord",
"date": "Rendez-vous amoureux",
"day": "journée",
"days": "journées",
"default_map": "Carte par défaut",
"delete": "Effacer",
"delete_device": "Supprimer le périphérique",
"details": "Détails",
@@ -85,6 +94,7 @@
"dismiss": "Rejeter",
"do_now": "Faire maintenant!",
"download": "Télécharger",
"duplicate": "Dupliquer",
"duration": "Durée",
"edit": "modifier",
"edit_user": "Modifier",
@@ -94,6 +104,7 @@
"error": "Erreur",
"error_adding_note": "Erreur lors de l'ajout de la note",
"error_code": "Code d'erreur",
"errors": "les erreurs",
"execute_now": "Souhaitez-vous exécuter cette commande maintenant ?",
"executed": "réalisé",
"exit": "Sortie",
@@ -110,6 +121,7 @@
"hours": "heures",
"id": "Id",
"invalid_credentials": "Nom d'utilisateur et / ou mot de passe incorrect",
"invalid_date_explanation": "Date invalide, merci d'utiliser le calendrier accessible avec le bouton à droite",
"invalid_file": "Le fichier choisi n'était pas valide, veuillez lire les instructions et ajuster votre fichier en conséquence",
"invalid_password": "Ce mot de passe ne confirme pas les règles de base des mots de passe. Veuillez visiter notre page Politique de mot de passe pour en savoir plus",
"invalid_pem": "Votre fichier .pem n'est pas valide. Il doit commencer par '-----BEGIN CERTIFICATE-----' OU '-----BEGIN PRIVATE KEY-----' et il doit se terminer par '-----END CERTIFICATE--- --' OU '-----FIN CLÉ PRIVÉE-----'",
@@ -138,21 +150,25 @@
"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",
"not_connected": "Pas connecté",
"of_connected": "% d'appareils",
"of_connected": "% d'appareils connectés",
"off": "De",
"on": "sur",
"optional": "Optionnel",
"overall_health": "Santé globale",
"password_policy": "Politique de mot de passe",
"preferences": "Préférences",
"preview": "Aperçu",
"program": "Programme",
"reason": "raison",
"recorded": "Enregistré",
"refresh": "Rafraîchir",
"refresh_device": "Actualiser l'appareil",
"remove_claim": "Supprimer la réclamation",
"required": "Champs obligatoires",
"result": "Résultat",
"save": "Sauvegarder",
@@ -170,12 +186,14 @@
"show_all": "Montre tout",
"socket_connection_closed": "Connexion fermée !",
"start": "Début",
"status": "Statut",
"stop_editing": "Arrêter la modification",
"submit": "Soumettre",
"submitted": "Soumis",
"success": "Succès",
"system": "Système",
"table": "Table",
"time_per_device": "Appareils/Seconde",
"timestamp": "Temps",
"to": "à",
"type": "Type",
@@ -190,6 +208,8 @@
"uuid": "UUID",
"vendors": "Vendeurs",
"view_more": "Afficher plus",
"visibility": "Visibilité",
"waiting_for_update": "En attente de mise à jour",
"yes": "Oui"
},
"configuration": {
@@ -210,6 +230,8 @@
"creation_success": "Configuration créée avec succès !",
"currently_associated": "Configuration associée actuelle : {{config}}",
"currently_selected_config": "Configuration actuellement sélectionnée : {{config}}",
"default_configs": "Configurations par défaut",
"default_configurations": "Configurations par défaut",
"delete_config": "Supprimer la configuration",
"details": "Détails",
"device_password": "Mot de passe",
@@ -218,6 +240,7 @@
"devices_affected": "Appareils concernés par cette configuration :",
"edit_configuration": "Modifier la configuration",
"error_delete": "Erreur lors de la tentative de suppression : {{error}}",
"error_delete_blacklist": "Erreur lors de la suppression de la liste noire : {{error}}",
"error_fetching_config": "Erreur lors de la récupération de la configuration",
"error_trying_delete": "Erreur lors de la tentative de suppression : {{error}}",
"error_update": "Erreur: {{error}}",
@@ -261,6 +284,7 @@
"contact": {
"access_pin": "NIP d'accès",
"add_contact": "Ajouter le contact",
"contact": "Contact",
"create_contact": "Créer un contact",
"currently_selected_contact": "Contact actuellement sélectionné : {{contact}}",
"delete": "Effacer le contact?",
@@ -300,12 +324,27 @@
"healthchecks_title": "Supprimer les vérifications d'état"
},
"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}}",
"health_explanation": "Santé des appareils connectés",
"memory_explanation": "Mémoire utilisée par les appareils connectés",
"uptimes_explanation": "Heure à laquelle les appareils connectés ont été activés et connectés"
"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 !",
"success_edit_blacklist": "Liste noire modifiée avec succès !",
"success_removed_blacklist": "Appareil supprimé de la liste noire !",
"uptimes_explanation": "Nombre d'appareils connectés en fonction de leur disponibilité"
},
"device_logs": {
"log": "Bûche",
@@ -320,23 +359,32 @@
"add_success": "Entité créée avec succès !",
"assigned_inventory": "Inventaire assigné",
"cannot_delete": "Vous ne pouvez pas supprimer des entités qui ont des enfants. Supprimez les enfants de cette entité pour pouvoir la supprimer.",
"confirm_map_delete": "Êtes-vous sûr de vouloir supprimer la carte {{name}}? Cette action ne peut pas être annulée",
"currently_selected_entity": "Entité actuellement sélectionnée : {{config}}",
"currently_selected_venue": "Lieu actuellement sélectionné : {{config}}",
"delete_success": "Entité supprimée avec succès",
"delete_warning": "Attention : cette opération ne peut pas être annulée",
"duplicate_from_node": "Dupliquer avec un nœud racine spécifique",
"duplicate_map": "Carte en double",
"duplicate_with_node": "Dupliquer {{mapName}} avec {{rootName}} comme nœud racine",
"edit_failure": "Échec de la mise à jour : {{error}}",
"enter_here": "Entrez les IP que vous souhaitez ajouter ici",
"entire_tree": "Site MAp",
"entire_tree": "Carte du réseau",
"entities": "Entités",
"entity": "Entité",
"error_deleting_map": "Erreur lors de la suppression de la carte : {{error}}",
"error_fetch_entity": "Erreur lors de la récupération des informations sur l'entité",
"error_fetching": "Erreur lors de la récupération des entités",
"error_fetching_map": "Erreur lors de la récupération de la carte : {{error}}",
"error_fetching_tree": "Erreur lors de la récupération de l'arborescence : {{error}}",
"error_saving": "Erreur lors de l'enregistrement de l'entité",
"error_saving_map": "Erreur lors de l'enregistrement de la carte : {{error}}",
"higher_priority": "Faire une priorité plus élevée",
"ip_detection": "Détection IP",
"ip_formats": "Vous pouvez ajouter des adresses IPv4 ou IPv6 aux formats suivants :",
"lower_priority": "Faire une priorité inférieure",
"map": "Carte",
"map_delete_success": "Carte supprimée avec succès !",
"need_select_entity": "Vous devez sélectionner une entité dans le tableau ci-dessous",
"no_ips": "Aucune adresse IP sélectionnée",
"not_assigned": "Non attribué",
@@ -344,6 +392,7 @@
"select_entity": "Sélectionnez cette entité",
"selected_entity": "Entité sélectionnée",
"selected_map": "Carte sélectionnée",
"tree_saved": "Carte enregistrée avec succès !",
"update_failure_error": "Erreur lors de la tentative de mise à jour de l'entité : {{error}}",
"valid_serial": "Doit être un numéro de série valide (12 caractères HEX)",
"venues": "Les lieux"
@@ -382,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..."
@@ -541,6 +590,9 @@
"verification_code": "Entrez votre vérification ici",
"wrong_code": "Le code de vérification saisi n'est pas valide."
},
"preferences": {
"provisioning": "Provisioning"
},
"reboot": {
"directions": "Quand souhaitez-vous redémarrer cet appareil ?",
"now": "Souhaitez-vous redémarrer cet appareil maintenant ?",
@@ -551,7 +603,7 @@
"channel": "Canal",
"directions": "Lancez une analyse wifi de cet appareil, ce qui devrait prendre environ 25 secondes.",
"re_scan": "Nouvelle analyse",
"result_directions": "Veuillez cliquer sur le bouton '$t(scan.re_scan)' si vous souhaitez effectuer un scan avec la même configuration que le précédent.",
"result_directions": "Vous pouvez cliquer sur le bouton 'Scan' en haut à droite pour $t(scan.re_scan)",
"results": "Résultats de l'analyse Wi-Fi",
"scan": "Balayage",
"scanning": "Balayage... ",
@@ -584,7 +636,7 @@
"mac_prefix": "Préfixe MAC",
"max_associations": "Max. Les associations",
"max_clients": "Max. Clients",
"messages_transmitted": "Messages transmis",
"messages_transmitted": "Émission de messages",
"min_associations": "Min. Les associations",
"min_clients": "Min. Clients",
"pause": "Pause",
@@ -592,7 +644,7 @@
"prefix_length": "Obligatoire, doit être d'une longueur de 6 caractères",
"previous_runs": "Courses précédentes",
"received": "reçu",
"received_messages": "Messages reçus",
"received_messages": "Réception des messages",
"reconnect_interval": "Intervalle de reconnexion",
"resume": "CV",
"resume_success": "Exécution reprise !",
@@ -615,11 +667,14 @@
},
"statistics": {
"data": "Données (Ko)",
"data_mb": "Données (Mo)",
"latest_statistics": "Dernières statistiques",
"lifetime_stats": "Statistiques à vie",
"memory": "mémoire",
"no_interfaces": "Aucune statistique de durée de vie de l'interface disponible",
"show_latest": "Dernières statistiques",
"title": "statistiques"
"title": "statistiques",
"used": "Mémoire utilisée %"
},
"status": {
"connection_status": "Statut",
@@ -631,9 +686,27 @@
"percentage_free": "{{percentage}}% de {{total}} gratuit",
"percentage_used": "{{percentage}}% de {{total}} utilisé",
"title": "#{{serialNumber}} état",
"total_memory": "Mémoire totale",
"uptime": "La disponibilité",
"used_total_memory": "{{used}} utilisé / {{total}} total"
},
"subscriber": {
"add_device_subscriber_explanation": "Pour réclamer d'autres appareils, vous pouvez utiliser notre barre de recherche ou réclamer directement à partir du tableau. Si un appareil a déjà été réclamé par un utilisateur, vous devrez accéder à ses détails et le désaffecter avant de le réclamer.",
"create": "Créer un abonné",
"devices_one": "{{count}} Appareil",
"devices_other": "{{count}} appareils",
"edit": "Modifier l'abonné",
"error_create": "Erreur lors de la création de l'abonné : {{error}}",
"error_delete": "Erreur lors de la suppression de l'abonné : {{error}}",
"error_fetching": "Erreur lors de la récupération des abonnés : {{error}}",
"error_fetching_single": "Erreur lors de la récupération de l'abonné : {{error}}",
"error_update": "Erreur lors de la mise à jour de l'abonné : {{error}}",
"is_already_claimed": "est déjà réclamé par",
"subscribers": "Les abonnés",
"success_create": "Abonné créé avec succès !",
"success_delete": "Abonné supprimé avec succès !",
"success_update": "Abonné mis à jour avec succès !"
},
"system": {
"error_fetching": "Erreur lors de la récupération des informations système",
"error_reloading": "Erreur lors du rechargement : {{error}}",
@@ -649,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": {
@@ -659,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"
},
@@ -723,6 +798,7 @@
"send_code_again": "Envoyer code à nouveau",
"show_hide_password": "Afficher/Masquer le mot de passe",
"successful_validation": "Numéro de téléphone validé ! Cliquez sur le bouton Enregistrer pour le lier à votre profil",
"table_title": "Utilisateurs administrateurs",
"update_failure": "Erreur lors de la tentative de mise à jour : {{error}}",
"update_failure_title": "mise à jour a échoué",
"update_success": "L'utilisateur a bien été mis à jour",
@@ -738,7 +814,11 @@
"associations": "Les associations",
"mode": "Mode",
"network_diagram": "Diagramme de réseau",
"override_dfs": "Remplacer DFS",
"radios": "Radios",
"title": "Analyse Wi-Fi"
"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",
"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",
@@ -17,6 +18,7 @@
"blink": "Piscar",
"device_leds": "LEDs do dispositivo",
"execute_now": "Você gostaria de definir este padrão agora?",
"explanation": "Que padrão você gostaria de definir neste dispositivo por 30 segundos?",
"pattern": "Escolha o padrão que deseja usar:",
"set_leds": "Definir LEDs",
"when_blink_leds": "Quando você gostaria de fazer os LEDs do dispositivo piscarem?"
@@ -26,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}}"
@@ -37,13 +40,16 @@
"add_note": "Adicionar nota",
"add_note_explanation": "Escreva sua nova nota abaixo e clique no botão '+' quando terminar",
"adding_ellipsis": "Adicionando ...",
"all": "Todos",
"are_you_sure": "Você tem certeza?",
"back_to_login": "Volte ao login",
"back_to_start": "Voltar ao Início",
"blacklist": "Lista negra",
"by": "Por",
"cancel": "Cancelar",
"certificate": "Certificado",
"certificates": "Certificados",
"claim": "Afirmação",
"clear": "Claro",
"close": "Perto",
"code": "Código",
@@ -58,16 +64,19 @@
"confirm_stop_editing": "Tem certeza que deseja parar de editar? Isso cancelará todas as alterações não salvas que você fez.",
"connected": "Conectado",
"copied": "Copiado!",
"copied_to_clipboard": "Copiado para a área de transferência!",
"copy_to_clipboard": "Copiar para área de transferência",
"create": "Crio",
"created": "Criado",
"created_by": "Criado Por",
"creator": "O Criador",
"current": "Atual",
"custom_date": "Data personalizada",
"dashboard": "painel de controle",
"date": "Encontro",
"day": "dia",
"days": "dias",
"default_map": "Mapa Padrão",
"delete": "Excluir",
"delete_device": "Apagar dispositivo",
"details": "Detalhes",
@@ -85,6 +94,7 @@
"dismiss": "Dispensar",
"do_now": "Faça agora!",
"download": "Baixar",
"duplicate": "Duplicado",
"duration": "Duração",
"edit": "Editar",
"edit_user": "Editar",
@@ -94,6 +104,7 @@
"error": "Erro",
"error_adding_note": "Erro ao adicionar nota",
"error_code": "Erro de código",
"errors": "Erros",
"execute_now": "Você gostaria de executar este comando agora?",
"executed": "Executado",
"exit": "Saída",
@@ -110,6 +121,7 @@
"hours": "horas",
"id": "identidade",
"invalid_credentials": "Nome de usuário e / ou senha inválidos",
"invalid_date_explanation": "Data inválida, use o calendário acessível com o botão à direita",
"invalid_file": "O arquivo escolhido era inválido, por favor, leia as instruções e ajuste seu arquivo de acordo",
"invalid_password": "Esta senha não está de acordo com as regras básicas de senha. Visite nossa página de Política de Senha para saber mais",
"invalid_pem": "Seu arquivo .pem é inválido. Deve começar com '----- BEGIN CERTIFICATE -----' OU '----- BEGIN PRIVATE KEY -----' e deve terminar com '----- END CERTIFICATE --- - 'OU' ----- END PRIVATE KEY ----- '",
@@ -138,21 +150,25 @@
"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",
"not_connected": "Não conectado",
"of_connected": "% de dispositivos",
"of_connected": "% de dispositivos conectados",
"off": "Fora",
"on": "em",
"optional": "Opcional",
"overall_health": "Saúde geral",
"password_policy": "Política de Senha",
"preferences": "Preferências",
"preview": "Visualizar",
"program": "Programa",
"reason": "RAZÃO",
"recorded": "Gravado",
"refresh": "REFRESH",
"refresh_device": "Atualizar dispositivo",
"remove_claim": "Remover reivindicação",
"required": "Requeridos",
"result": "Resultado",
"save": "Salve",
@@ -170,12 +186,14 @@
"show_all": "mostre tudo",
"socket_connection_closed": "Conexão fechada!",
"start": "Começar",
"status": "Status",
"stop_editing": "Pare de editar",
"submit": "Enviar",
"submitted": "Submetido",
"success": "Sucesso",
"system": "Sistema",
"table": "Mesa",
"time_per_device": "Dispositivo / segundo",
"timestamp": "tempo",
"to": "Para",
"type": "Tipo",
@@ -190,6 +208,8 @@
"uuid": "UUID",
"vendors": "Vendedores",
"view_more": "Veja mais",
"visibility": "visibilidade",
"waiting_for_update": "Aguardando atualização",
"yes": "sim"
},
"configuration": {
@@ -210,6 +230,8 @@
"creation_success": "Configuração criada com sucesso!",
"currently_associated": "Configuração atual associada: {{config}}",
"currently_selected_config": "Configuração atualmente selecionada: {{config}}",
"default_configs": "Configurações padrão",
"default_configurations": "Configurações padrão",
"delete_config": "Excluir configuração",
"details": "Detalhes",
"device_password": "Senha",
@@ -218,6 +240,7 @@
"devices_affected": "Dispositivos afetados por esta configuração:",
"edit_configuration": "Editar configuração",
"error_delete": "Erro ao tentar excluir: {{error}}",
"error_delete_blacklist": "Erro ao excluir da lista negra: {{error}}",
"error_fetching_config": "Erro ao buscar configuração",
"error_trying_delete": "Erro ao tentar excluir: {{error}}",
"error_update": "Erro: {{error}}",
@@ -261,6 +284,7 @@
"contact": {
"access_pin": "PIN de acesso",
"add_contact": "Adicionar contato",
"contact": "Contato",
"create_contact": "Criar Contato",
"currently_selected_contact": "Contato atualmente selecionado: {{contact}}",
"delete": "Excluir contato?",
@@ -300,12 +324,27 @@
"healthchecks_title": "Excluir verificações de saúde"
},
"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}}",
"health_explanation": "Saúde de dispositivos conectados",
"memory_explanation": "Memória usada por dispositivos conectados",
"uptimes_explanation": "Há tempo em que os dispositivos conectados estão ativados e conectados"
"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!",
"success_edit_blacklist": "Lista negra editada com sucesso!",
"success_removed_blacklist": "Dispositivo removido com sucesso da lista negra!",
"uptimes_explanation": "Quantidade de dispositivos conectados com base em seu tempo de atividade"
},
"device_logs": {
"log": "Registro",
@@ -320,23 +359,32 @@
"add_success": "Entidade criada com sucesso!",
"assigned_inventory": "Estoque Atribuído",
"cannot_delete": "Você não pode excluir entidades que têm filhos. Exclua os filhos desta entidade para poder excluí-la.",
"confirm_map_delete": "Tem certeza que deseja excluir o mapa {{name}}? Esta ação não pode ser revertida",
"currently_selected_entity": "Entidade atualmente selecionada: {{config}}",
"currently_selected_venue": "Local selecionado atualmente: {{config}}",
"delete_success": "Entidade excluída com sucesso",
"delete_warning": "Aviso: esta operação não pode ser revertida",
"duplicate_from_node": "Duplicar com nó raiz específico",
"duplicate_map": "Mapa duplicado",
"duplicate_with_node": "Duplicar {{mapName}} com {{rootName}} como nó raiz",
"edit_failure": "Atualização malsucedida: {{error}}",
"enter_here": "Digite o (s) IP (s) que deseja adicionar aqui",
"entire_tree": "Mapa do Site",
"entire_tree": "Mapa de Rede",
"entities": "Entidades",
"entity": "Entidade",
"error_deleting_map": "Erro ao excluir mapa: {{error}}",
"error_fetch_entity": "Erro ao buscar informações da entidade",
"error_fetching": "Erro ao buscar entidades",
"error_fetching_map": "Erro ao buscar mapa: {{error}}",
"error_fetching_tree": "Erro ao buscar árvore: {{error}}",
"error_saving": "Erro ao salvar entidade",
"error_saving_map": "Erro ao salvar o mapa: {{error}}",
"higher_priority": "Dê maior prioridade",
"ip_detection": "Detecção de IP",
"ip_formats": "Você pode adicionar endereços IPv4 ou IPv6 nos seguintes formatos:",
"lower_priority": "Faça menor prioridade",
"map": "Mapa",
"map_delete_success": "Mapa excluído com sucesso!",
"need_select_entity": "Você precisa selecionar uma entidade da tabela abaixo",
"no_ips": "Nenhum IP selecionado",
"not_assigned": "Não atribuído",
@@ -344,6 +392,7 @@
"select_entity": "Selecione esta Entidade",
"selected_entity": "Entidade Selecionada",
"selected_map": "Mapa Selecionado",
"tree_saved": "Mapa salvo com sucesso!",
"update_failure_error": "Erro ao tentar atualizar a entidade: {{error}}",
"valid_serial": "Precisa ser um número de série válido (12 caracteres HEX)",
"venues": "Locais"
@@ -382,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 ..."
@@ -541,6 +590,9 @@
"verification_code": "Insira sua verificação aqui",
"wrong_code": "O código de verificação inserido não é válido."
},
"preferences": {
"provisioning": "Provisioning"
},
"reboot": {
"directions": "Quando você gostaria de reinicializar este dispositivo?",
"now": "Você gostaria de reiniciar este dispositivo agora?",
@@ -551,7 +603,7 @@
"channel": "Canal",
"directions": "Inicie uma verificação de wi-fi deste dispositivo, o que deve levar aproximadamente 25 segundos.",
"re_scan": "Verificar novamente",
"result_directions": "Clique no botão '$ t (scan.re_scan)' se desejar fazer uma varredura com a mesma configuração da anterior.",
"result_directions": "Você pode clicar no botão 'Scan' no canto superior direito para $t(scan.re_scan)",
"results": "Resultados da verificação de Wi-Fi",
"scan": "Varredura",
"scanning": "Scanning... ",
@@ -584,7 +636,7 @@
"mac_prefix": "Prefixo MAC",
"max_associations": "Máx. Associações",
"max_clients": "Máx. Clientes",
"messages_transmitted": "Mensagens Transmitidas",
"messages_transmitted": "Msgs TX",
"min_associations": "Min. Associações",
"min_clients": "Min. Clientes",
"pause": "pausa",
@@ -592,7 +644,7 @@
"prefix_length": "Obrigatório, deve ter 6 caracteres",
"previous_runs": "Execuções anteriores",
"received": "recebido",
"received_messages": "Mensagens recebidas",
"received_messages": "Msgs RX",
"reconnect_interval": "Intervalo de reconexão",
"resume": "Currículo",
"resume_success": "Executar retomado!",
@@ -615,11 +667,14 @@
},
"statistics": {
"data": "Dados (KB)",
"data_mb": "Dados (MB)",
"latest_statistics": "Estatísticas mais recentes",
"lifetime_stats": "Estatísticas de vida",
"memory": "Memória",
"no_interfaces": "Nenhuma estatística de tempo de vida da interface disponível",
"show_latest": "Últimas estatísticas",
"title": "Estatisticas"
"title": "Estatisticas",
"used": "Memoria usada %"
},
"status": {
"connection_status": "Status",
@@ -631,9 +686,27 @@
"percentage_free": "{{percentage}}% de {{total}} grátis",
"percentage_used": "{{percentage}}% de {{total}} usado",
"title": "#{{serialNumber}} status",
"total_memory": "Memória total",
"uptime": "Tempo de atividade",
"used_total_memory": "{{used}} usado / {{total}} total"
},
"subscriber": {
"add_device_subscriber_explanation": "Para reivindicar outros dispositivos, você pode usar nossa barra de pesquisa ou reivindicar diretamente na tabela. Se um dispositivo já foi reivindicado por um usuário, você precisará acessar os detalhes dele e cancelar a atribuição antes de reivindicá-lo.",
"create": "Criar assinante",
"devices_one": "{{count}} Dispositivo",
"devices_other": "{{count}} dispositivos",
"edit": "Editar Assinante",
"error_create": "Erro ao criar assinante: {{error}}",
"error_delete": "Erro ao excluir assinante: {{error}}",
"error_fetching": "Erro ao buscar assinantes: {{error}}",
"error_fetching_single": "Erro ao buscar assinante: {{error}}",
"error_update": "Erro ao atualizar assinante: {{error}}",
"is_already_claimed": "já é reivindicado por",
"subscribers": "Inscritos",
"success_create": "Assinante criado com sucesso!",
"success_delete": "Assinante excluído com sucesso!",
"success_update": "Assinante atualizado com sucesso!"
},
"system": {
"error_fetching": "Erro ao buscar informações do sistema",
"error_reloading": "Erro ao recarregar: {{error}}",
@@ -649,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": {
@@ -659,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"
},
@@ -723,6 +798,7 @@
"send_code_again": "Envie o Código Novamente",
"show_hide_password": "Mostrar / ocultar senha",
"successful_validation": "Número de telefone validado! Clique no botão Salvar para vinculá-lo ao seu perfil",
"table_title": "Usuários administrativos",
"update_failure": "Erro ao tentar atualizar: {{error}}",
"update_failure_title": "Atualização falhou",
"update_success": "Usuário atualizado com sucesso",
@@ -738,7 +814,11 @@
"associations": "Associações",
"mode": "Modo",
"network_diagram": "Diagrama de rede",
"override_dfs": "Substituir DFS",
"radios": "Rádios",
"title": "Análise de Wi-Fi"
"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",
"waiting_for_data": "Aguardando para receber dados do dispositivo. Verifique novamente mais tarde"
}
}

BIN
src/assets/NotFound.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -13,6 +13,7 @@ import {
cilArrowTop,
cilAsterisk,
cilBan,
cilBarcode,
cilBasket,
cilBell,
cilBold,
@@ -108,6 +109,7 @@ export const icons = {
cilArrowTop,
cilAsterisk,
cilBan,
cilBarcode,
cilBasket,
cilBell,
cilBold,

View File

@@ -0,0 +1,163 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import {
CForm,
CInput,
CLabel,
CCol,
CFormGroup,
CInvalidFeedback,
CFormText,
CRow,
CTextarea,
} from '@coreui/react';
import { CopyToClipboardButton } from 'ucentral-libs';
const AddDefaultConfigurationForm = ({
t,
disable,
fields,
updateField,
updateFieldWithKey,
deviceTypes,
}) => {
const [typeOptions, setTypeOptions] = useState([]);
const [chosenTypes, setChosenTypes] = useState([]);
const parseOptions = () => {
const options = [{ value: '*', label: 'All' }];
const newOptions = deviceTypes.map((option) => ({
value: option,
label: option,
}));
options.push(...newOptions);
setTypeOptions(options);
setChosenTypes([]);
};
const typeOnChange = (chosenArray) => {
const allIndex = chosenArray.findIndex((el) => el.value === '*');
// If the All option was chosen before, we take it out of the array
if (allIndex === 0 && chosenTypes.length > 0) {
const newResults = chosenArray.slice(1);
setChosenTypes(newResults);
updateFieldWithKey('deviceTypes', {
value: newResults.map((el) => el.value),
error: false,
notEmpty: true,
});
} else if (allIndex > 0) {
setChosenTypes([{ value: '*', label: 'All' }]);
updateFieldWithKey('deviceTypes', { value: ['*'], error: false, notEmpty: true });
} else if (chosenArray.length > 0) {
setChosenTypes(chosenArray);
updateFieldWithKey('deviceTypes', {
value: chosenArray.map((el) => el.value),
error: false,
notEmpty: true,
});
} else {
setChosenTypes([]);
updateFieldWithKey('deviceTypes', { value: [], error: false, notEmpty: true });
}
};
useEffect(() => {
parseOptions();
}, [deviceTypes]);
return (
<CForm>
<CFormGroup row className="pb-3">
<CLabel col htmlFor="name">
{t('user.name')}
</CLabel>
<CCol sm="7">
<CInput
id="name"
type="text"
required
value={fields.name.value}
onChange={updateField}
invalid={fields.name.error}
disabled={disable}
maxLength="50"
/>
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
</CFormGroup>
<CFormGroup row className="pb-3">
<CLabel col htmlFor="description">
{t('user.description')}
</CLabel>
<CCol sm="7">
<CInput
id="description"
type="text"
required
value={fields.description.value}
onChange={updateField}
invalid={fields.description.error}
disabled={disable}
maxLength="50"
/>
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
</CFormGroup>
<CRow className="pb-3">
<CLabel col htmlFor="deviceTypes">
<div>{t('configuration.supported_device_types')}:</div>
</CLabel>
<CCol sm="7">
<Select
isMulti
closeMenuOnSelect={false}
id="deviceTypes"
options={typeOptions}
onChange={typeOnChange}
value={chosenTypes}
className={`basic-multi-select ${fields.deviceTypes.error ? 'border-danger' : ''}`}
classNamePrefix="select"
/>
<CFormText hidden={!fields.deviceTypes.error} color="danger">
{t('configuration.need_device_type')}
</CFormText>
</CCol>
</CRow>
<div className="pb-3">
{t('configure.enter_new')}
<CopyToClipboardButton t={t} size="sm" content={fields.configuration.value} />
</div>
<CRow className="pb-3">
<CCol>
<CTextarea
style={{ overflowY: 'scroll', height: '500px' }}
id="configuration"
type="text"
required
value={fields.configuration.value}
onChange={updateField}
invalid={fields.configuration.error}
disabled={disable}
/>
<CFormText hidden={!fields.configuration.error} color="danger">
{t('configure.valid_json')}
</CFormText>
</CCol>
</CRow>
</CForm>
);
};
AddDefaultConfigurationForm.propTypes = {
t: PropTypes.func.isRequired,
disable: PropTypes.bool.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
updateFieldWithKey: PropTypes.func.isRequired,
deviceTypes: PropTypes.instanceOf(Array).isRequired,
};
export default AddDefaultConfigurationForm;

View File

@@ -0,0 +1,183 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { CModal, CModalHeader, CModalTitle, CModalBody, CButton, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX, cilSave } from '@coreui/icons';
import { useToast, useFormFields, useAuth } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { useTranslation } from 'react-i18next';
import { checkIfJson } from 'utils/helper';
import Form from './Form';
const initialForm = {
name: {
value: '',
error: false,
required: true,
},
description: {
value: '',
error: false,
},
deviceTypes: {
value: [],
error: false,
notEmpty: true,
},
configuration: {
value: '',
error: false,
required: true,
},
};
const AddConfigurationModal = ({ show, toggle, refresh }) => {
const { t } = useTranslation();
const { addToast } = useToast();
const { currentToken, endpoints } = useAuth();
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialForm);
const [loading, setLoading] = useState(false);
const [deviceTypes, setDeviceTypes] = useState([]);
const getDeviceTypes = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.owfms}/api/v1/firmwares?deviceSet=true`, {
headers,
})
.then((response) => {
setDeviceTypes([...response.data.deviceTypes]);
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
const validation = () => {
let success = true;
for (const [key, field] of Object.entries(fields)) {
if (field.required && field.value === '') {
updateField(key, { error: true });
success = false;
break;
}
if (field.notEmpty && field.value.length === 0) {
updateField(key, { error: true, notEmpty: true });
success = false;
break;
}
}
if (!checkIfJson(fields.configuration.value)) {
updateField('configuration', { error: true });
success = false;
}
return success;
};
const addConfiguration = () => {
if (validation()) {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
const parameters = {
name: fields.name.value,
description: fields.description.value,
modelIds: fields.deviceTypes.value,
configuration: fields.configuration.value,
};
axiosInstance
.post(
`${endpoints.owgw}/api/v1/default_configuration/${fields.name.value}`,
parameters,
options,
)
.then(() => {
if (refresh !== null) refresh();
toggle();
addToast({
title: t('common.success'),
body: t('configuration.creation_success'),
color: 'success',
autohide: true,
});
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('entity.add_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
}
};
useEffect(() => {
if (show) {
getDeviceTypes();
setFormFields(initialForm);
}
}, [show]);
return (
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('configuration.create')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.add')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={addConfiguration}>
<CIcon content={cilSave} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody className="px-5">
<Form
t={t}
disable={loading}
fields={fields}
updateField={updateFieldWithId}
updateFieldWithKey={updateField}
deviceTypes={deviceTypes}
show={show}
/>
</CModalBody>
</CModal>
);
};
AddConfigurationModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
refresh: PropTypes.func,
};
AddConfigurationModal.defaultProps = {
refresh: null,
};
export default AddConfigurationModal;

View File

@@ -0,0 +1,158 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import {
CButton,
CModal,
CModalHeader,
CModalTitle,
CModalBody,
CPopover,
CRow,
CCol,
CLabel,
CTextarea,
CInput,
CInvalidFeedback,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { useAuth, useToast } from 'ucentral-libs';
import { cilPlus, cilX } from '@coreui/icons';
import axiosInstance from 'utils/axiosInstance';
const AddToBlacklistModal = ({ show, toggle, serialNumber, refresh }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { addToast } = useToast();
const { endpoints, currentToken } = useAuth();
const [chosenSerialNumber, setChosenSerialNumber] = useState('');
const [reason, setReason] = useState('');
const addToBlacklist = () => {
setLoading(true);
const parameters = {
serialNumber: chosenSerialNumber,
reason,
};
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.post(`${endpoints.owgw}/api/v1/blacklist/${chosenSerialNumber}`, parameters, { headers })
.then(() => {
addToast({
title: t('common.success'),
body: t('device.success_added_blacklist'),
color: 'success',
autohide: true,
});
toggle();
if (refresh) refresh();
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_adding_blacklist', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
if (show) {
if (serialNumber) setChosenSerialNumber(serialNumber);
else setChosenSerialNumber('');
}
}, [show, serialNumber]);
return (
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('device.add_to_blacklist')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.add')}>
<CButton
color="primary"
variant="outline"
className="ml-2"
onClick={addToBlacklist}
disabled={
chosenSerialNumber.length !== 12 ||
!chosenSerialNumber.match('^[a-fA-F0-9]+$') ||
reason === '' ||
loading
}
>
<CIcon content={cilPlus} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<CRow>
<CLabel col sm="3">
{t('common.serial_number')}
</CLabel>
<CCol sm="9" className="pt-1">
<CInput
id="description"
type="text"
required
value={chosenSerialNumber}
onChange={(e) => setChosenSerialNumber(e.target.value)}
invalid={
chosenSerialNumber.length !== 12 && chosenSerialNumber.match('^[a-fA-F0-9]+$')
}
disabled={loading}
maxLength="50"
/>
<CInvalidFeedback>{t('entity.valid_serial')}</CInvalidFeedback>
</CCol>
</CRow>
<CRow>
<CLabel col sm="3">
{t('common.reason')}
</CLabel>
<CCol sm="9" className="pt-2">
<CTextarea
name="reason"
id="reason"
rows="3"
type="text"
required
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</CCol>
</CRow>
</CModalBody>
</CModal>
);
};
AddToBlacklistModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
serialNumber: PropTypes.string,
refresh: PropTypes.func,
};
AddToBlacklistModal.defaultProps = {
serialNumber: '',
refresh: null,
};
export default AddToBlacklistModal;

View File

@@ -0,0 +1,210 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import {
CCardBody,
CDataTable,
CButton,
CLink,
CCard,
CCardHeader,
CPopover,
CSelect,
CButtonToolbar,
} from '@coreui/react';
import { cilSearch, cilPencil, cilPlus, cilTrash } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import ReactTooltip from 'react-tooltip';
import { FormattedDate } from 'ucentral-libs';
const BlacklistTable = ({
currentPage,
devices,
toggleAddBlacklist,
toggleEditModal,
devicesPerPage,
loading,
removeFromBlacklist,
updateDevicesPerPage,
pageCount,
updatePage,
t,
}) => {
const columns = [
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
{ key: 'created', label: t('device.blacklisted_on'), _style: { width: '1%' } },
{ key: 'author', label: t('common.by'), filter: false, _style: { width: '15%' } },
{ key: 'reason', label: t('common.reason'), filter: false },
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
];
const hideTooltips = () => ReactTooltip.hide();
const escFunction = (event) => {
if (event.keyCode === 27) {
hideTooltips();
}
};
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}, []);
return (
<>
<CCard className="m-0 p-0">
<CCardHeader className="p-0 text-right">
<CPopover content={t('device.add_to_blacklist')}>
<CButton size="sm" color="primary" onClick={toggleAddBlacklist}>
<CIcon content={cilPlus} />
</CButton>
</CPopover>
</CCardHeader>
<CCardBody className="p-0">
<CDataTable
addTableClasses="ignore-overflow table-sm"
items={devices ?? []}
fields={columns}
hover
border
loading={loading}
scopedSlots={{
serialNumber: (item) => (
<td className="text-center align-middle">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
{item.serialNumber}
</CLink>
</td>
),
created: (item) => (
<td className="text-left align-middle">
<div style={{ width: '130px' }}>
<FormattedDate date={item.created} />
</div>
</td>
),
author: (item) => <td className="align-middle">{item.author}</td>,
reason: (item) => <td className="align-middle">{item.reason}</td>,
actions: (item) => (
<td className="text-center align-middle">
<CButtonToolbar
role="group"
className="justify-content-center"
style={{ width: '130px' }}
>
<CPopover content={t('configuration.details')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-search" content={cilSearch} size="sm" />
</CButton>
</CLink>
</CPopover>
<CPopover content={t('device.remove_from_blacklist')}>
<CButton
onClick={() => removeFromBlacklist(item.serialNumber)}
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
style={{ width: '33px', height: '30px' }}
>
<CIcon content={cilTrash} size="sm" />
</CButton>
</CPopover>
<CPopover content={t('common.edit')}>
<CButton
onClick={() => toggleEditModal(item.serialNumber)}
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
style={{ width: '33px', height: '30px' }}
>
<CIcon content={cilPencil} size="sm" />
</CButton>
</CPopover>
</CButtonToolbar>
</td>
),
}}
/>
<div className="d-flex flex-row pl-3">
<div className="pr-3">
<ReactPaginate
previousLabel="← Previous"
nextLabel="Next →"
pageCount={pageCount}
onPageChange={updatePage}
forcePage={Number(currentPage)}
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
nextClassName="page-item"
nextLinkClassName="page-link"
activeClassName="active"
/>
</div>
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
<div style={{ width: '100px' }} className="px-2">
<CSelect
custom
defaultValue={devicesPerPage}
onChange={(e) => updateDevicesPerPage(e.target.value)}
disabled={loading}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</CSelect>
</div>
</div>
</CCardBody>
</CCard>
</>
);
};
BlacklistTable.propTypes = {
currentPage: PropTypes.string,
devices: PropTypes.instanceOf(Array).isRequired,
toggleAddBlacklist: PropTypes.func.isRequired,
toggleEditModal: PropTypes.func.isRequired,
updateDevicesPerPage: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
updatePage: PropTypes.func.isRequired,
devicesPerPage: PropTypes.string.isRequired,
removeFromBlacklist: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
};
BlacklistTable.defaultProps = {
currentPage: '0',
};
export default React.memo(BlacklistTable);

View File

@@ -0,0 +1,30 @@
.firmwareTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 400px;
}
.deleteTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 200px;
}
.tooltipHeader {
padding-left: 5px;
padding-right: 10px;
border-top-left-radius: 1rem !important;
border-top-right-radius: 1rem !important;
}

View File

@@ -0,0 +1,244 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import axiosInstance from 'utils/axiosInstance';
import { getItem, setItem } from 'utils/localStorageHelper';
import { useAuth, useToast, useToggle } from 'ucentral-libs';
import AddToBlacklistModal from 'components/AddToBlacklistModal';
import EditBlacklistModal from 'components/EditBlacklistModal';
import Table from './Table';
const BlacklistTable = () => {
const { t } = useTranslation();
const { addToast } = useToast();
const history = useHistory();
const [page, setPage] = useState(parseInt(sessionStorage.getItem('deviceTable') ?? 0, 10));
const { currentToken, endpoints } = useAuth();
const [deviceCount, setDeviceCount] = useState(0);
const [pageCount, setPageCount] = useState(0);
const [devicesPerPage, setDevicesPerPage] = useState(getItem('devicesPerPage') || '10');
const [devices, setDevices] = useState([]);
const [loading, setLoading] = useState(true);
const [editSerial, setEditSerial] = useState('');
const [showEditModal, setShowEditModal] = useState(false);
const [showAddModal, toggleAddModal] = useToggle(false);
const toggleEditModal = (serialNumber) => {
if (serialNumber) setEditSerial(serialNumber);
setShowEditModal(!showEditModal);
};
const getDeviceInformation = (selectedPage = page, devicePerPage = devicesPerPage) => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.owgw}/api/v1/blacklist?limit=${devicePerPage}&offset=${
devicePerPage * selectedPage
}`,
options,
)
.then((response) => {
setDevices(response.data.devices);
setLoading(false);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
const getCount = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/blacklist?countOnly=true`, {
headers,
})
.then((response) => {
const devicesCount = response.data.count;
const pagesCount = Math.ceil(devicesCount / devicesPerPage);
setPageCount(pagesCount);
setDeviceCount(devicesCount);
let selectedPage = page;
if (page >= pagesCount) {
history.push(`/devices?page=${pagesCount - 1}`);
selectedPage = pagesCount - 1;
}
getDeviceInformation(selectedPage);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
const refreshDevice = (serialNumber) => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
let newDevice;
axiosInstance
.get(
`${endpoints.owgw}/api/v1/blacklist?deviceWithStatus=true&select=${encodeURIComponent(
serialNumber,
)}`,
options,
)
.then(
({
data: {
devicesWithStatus: [device],
},
}) => {
newDevice = device;
return axiosInstance.get(
`${endpoints.owfms}/api/v1/firmwareAge?select=${serialNumber}`,
options,
);
},
)
.then((response) => {
newDevice.firmwareInfo = {
age: response.data.ages[0].age,
latest: response.data.ages[0].latest,
};
const foundIndex = devices.findIndex((obj) => obj.serialNumber === serialNumber);
const newList = devices;
newList[foundIndex] = newDevice;
setDevices(newList);
setLoading(false);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
const updateDevicesPerPage = (value) => {
setItem('devicesPerPage', value);
setDevicesPerPage(value);
const newPageCount = Math.ceil(deviceCount / value);
setPageCount(newPageCount);
let selectedPage = page;
if (page >= newPageCount) {
history.push(`/blacklist?page=${newPageCount - 1}`);
selectedPage = newPageCount - 1;
}
getDeviceInformation(selectedPage, value);
};
const updatePageCount = ({ selected: selectedPage }) => {
sessionStorage.setItem('deviceTable', selectedPage);
setPage(selectedPage);
getDeviceInformation(selectedPage);
};
const removeFromBlacklist = (serialNumber) => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.delete(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, { headers })
.then(() => {
addToast({
title: t('common.success'),
body: t('device.success_removed_blacklist'),
color: 'success',
autohide: true,
});
getCount();
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_adding_blacklist', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
getCount();
}, []);
return (
<div>
<Table
currentPage={page}
t={t}
devices={devices}
loading={loading}
toggleAddBlacklist={toggleAddModal}
toggleEditModal={toggleEditModal}
updateDevicesPerPage={updateDevicesPerPage}
devicesPerPage={devicesPerPage}
pageCount={pageCount}
updatePage={updatePageCount}
pageRangeDisplayed={5}
refreshDevice={refreshDevice}
removeFromBlacklist={removeFromBlacklist}
/>
{showAddModal ? (
<AddToBlacklistModal show={showAddModal} toggle={toggleAddModal} refresh={getCount} />
) : null}
<EditBlacklistModal
show={showEditModal}
toggle={toggleEditModal}
refresh={getCount}
serialNumber={editSerial}
/>
</div>
);
};
export default BlacklistTable;

View File

@@ -5,21 +5,18 @@ import {
CModalTitle,
CModalBody,
CModalFooter,
CSwitch,
CCol,
CRow,
CFormGroup,
CInputRadio,
CLabel,
CPopover,
CRow,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import { dateToUnix } from 'utils/helper';
import 'react-widgets/styles.css';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
@@ -31,38 +28,24 @@ const BlinkModal = ({ show, toggleModal }) => {
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [isNow, setIsNow] = useState(false);
const [waiting, setWaiting] = useState(false);
const [chosenDate, setChosenDate] = useState(new Date().toString());
const [chosenPattern, setPattern] = useState('on');
const [chosenPattern, setPattern] = useState('blink');
const [result, setResult] = useState(null);
const toggleNow = () => {
setIsNow(!isNow);
};
const setDate = (date) => {
if (date) {
setChosenDate(date.toString());
}
};
useEffect(() => {
if (show) {
setWaiting(false);
setChosenDate(new Date().toString());
setPattern('on');
setPattern('blink');
setResult(null);
}
}, [show]);
const doAction = () => {
setWaiting(true);
const utcDate = new Date(chosenDate);
const parameters = {
serialNumber: deviceSerialNumber,
when: isNow ? 0 : dateToUnix(utcDate),
when: 0,
pattern: chosenPattern,
duration: 30,
};
@@ -79,15 +62,28 @@ const BlinkModal = ({ show, toggleModal }) => {
{ headers },
)
.then(() => {
if (chosenPattern !== 'blink') {
addToast({
title: t('common.success'),
body: t('commands.command_success'),
color: 'success',
autohide: true,
});
}
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(() => {
@@ -113,11 +109,26 @@ const BlinkModal = ({ show, toggleModal }) => {
) : (
<div>
<CModalBody>
<CFormGroup row>
<CRow className="mb-3">
<CCol>{t('blink.explanation')}</CCol>
</CRow>
<CFormGroup row className="mb-0">
<CCol md="3">
<CLabel>{t('blink.pattern')}</CLabel>
</CCol>
<CCol>
<CFormGroup variant="custom-radio" onClick={() => setPattern('blink')} inline>
<CInputRadio
custom
defaultChecked={chosenPattern === 'blink'}
id="radio3"
name="radios"
value="option3"
/>
<CLabel variant="custom-checkbox" htmlFor="radio3">
{t('blink.blink')}
</CLabel>
</CFormGroup>
<CFormGroup variant="custom-radio" onClick={() => setPattern('on')} inline>
<CInputRadio
custom
@@ -142,56 +153,15 @@ const BlinkModal = ({ show, toggleModal }) => {
{t('common.off')}
</CLabel>
</CFormGroup>
<CFormGroup variant="custom-radio" onClick={() => setPattern('blink')} inline>
<CInputRadio
custom
defaultChecked={chosenPattern === 'blink'}
id="radio3"
name="radios"
value="option3"
/>
<CLabel variant="custom-checkbox" htmlFor="radio3">
{t('blink.blink')}
</CLabel>
</CFormGroup>
</CCol>
</CFormGroup>
<CRow className="pt-1">
<CCol md="8">
<p>{t('blink.execute_now')}</p>
</CCol>
<CCol>
<CSwitch
disabled={waiting}
color="primary"
defaultChecked={isNow}
onClick={toggleNow}
labelOn={t('common.yes')}
labelOff={t('common.no')}
/>
</CCol>
</CRow>
<CRow hidden={isNow} className="pt-3">
<CCol md="4" className="pt-2">
<p>{t('common.custom_date')}</p>
</CCol>
<CCol xs="12" md="8">
<DatePicker
selected={new Date(chosenDate)}
includeTime
value={new Date(chosenDate)}
placeholder="Select custom date"
disabled={waiting}
onChange={(date) => setDate(date)}
min={new Date()}
/>
</CCol>
</CRow>
</CModalBody>
<CModalFooter>
<LoadingButton
label={isNow ? t('blink.set_leds') : t('common.schedule')}
isLoadingLabel={t('common.loading_ellipsis')}
label={t('common.submit')}
isLoadingLabel={
chosenPattern === 'blink' ? 'LEDs are blinking... ' : t('common.loading_ellipsis')
}
isLoading={waiting}
action={doAction}
block={false}

View File

@@ -0,0 +1,100 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
CRow,
CCol,
CCard,
CCardBody,
CCardHeader,
CLabel,
CPopover,
CSpinner,
CButton,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSync } from '@coreui/icons';
import { useTranslation } from 'react-i18next';
import { CopyToClipboardButton, useAuth, useToast, FormattedDate } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
const CapabilitiesDisplay = ({ serialNumber }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [capabilities, setCapabilities] = useState({});
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const getCapabilities = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(serialNumber)}/capabilities`,
options,
)
.then((response) => {
setCapabilities(response.data);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
getCapabilities();
}, []);
return (
<CCard className="m-0">
<CCardHeader className="dark-header">
<div className="d-flex flex-row-reverse align-items-center">
<div className="text-right">
<CPopover content={t('common.refresh')}>
<CButton size="sm" color="info" onClick={getCapabilities}>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody>
<h5>
{t('device.capabilities')}
<CopyToClipboardButton
t={t}
size="sm"
content={JSON.stringify(capabilities?.capabilities ?? {})}
/>
</h5>
<CRow>
<CCol>
<CLabel>
{t('inventory.last_modification')}: <FormattedDate date={capabilities?.lastUpdate} />
</CLabel>
</CCol>
</CRow>
{loading ? <CSpinner /> : null}
<pre className="ignore">{JSON.stringify(capabilities?.capabilities ?? {}, null, 4)}</pre>
</CCardBody>
</CCard>
);
};
CapabilitiesDisplay.propTypes = {
serialNumber: PropTypes.string.isRequired,
};
export default CapabilitiesDisplay;

View File

@@ -2,14 +2,14 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
CWidgetDropdown,
CRow,
CCol,
CCardHeader,
CCardBody,
CButton,
CDataTable,
CCard,
CPopover,
CButtonToolbar,
CFormText,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import DatePicker from 'react-widgets/DatePicker';
@@ -41,7 +41,9 @@ const DeviceCommands = () => {
const [commands, setCommands] = useState([]);
const [loading, setLoading] = useState(false);
const [start, setStart] = useState('');
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState('');
const [endError, setEndError] = useState(false);
const [commandLimit, setCommandLimit] = useState(25);
// Load more button related
const [loadingMore, setLoadingMore] = useState(false);
@@ -65,11 +67,25 @@ const DeviceCommands = () => {
};
const modifyStart = (value) => {
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
} catch (e) {
setEnd('');
setEndError(true);
}
};
const deleteCommandFromList = (commandUuid) => {
@@ -189,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: '',
@@ -245,25 +262,47 @@ const DeviceCommands = () => {
return (
<div>
<CWidgetDropdown
className="m-0"
inverse="true"
color="gradient-primary"
header={t('commands.title')}
footerSlot={
<div className="pb-1 px-3">
<CRow className="mb-2">
<CCol>
From:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
</CCol>
<CCol>
<CCard className="m-0">
<CCardHeader className="dark-header">
<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={getCommands}
disabled={startError || endError}
>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard>
<div className="overflow-auto" style={{ height: '200px' }}>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
</div>
</CCardHeader>
<CCardBody className="p-1">
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
@@ -279,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>
),
@@ -297,7 +337,7 @@ const DeviceCommands = () => {
{item.submitted && item.submitted !== '' ? (
<FormattedDate date={item.submitted} />
) : (
'Pending'
'-'
)}
</td>
),
@@ -320,22 +360,15 @@ const DeviceCommands = () => {
shape="square"
size="sm"
className="mx-2"
disabled={item.completed === 0}
onClick={() => {
toggleDetails(item);
}}
>
{item.command === 'trace' ? (
<CIcon
name="cil-cloud-download"
content={cilCloudDownload}
size="md"
/>
<CIcon name="cil-cloud-download" content={cilCloudDownload} />
) : (
<CIcon
name="cil-calendar-check"
content={cilCalendarCheck}
size="md"
/>
<CIcon name="cil-calendar-check" content={cilCalendarCheck} />
)}
</CButton>
</CPopover>
@@ -350,7 +383,7 @@ const DeviceCommands = () => {
toggleResponse(item);
}}
>
<CIcon name="cilList" size="md" />
<CIcon name="cilList" />
</CButton>
</CPopover>
<CPopover content={t('common.delete')}>
@@ -364,7 +397,7 @@ const DeviceCommands = () => {
toggleConfirmModal(item.UUID, index);
}}
>
<CIcon name="cilTrash" size="mdå" />
<CIcon name="cilTrash" />
</CButton>
</CPopover>
</CButtonToolbar>
@@ -385,17 +418,8 @@ const DeviceCommands = () => {
</div>
)}
</div>
</CCardBody>
</CCard>
</div>
}
>
<div className="text-right float-right">
<CButton onClick={refreshCommands} size="sm">
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
</CButton>
</div>
</CWidgetDropdown>
<WifiScanResultModalWidget
show={showScanModal}
toggle={toggleScanModal}

View File

@@ -37,14 +37,16 @@ const ConfigurationDisplay = ({ getData, deviceConfig }) => {
<CopyToClipboardButton
t={t}
size="sm"
content={JSON.stringify(deviceConfig?.configuration ?? {})}
content={JSON.stringify(deviceConfig?.configuration ?? {}, null, 4)}
/>
</h5>
<CRow>
<CCol md="2" xl="2" xxl="1">
<CLabel>{t('configuration.last_configuration_change')}: </CLabel>
<CCol>
<CLabel>
{t('configuration.last_configuration_change')}:{' '}
{prettyDate(deviceConfig?.lastConfigurationChange)}
</CLabel>
</CCol>
<CCol>{prettyDate(deviceConfig?.lastConfigurationChange)}</CCol>
</CRow>
<pre className="ignore">{JSON.stringify(deviceConfig?.configuration ?? {}, null, 4)}</pre>
</CCardBody>

View File

@@ -1,4 +1,5 @@
import {
CAlert,
CButton,
CModal,
CModalHeader,
@@ -20,7 +21,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 { checkIfJson } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
@@ -29,6 +30,7 @@ import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
const ConfigureModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const { deviceSerialNumber } = useDevice();
const [hadSuccess, setHadSuccess] = useState(false);
const [hadFailure, setHadFailure] = useState(false);
@@ -91,10 +93,27 @@ const ConfigureModal = ({ show, toggleModal }) => {
{ headers },
)
.then(() => {
setHadSuccess(true);
addToast({
title: t('common.success'),
body: t('commands.command_success'),
color: 'success',
autohide: true,
});
toggleModal();
})
.catch(() => {
.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: split[1],
color: 'danger',
autohide: true,
});
}
}
setHadFailure(true);
})
.finally(() => {
@@ -189,11 +208,9 @@ const ConfigureModal = ({ show, toggleModal }) => {
/>
</CCol>
</CRow>
<div hidden={!hadSuccess && !hadFailure}>
<div>
<pre className="ignore">{responseBody}</pre>
</div>
</div>
<CAlert color="danger" hidden={!hadSuccess && !hadFailure}>
{responseBody}
</CAlert>
</CModalBody>
<CModalFooter>
<div hidden={!checkingIfSure}>Are you sure?</div>

View File

@@ -1,161 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { CModal, CModalHeader, CModalBody, CModalTitle, CPopover, CButton } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSave, cilX } from '@coreui/icons';
import { CreateUserForm, useFormFields, useAuth, useToast } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { testRegex, validateEmail } from 'utils/helper';
const initialState = {
name: {
value: '',
error: false,
optional: true,
},
email: {
value: '',
error: false,
},
currentPassword: {
value: '',
error: false,
},
changePassword: {
value: 'on',
error: false,
},
userRole: {
value: 'accounting',
error: false,
},
notes: {
value: '',
error: false,
optional: true,
},
description: {
value: '',
error: false,
optional: true,
},
};
const CreateUserModal = ({ show, toggle, getUsers, policies }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const [formFields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialState);
const toggleChange = () => {
updateField('changePassword', { value: !formFields.changePassword.value });
};
const createUser = () => {
setLoading(true);
const parameters = {
id: 0,
};
let validationSuccess = true;
for (const [key, value] of Object.entries(formFields)) {
if (!value.optional && value.value === '') {
validationSuccess = false;
updateField(key, { value: value.value, error: true });
} else if (key === 'currentPassword' && !testRegex(value.value, policies.passwordPattern)) {
validationSuccess = false;
updateField(key, { value: value.value, error: true });
} else if (key === 'email' && !validateEmail(value.value)) {
validationSuccess = false;
updateField(key, { value: value.value, error: true });
} else if (key === 'notes') {
parameters[key] = [{ note: value.value }];
} else if (key === 'changePassword') {
parameters[key] = value.value === 'on';
} else {
parameters[key] = value.value;
}
}
if (validationSuccess) {
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.post(`${endpoints.owsec}/api/v1/user/0`, parameters, {
headers,
})
.then(() => {
getUsers();
setFormFields(initialState);
addToast({
title: t('common.success'),
body: t('user.create_success'),
color: 'success',
autohide: true,
});
toggle();
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('user.create_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
};
useEffect(() => {
setFormFields(initialState);
}, [show]);
return (
<CModal show={show} onClose={toggle} size="xl">
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('user.create')}</CModalTitle>
<div className="text-right">
<CPopover content={t('user.create')}>
<CButton color="primary" variant="outline" onClick={createUser} disabled={loading}>
<CIcon content={cilSave} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<CreateUserForm
t={t}
fields={formFields}
updateField={updateFieldWithId}
policies={policies}
toggleChange={toggleChange}
/>
</CModalBody>
</CModal>
);
};
CreateUserModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
getUsers: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(CreateUserModal);

View File

@@ -0,0 +1,88 @@
import React, { useState } from 'react';
import ReactTooltip from 'react-tooltip';
import { v4 as createUuid } from 'uuid';
import { CButton, CCardBody, CCardHeader, CRow, CCol, CPopover, CButtonClose } from '@coreui/react';
import { cilTrash } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { LoadingButton } from 'ucentral-libs';
import PropTypes from 'prop-types';
import styles from './index.module.scss';
const DeleteButton = ({ t, config, deleteConfig, hideTooltips }) => {
const [tooltipId] = useState(createUuid());
return (
<CPopover content={t('common.delete')}>
<div className="d-inline">
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
data-tip
data-for={tooltipId}
data-event="click"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-trash" content={cilTrash} size="sm" />
</CButton>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left - tooltipWidth * 0.25;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{t('configuration.delete_config')}
<CButtonClose
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody className="py-1 px-4">
<CRow>
<CCol>
<LoadingButton
data-toggle="dropdown"
variant="outline"
color="danger"
label={t('common.confirm')}
isLoadingLabel={t('user.deleting')}
isLoading={false}
action={() => deleteConfig(config.name)}
block
disabled={false}
/>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</div>
</CPopover>
);
};
DeleteButton.propTypes = {
t: PropTypes.func.isRequired,
config: PropTypes.instanceOf(Object).isRequired,
deleteConfig: PropTypes.func.isRequired,
hideTooltips: PropTypes.func.isRequired,
};
export default DeleteButton;

View File

@@ -0,0 +1,184 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import {
CCardBody,
CDataTable,
CButton,
CCard,
CCardHeader,
CPopover,
CSelect,
CButtonToolbar,
} from '@coreui/react';
import { cilPencil, cilPlus } from '@coreui/icons';
import ReactTooltip from 'react-tooltip';
import CIcon from '@coreui/icons-react';
import { FormattedDate } from 'ucentral-libs';
import DeleteButton from './DeleteButton';
const DefaultConfigurationTable = ({
currentPage,
configurations,
toggleAddBlacklist,
toggleEditModal,
configurationsPerPage,
loading,
deleteConfig,
updateDevicesPerPage,
pageCount,
updatePage,
t,
}) => {
const columns = [
{ key: 'name', label: t('user.name'), _style: { width: '20%' } },
{ key: 'description', label: t('user.description'), _style: { width: '20%' } },
{ key: 'created', label: t('common.created'), _style: { width: '10%' } },
{ key: 'modified', label: t('common.modified'), _style: { width: '10%' } },
{ key: 'deviceTypes', label: t('firmware.device_types'), _style: { width: '20%' } },
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
];
const hideTooltips = () => ReactTooltip.hide();
const escFunction = (event) => {
if (event.keyCode === 27) {
hideTooltips();
}
};
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}, []);
return (
<>
<CCard className="m-0 p-0">
<CCardHeader className="dark-header text-right">
<div className="text-value-lg float-left">
{t('configuration.default_configurations')}
</div>
<div className="text-right float-right">
<CPopover content={t('configuration.create_config')}>
<CButton size="sm" color="info" onClick={toggleAddBlacklist}>
<CIcon content={cilPlus} />
</CButton>
</CPopover>
</div>
</CCardHeader>
<CCardBody className="p-0">
<CDataTable
addTableClasses="ignore-overflow table-sm"
items={configurations ?? []}
fields={columns}
hover
border
loading={loading}
scopedSlots={{
name: (item) => <td className="align-middle">{item.name}</td>,
description: (item) => <td className="align-middle">{item.description}</td>,
deviceTypes: (item) => <td className="align-middle">{item.modelIds.join(', ')}</td>,
created: (item) => (
<td className="align-middle">
<FormattedDate date={item.created} />
</td>
),
modified: (item) => (
<td className="align-middle">
<FormattedDate date={item.lastModified} />
</td>
),
actions: (item) => (
<td className="text-center align-middle">
<CButtonToolbar
role="group"
className="justify-content-center"
style={{ width: '90px' }}
>
<DeleteButton
t={t}
config={item}
deleteConfig={deleteConfig}
hideTooltips={hideTooltips}
/>
<CPopover content={t('common.edit')}>
<CButton
onClick={() => toggleEditModal(item.name)}
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
style={{ width: '33px', height: '30px' }}
>
<CIcon content={cilPencil} size="sm" />
</CButton>
</CPopover>
</CButtonToolbar>
</td>
),
}}
/>
<div className="d-flex flex-row pl-3">
<div className="pr-3">
<ReactPaginate
previousLabel="← Previous"
nextLabel="Next →"
pageCount={pageCount}
onPageChange={updatePage}
forcePage={Number(currentPage)}
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
nextClassName="page-item"
nextLinkClassName="page-link"
activeClassName="active"
/>
</div>
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
<div style={{ width: '100px' }} className="px-2">
<CSelect
custom
defaultValue={configurationsPerPage}
onChange={(e) => updateDevicesPerPage(e.target.value)}
disabled={loading}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</CSelect>
</div>
</div>
</CCardBody>
</CCard>
</>
);
};
DefaultConfigurationTable.propTypes = {
currentPage: PropTypes.string,
configurations: PropTypes.instanceOf(Array).isRequired,
toggleAddBlacklist: PropTypes.func.isRequired,
toggleEditModal: PropTypes.func.isRequired,
updateDevicesPerPage: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
updatePage: PropTypes.func.isRequired,
configurationsPerPage: PropTypes.string.isRequired,
deleteConfig: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
};
DefaultConfigurationTable.defaultProps = {
currentPage: '0',
};
export default React.memo(DefaultConfigurationTable);

View File

@@ -0,0 +1,32 @@
.firmwareTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 400px;
}
.deleteTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 150px;
}
.tooltipHeader {
padding-left: 5px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
border-top-left-radius: 1rem !important;
border-top-right-radius: 1rem !important;
}

View File

@@ -0,0 +1,199 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import axiosInstance from 'utils/axiosInstance';
import { getItem, setItem } from 'utils/localStorageHelper';
import { useAuth, useToast, useToggle } from 'ucentral-libs';
import AddConfigurationModal from 'components/AddConfigurationModal';
import EditConfigurationModal from 'components/EditConfigurationModal';
import Table from './Table';
const DefaultConfigurationTable = () => {
const { t } = useTranslation();
const { addToast } = useToast();
const history = useHistory();
const [page, setPage] = useState(parseInt(sessionStorage.getItem('configurationTable') ?? 0, 10));
const { currentToken, endpoints } = useAuth();
const [configurationCount, setConfigurationCount] = useState(0);
const [pageCount, setPageCount] = useState(0);
const [configurationsPerPage, setConfigurationsPerPage] = useState(
getItem('configurationsPerPage') || '10',
);
const [configurations, setConfigurations] = useState([]);
const [loading, setLoading] = useState(true);
const [editId, setEditId] = useState('');
const [showEditModal, setShowEditModal] = useState(false);
const [showAddModal, toggleAddModal] = useToggle(false);
const toggleEditModal = (serialNumber) => {
if (serialNumber) setEditId(serialNumber);
setShowEditModal(!showEditModal);
};
const getConfigurationInformation = (
selectedPage = page,
configurationPerPage = configurationsPerPage,
) => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.owgw}/api/v1/default_configurations?limit=${configurationPerPage}&offset=${
configurationPerPage * selectedPage
}`,
options,
)
.then((response) => {
setConfigurations(response.data.configurations);
setLoading(false);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('configuration.error_fetching_configurations', {
error: e.response?.data?.ErrorDescription,
}),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
const getCount = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/default_configurations?countOnly=true`, {
headers,
})
.then((response) => {
const configurationsCount = response.data.count;
const pagesCount = Math.ceil(configurationsCount / configurationsPerPage);
setPageCount(pagesCount);
setConfigurationCount(configurationsCount);
let selectedPage = page;
if (page >= pagesCount) {
history.push(`/defaultconfigurations?page=${pagesCount - 1}`);
selectedPage = pagesCount - 1;
}
getConfigurationInformation(selectedPage);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('configuration.error_fetching_configurations', {
error: e.response?.data?.ErrorDescription,
}),
color: 'danger',
autohide: true,
});
setLoading(false);
});
};
const updateConfigurationsPerPage = (value) => {
setItem('configurationsPerPage', value);
setConfigurationsPerPage(value);
const newPageCount = Math.ceil(configurationCount / value);
setPageCount(newPageCount);
let selectedPage = page;
if (page >= newPageCount) {
history.push(`/default_configurations?page=${newPageCount - 1}`);
selectedPage = newPageCount - 1;
}
getConfigurationInformation(selectedPage, value);
};
const updatePageCount = ({ selected: selectedPage }) => {
sessionStorage.setItem('configurationTable', selectedPage);
setPage(selectedPage);
getConfigurationInformation(selectedPage);
};
const deleteConfig = (name) => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.delete(`${endpoints.owgw}/api/v1/default_configuration/${name}`, { headers })
.then(() => {
addToast({
title: t('common.success'),
body: t('configuration.successful_delete'),
color: 'success',
autohide: true,
});
getCount();
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('configuration.error_adding_blacklist', {
error: e.response?.data?.ErrorDescription,
}),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
getCount();
}, []);
return (
<div>
<Table
currentPage={page}
t={t}
configurations={configurations}
loading={loading}
toggleAddBlacklist={toggleAddModal}
toggleEditModal={toggleEditModal}
updateConfigurationsPerPage={updateConfigurationsPerPage}
configurationsPerPage={configurationsPerPage}
pageCount={pageCount}
updatePage={updatePageCount}
pageRangeDisplayed={5}
deleteConfig={deleteConfig}
/>
{showAddModal ? (
<AddConfigurationModal show={showAddModal} toggle={toggleAddModal} refresh={getCount} />
) : null}
<EditConfigurationModal
show={showEditModal}
toggle={toggleEditModal}
refresh={getCount}
configId={editId}
/>
</div>
);
};
export default DefaultConfigurationTable;

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { CButton, CCard, CCardHeader, CCardBody, CRow, CCol } from '@coreui/react';
import axiosInstance from 'utils/axiosInstance';
import { LoadingButton, useAuth, useDevice, useToast } from 'ucentral-libs';
import { LoadingButton, useAuth, useDevice, useToast, useToggle } from 'ucentral-libs';
import RebootModal from 'components/RebootModal';
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
import ConfigureModal from 'components/ConfigureModal';
@@ -13,7 +14,7 @@ import FactoryResetModal from 'components/FactoryResetModal';
import EventQueueModal from 'components/EventQueueModal';
import TelemetryModal from 'components/TelemetryModal';
const DeviceActions = () => {
const DeviceActions = ({ device }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
@@ -21,35 +22,16 @@ const DeviceActions = () => {
const [upgradeStatus, setUpgradeStatus] = useState({
loading: false,
});
const [device, setDevice] = useState({});
const [showRebootModal, setShowRebootModal] = useState(false);
const [showBlinkModal, setShowBlinkModal] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [showTraceModal, setShowTraceModal] = useState(false);
const [showScanModal, setShowScanModal] = useState(false);
const [connectLoading, setConnectLoading] = useState(false);
const [showConfigModal, setConfigModal] = useState(false);
const [showFactoryModal, setShowFactoryModal] = useState(false);
const [showQueueModal, setShowQueueModal] = useState(false);
const [showTelemetryModal, setShowTelemetryModal] = useState(false);
const toggleRebootModal = () => setShowRebootModal(!showRebootModal);
const toggleBlinkModal = () => setShowBlinkModal(!showBlinkModal);
const toggleUpgradeModal = () => setShowUpgradeModal(!showUpgradeModal);
const toggleTraceModal = () => setShowTraceModal(!showTraceModal);
const toggleScanModal = () => setShowScanModal(!showScanModal);
const toggleConfigModal = () => setConfigModal(!showConfigModal);
const toggleFactoryResetModal = () => setShowFactoryModal(!showFactoryModal);
const toggleQueueModal = () => setShowQueueModal(!showQueueModal);
const toggleTelemetryModal = () => setShowTelemetryModal(!showTelemetryModal);
const [showRebootModal, toggleRebootModal] = useToggle(false);
const [showBlinkModal, toggleBlinkModal] = useToggle(false);
const [showUpgradeModal, toggleUpgradeModal, setShowUpgradeModal] = useToggle(false);
const [showTraceModal, toggleTraceModal] = useToggle(false);
const [showScanModal, toggleScanModal] = useToggle(false);
const [showConfigModal, toggleConfigModal] = useToggle(false);
const [showFactoryModal, toggleFactoryResetModal] = useToggle(false);
const [showQueueModal, toggleQueueModal] = useToggle(false);
const [showTelemetryModal, toggleTelemetryModal] = useToggle(false);
const getRttysInfo = () => {
setConnectLoading(true);
@@ -67,40 +49,31 @@ const DeviceActions = () => {
)
.then((response) => {
const url = `https://${response.data.server}:${response.data.viewport}/connect/${response.data.connectionId}`;
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
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);
});
};
const getDeviceInformation = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}`, options)
.then((response) => {
setDevice(response.data);
})
.catch(() => {});
};
useEffect(() => {
if (upgradeStatus.result !== undefined) {
if (upgradeStatus.result.success) {
addToast({
title: upgradeStatus.result.success ? t('common.success') : t('common.error'),
body: upgradeStatus.result.success
@@ -109,17 +82,14 @@ const DeviceActions = () => {
color: upgradeStatus.result.success ? 'success' : 'danger',
autohide: true,
});
setShowUpgradeModal(false);
}
setUpgradeStatus({
loading: false,
});
setShowUpgradeModal(false);
}
}, [upgradeStatus]);
useEffect(() => {
getDeviceInformation();
}, [deviceSerialNumber]);
return (
<CCard>
<CCardHeader className="dark-header">
@@ -128,36 +98,41 @@ const DeviceActions = () => {
<CCardBody>
<CRow>
<CCol>
<CButton block onClick={toggleRebootModal} color="primary">
<CButton block disabled={device === null} onClick={toggleRebootModal} color="primary">
{t('actions.reboot')}
</CButton>
</CCol>
<CCol>
<CButton block onClick={toggleBlinkModal} color="primary">
<CButton block disabled={device === null} onClick={toggleBlinkModal} color="primary">
{t('actions.blink')}
</CButton>
</CCol>
</CRow>
<CRow className="my-1">
<CCol>
<CButton block color="primary" onClick={toggleUpgradeModal}>
<CButton block disabled={device === null} color="primary" onClick={toggleUpgradeModal}>
{t('actions.firmware_upgrade')}
</CButton>
</CCol>
<CCol>
<CButton block color="primary" onClick={toggleTraceModal}>
<CButton block disabled={device === null} color="primary" onClick={toggleTraceModal}>
{t('actions.trace')}
</CButton>
</CCol>
</CRow>
<CRow className="my-1">
<CCol>
<CButton block color="primary" onClick={toggleScanModal}>
<CButton block disabled={device === null} color="primary" onClick={toggleScanModal}>
{t('actions.wifi_scan')}
</CButton>
</CCol>
<CCol>
<CButton block color="primary" onClick={toggleFactoryResetModal}>
<CButton
block
disabled={device === null}
color="primary"
onClick={toggleFactoryResetModal}
>
{t('actions.factory_reset')}
</CButton>
</CCol>
@@ -165,6 +140,7 @@ const DeviceActions = () => {
<CRow className="my-1">
<CCol>
<LoadingButton
disabled={device === null}
isLoading={connectLoading}
label={t('actions.connect')}
isLoadingLabel={t('actions.connecting')}
@@ -172,19 +148,24 @@ const DeviceActions = () => {
/>
</CCol>
<CCol>
<CButton block color="primary" onClick={toggleConfigModal}>
<CButton block disabled={device === null} color="primary" onClick={toggleConfigModal}>
{t('actions.configure')}
</CButton>
</CCol>
</CRow>
<CRow className="my-1">
<CCol>
<CButton block color="primary" onClick={toggleQueueModal}>
<CButton block disabled={device === null} color="primary" onClick={toggleQueueModal}>
{t('commands.event_queue')}
</CButton>
</CCol>
<CCol>
<CButton block color="primary" onClick={toggleTelemetryModal}>
<CButton
block
disabled={device === null}
color="primary"
onClick={toggleTelemetryModal}
>
{t('actions.telemetry')}
</CButton>
</CCol>
@@ -212,4 +193,12 @@ const DeviceActions = () => {
);
};
DeviceActions.propTypes = {
device: PropTypes.instanceOf(Object),
};
DeviceActions.defaultProps = {
device: null,
};
export default DeviceActions;

View File

@@ -0,0 +1,434 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CCard,
CCardBody,
CCardHeader,
CCol,
CPopover,
CRow,
CSpinner,
CWidgetIcon,
} from '@coreui/react';
import { CChartBar, CChartHorizontalBar, CChartPie } from '@coreui/react-chartjs';
import { cilClock, cilInfo, cilMedicalCross, cilThumbUp, cilWarning } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { FormattedDate } from 'ucentral-libs';
import styles from './index.module.scss';
const getColor = (health) => {
const numberHealth = health ? Number(health.replace('%', '')) : 0;
if (numberHealth >= 90) return 'success';
if (numberHealth >= 60) return 'warning';
return 'danger';
};
const getIcon = (health) => {
const numberHealth = health ? Number(health.replace('%', '')) : 0;
if (numberHealth >= 90) return <CIcon width={36} name="cil-thumbs-up" content={cilThumbUp} />;
if (numberHealth >= 60) return <CIcon width={36} name="cil-warning" content={cilWarning} />;
return <CIcon width={36} name="cil-medical-cross" content={cilMedicalCross} />;
};
const DeviceDashboard = ({ t, data, loading }) => (
<div style={{ position: 'relative' }}>
<div style={{ opacity: loading ? '20%' : '100%' }}>
<CRow className="mt-3">
<CCol>
<CWidgetIcon
text={t('common.last_dashboard_refresh')}
header={data.snapshot ? <FormattedDate date={data.snapshot} size="lg" /> : <h2>-</h2>}
color="info"
iconPadding={false}
>
<CIcon width={36} name="cil-clock" content={cilClock} />
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={
<div>
<div className="float-left">{t('common.overall_health')}</div>
<div className="float-left ml-2">
<CPopover content={t('device.health_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
}
header={<h2>{data.overallHealth}</h2>}
color={getColor(data.overallHealth)}
iconPadding={false}
>
{getIcon(data.overallHealth)}
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={
<div>
<div className="float-left">{t('common.devices')}</div>
<div className="float-left ml-2">
<CPopover content={t('device.count_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
}
header={<h2>{data.numberOfDevices}</h2>}
color="primary"
iconPadding={false}
>
<CIcon width={36} name="cil-router" />
</CWidgetIcon>
</CCol>
</CRow>
<CRow>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">{t('common.device_status')}</CCardHeader>
<CCardBody className="p-1">
<CChartPie
datasets={data.status.datasets}
labels={data.status.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} devices, (${
data.statusDevices[ds.datasets[0].data[item.index]]
}%)`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">
<div>
<div className="float-left">{t('common.device_health')}</div>
<div className="float-left ml-2">
<CPopover content={t('device.health_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<CChartPie
datasets={data.healths.datasets}
labels={data.healths.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} connected devices (${
data.healthDevices[ds.datasets[0].data[item.index]]
}%)`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">
{data.totalAssociations}{' '}
{data.totalAssociations === 1
? t('wifi_analysis.association')
: t('wifi_analysis.associations')}
</CCardHeader>
<CCardBody className="p-1">
<CChartPie
datasets={data.associations.datasets}
labels={data.associations.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} associations (${
data.associationData[ds.datasets[0].data[item.index]]
}%)`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">{t('common.vendors')}</CCardHeader>
<CCardBody className="p-1">
<CChartHorizontalBar
datasets={data.vendors.datasets}
labels={data.vendors.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
},
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
scales: {
xAxes: [
{
ticks: {
maxTicksLimit: 5,
beginAtZero: true,
stepSize: 1,
},
},
],
yAxes: [
{
ticks: {
callback: (value) => value.split(' ')[0],
},
},
],
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">{t('firmware.device_types')}</CCardHeader>
<CCardBody className="p-1">
<CChartPie
datasets={data.deviceType.datasets}
labels={data.deviceType.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">
<div>
<div className="float-left">{t('common.uptimes')}</div>
<div className="float-left ml-2">
<CPopover content={t('device.uptimes_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<CChartBar
datasets={data.upTimes.datasets}
labels={data.upTimes.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
},
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
scales: {
yAxes: [
{
ticks: {
maxTicksLimit: 5,
beginAtZero: true,
stepSize: 1,
},
},
],
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">
<div>
<div className="float-left">{t('common.certificates')}</div>
<div className="float-left ml-2">
<CPopover content={t('device.certificate_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<CChartPie
datasets={data.certificates.datasets}
labels={data.certificates.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} connected devices (${
data.certificateData[ds.datasets[0].data[item.index]]
}%)`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">{t('common.commands')}</CCardHeader>
<CCardBody className="p-1">
<CChartBar
datasets={data.commands.datasets}
labels={data.commands.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
scales: {
yAxes: [
{
ticks: {
maxTicksLimit: 5,
beginAtZero: true,
stepSize: 1,
},
},
],
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol lg="6" xl="4">
<CCard>
<CCardHeader className="dark-header">
<div>
<div className="float-left">{t('common.memory_used')}</div>
<div className="float-left ml-2">
<CPopover content={t('device.memory_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<CChartBar
datasets={data.memoryUsed.datasets}
labels={data.memoryUsed.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
scales: {
yAxes: [
{
ticks: {
maxTicksLimit: 10,
beginAtZero: true,
stepSize: 1,
},
},
],
},
}}
/>
</CCardBody>
</CCard>
</CCol>
</CRow>
</div>
{loading ? (
<div className={styles.centerContainer}>
<CSpinner className={styles.spinner} />
</div>
) : null}
</div>
);
DeviceDashboard.propTypes = {
t: PropTypes.func.isRequired,
data: PropTypes.instanceOf(Object).isRequired,
loading: PropTypes.bool.isRequired,
};
export default React.memo(DeviceDashboard);

View File

@@ -0,0 +1,10 @@
.centerContainer {
position: absolute;
top: 5%;
right: 50%;
}
.spinner {
height: 50px;
width: 50px;
}

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { DeviceDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
import { useAuth, COLOR_LIST } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import Dashboard from './Dashboard';
const DeviceDashboard = () => {
const { t } = useTranslation();
@@ -54,9 +55,12 @@ const DeviceDashboard = () => {
const statusColors = [];
const statusLabels = [];
let totalDevices = parsedData.status.reduce((acc, point) => acc + point.value, 0);
parsedData.numberOfDevices = totalDevices;
parsedData.statusDevices = {};
for (const point of parsedData.status) {
statusDs.push(Math.round((point.value / totalDevices) * 100));
statusDs.push(point.value);
statusLabels.push(point.tag);
parsedData.statusDevices[point.value] = Math.round((point.value / totalDevices) * 100);
let color = '';
switch (point.tag) {
case 'connected':
@@ -95,7 +99,7 @@ const DeviceDashboard = () => {
const healthLabels = [];
totalDevices = parsedData.healths.reduce((acc, point) => acc + point.value, 0);
for (const point of parsedData.healths) {
healthDs.push(Math.round((point.value / totalDevices) * 100));
healthDs.push(point.value);
healthLabels.push(point.tag);
let color = '';
switch (point.tag) {
@@ -121,6 +125,12 @@ const DeviceDashboard = () => {
}
healthColors.push(color);
}
parsedData.healthDevices = {
[devicesAt100]: Math.round((devicesAt100 / totalDevices) * 100),
[devicesUp90]: Math.round((devicesUp90 / totalDevices) * 100),
[devicesUp60]: Math.round((devicesUp60 / totalDevices) * 100),
[devicesDown60]: Math.round((devicesDown60 / totalDevices) * 100),
};
parsedData.healths = {
datasets: [
{
@@ -143,10 +153,12 @@ const DeviceDashboard = () => {
const associationsColors = [];
const associationsLabels = [];
const totalAssociations = parsedData.associations.reduce((acc, point) => acc + point.value, 0);
parsedData.associationData = {};
for (let i = 0; i < parsedData.associations.length; i += 1) {
const point = parsedData.associations[i];
associationsDs.push(Math.round((point.value / totalAssociations) * 100));
associationsDs.push(point.value);
associationsLabels.push(point.tag);
parsedData.associationData[point.value] = Math.round((point.value / totalAssociations) * 100);
switch (parsedData.associations[i].tag) {
case '2G':
@@ -257,8 +269,10 @@ const DeviceDashboard = () => {
const certificatesColors = [];
const certificatesLabels = [];
const totalCerts = parsedData.certificates.reduce((acc, point) => acc + point.value, 0);
parsedData.certificateData = {};
for (const point of parsedData.certificates) {
certificatesDs.push(Math.round((point.value / totalCerts) * 100));
certificatesDs.push(point.value);
parsedData.certificateData[point.value] = Math.round((point.value / totalCerts) * 100);
certificatesLabels.push(point.tag);
let color = '';
switch (point.tag) {

View File

@@ -0,0 +1,145 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
CButton,
CDataTable,
CModal,
CModalHeader,
CModalTitle,
CModalBody,
CRow,
CCol,
CInput,
CPopover,
CSwitch,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
import { LoadingButton } from 'ucentral-libs';
import { cleanBytesString, prettyDate } from 'utils/helper';
const DeviceFirmwareModal = ({
t,
device,
show,
toggle,
firmwareVersions,
upgradeToVersion,
loading,
upgradeStatus,
keepRedirector,
toggleRedirector,
}) => {
const [filter, setFilter] = useState('');
const fields = [
{ key: 'imageDate', label: t('firmware.image_date'), _style: { width: '17%' }, filter: false },
{ key: 'size', label: t('firmware.size'), _style: { width: '8%' }, filter: false },
{ key: 'revision', label: t('firmware.revision'), _style: { width: '60%' } },
{ key: 'show_details', label: '', _style: { width: '15%' }, filter: false },
];
useEffect(() => {
setFilter('');
}, [show]);
return (
<CModal show={show} onClose={toggle} size="xl">
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">#{device?.serialNumber}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
{show ? (
<div>
<CRow>
<CCol sm="2" className="pt-2">
{t('firmware.installed_firmware')}
</CCol>
<CCol className="pt-2">{device.firmware}</CCol>
</CRow>
<CRow className="mt-3">
<CCol sm="2" className="pt-2">
{t('factory_reset.redirector')}
</CCol>
<CCol className="pt-2">
<CSwitch
color="primary"
defaultChecked={keepRedirector}
onClick={toggleRedirector}
labelOn="Yes"
labelOff="No"
/>
</CCol>
</CRow>
<CRow className="my-4">
<CCol sm="5">
<CInput
type="text"
placeholder="Search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</CCol>
<CCol />
</CRow>
<CRow className="mb-4">
<CCol>
<div className="overflow-auto" style={{ height: '600px' }}>
<CDataTable
addTableClasses="table-sm"
items={firmwareVersions}
fields={fields}
loading={loading}
hover
tableFilterValue={filter}
border
scopedSlots={{
imageDate: (item) => <td>{prettyDate(item.imageDate)}</td>,
size: (item) => <td>{cleanBytesString(item.size)}</td>,
show_details: (item) => (
<td className="text-center">
<LoadingButton
label={t('firmware.upgrade')}
isLoadingLabel={t('firmware.upgrading')}
isLoading={false}
action={() => upgradeToVersion(item.uri)}
block={false}
disabled={upgradeStatus.loading}
/>
</td>
),
}}
/>
</div>
</CCol>
</CRow>
</div>
) : (
<div />
)}
</CModalBody>
</CModal>
);
};
DeviceFirmwareModal.propTypes = {
t: PropTypes.func.isRequired,
device: PropTypes.instanceOf(Object).isRequired,
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
firmwareVersions: PropTypes.instanceOf(Array).isRequired,
upgradeToVersion: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
keepRedirector: PropTypes.bool.isRequired,
toggleRedirector: PropTypes.func.isRequired,
};
export default React.memo(DeviceFirmwareModal);

View File

@@ -1,9 +1,11 @@
/* eslint-disable no-await-in-loop */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { DeviceFirmwareModal as Modal, useAuth, useToast } from 'ucentral-libs';
import { useAuth, useToast, useToggle } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { useTranslation } from 'react-i18next';
import { useGlobalWebSocket } from 'contexts/WebSocketProvider';
import Modal from './Modal';
const DeviceFirmwareModal = ({
device,
@@ -17,6 +19,8 @@ const DeviceFirmwareModal = ({
const { currentToken, endpoints } = useAuth();
const [loading, setLoading] = useState(false);
const [firmwareVersions, setFirmwareVersions] = useState([]);
const [keepRedirector, toggleKeepRedirector, setKeepRedirector] = useToggle(true);
const { addDeviceListener } = useGlobalWebSocket();
const getPartialFirmware = async (offset) => {
const headers = {
@@ -78,6 +82,7 @@ const DeviceFirmwareModal = ({
const parameters = {
serialNumber: device.serialNumber,
keepRedirector,
when: 0,
uri,
};
@@ -87,6 +92,17 @@ const DeviceFirmwareModal = ({
headers,
})
.then((response) => {
addDeviceListener({
serialNumber: device.serialNumber,
types: ['device_firmware_upgrade'],
addToast: (title, body) =>
addToast({
title,
body,
color: 'info',
autohide: true,
}),
});
setUpgradeStatus({
loading: false,
result: {
@@ -95,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: {
@@ -108,6 +135,7 @@ const DeviceFirmwareModal = ({
useEffect(() => {
if (show && device.compatible) getFirmwareList();
if (show) setKeepRedirector(true);
}, [device, show]);
return (
@@ -120,6 +148,8 @@ const DeviceFirmwareModal = ({
upgradeToVersion={upgradeToVersion}
loading={loading}
upgradeStatus={upgradeStatus}
keepRedirector={keepRedirector}
toggleRedirector={toggleKeepRedirector}
/>
);
};

View File

@@ -1,17 +1,17 @@
/* eslint-disable-rule prefer-destructuring */
import React, { useState, useEffect } from 'react';
import {
CWidgetDropdown,
CCardBody,
CButton,
CDataTable,
CCard,
CRow,
CCol,
CProgress,
CCardHeader,
CPopover,
CCard,
CFormText,
CBadge,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilTrash } from '@coreui/icons';
import { cilSync, cilTrash } from '@coreui/icons';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import { dateToUnix } from 'utils/helper';
@@ -27,7 +27,9 @@ const DeviceHealth = () => {
const [loading, setLoading] = useState(false);
const [healthChecks, setHealthChecks] = useState([]);
const [start, setStart] = useState('');
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState('');
const [endError, setEndError] = useState(false);
const [logLimit, setLogLimit] = useState(25);
const [loadingMore, setLoadingMore] = useState(false);
const [showLoadingMore, setShowLoadingMore] = useState(true);
@@ -40,13 +42,26 @@ const DeviceHealth = () => {
};
const modifyStart = (value) => {
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
} catch (e) {
setEnd('');
setEndError(true);
}
};
const showMoreLogs = () => {
setLogLimit(logLimit + 50);
};
@@ -128,14 +143,14 @@ const DeviceHealth = () => {
const tempSanityLevel = sortedHealthchecks[healthChecks.length - 1].sanity;
setSanityLevel(tempSanityLevel);
if (tempSanityLevel === 100) {
setBarColor('gradient-success');
setBarColor('success');
} else if (tempSanityLevel >= 90) {
setBarColor('gradient-warning');
setBarColor('warning');
} else {
setBarColor('gradient-danger');
setBarColor('danger');
}
} else {
setBarColor('gradient-dark');
setBarColor('dark');
}
}, [healthChecks]);
@@ -156,30 +171,61 @@ const DeviceHealth = () => {
}, []);
return (
<CWidgetDropdown
className="m-0"
header={t('health.title')}
text={sanityLevel ? `${sanityLevel}%` : t('common.unknown')}
value={sanityLevel ?? 100}
color={barColor}
inverse="true"
footerSlot={
<div className="pb-1 px-3">
<CProgress className="mb-3" color="white" value={sanityLevel ?? 0} />
<CRow className="mb-3">
<CCol>
{t('common.from')}
:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
</CCol>
<CCol>
{t('common.to')}
:
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard className="p-0">
<div className="overflow-auto" style={{ height: '200px' }}>
<CCard className="m-0">
<CCardHeader className="dark-header">
<div className="float-left align-middle pt-1">
<h4>
<CBadge color={barColor} className="my-0">
{sanityLevel ? `${sanityLevel}%` : `${t('common.unknown')} Sanity Level`}
</CBadge>
</h4>
</div>
<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={getDeviceHealth}
disabled={startError || endError}
>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
<div className="px-2">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm" color="danger">
<CIcon name="cil-trash" content={cilTrash} />
</CButton>
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
@@ -214,25 +260,15 @@ const DeviceHealth = () => {
/>
</div>
)}
</div>
</CCard>
<DeleteLogModal
serialNumber={deviceSerialNumber}
object="healthchecks"
object="logs"
show={showDeleteModal}
toggle={toggleDeleteModal}
/>
</div>
}
>
<div className="text-right float-right">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm">
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
</CButton>
</CPopover>
</div>
</CWidgetDropdown>
</CCardBody>
</CCard>
);
};

View File

@@ -0,0 +1,466 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import {
CCardBody,
CDataTable,
CButton,
CLink,
CCard,
CCardHeader,
CRow,
CCol,
CPopover,
CSelect,
CButtonClose,
} from '@coreui/react';
import {
cilSync,
cilArrowCircleTop,
cilCheckCircle,
cilTerminal,
cilTrash,
cilSearch,
} from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import ReactTooltip from 'react-tooltip';
import { v4 as createUuid } from 'uuid';
import { cleanBytesString } from 'utils/helper';
import { DeviceBadge, LoadingButton } from 'ucentral-libs';
import ReactCountryFlag from 'react-country-flag';
import styles from './index.module.scss';
const DeviceListTable = ({
currentPage,
devices,
searchBar,
devicesPerPage,
loading,
updateDevicesPerPage,
pageCount,
updatePage,
refreshDevice,
t,
toggleFirmwareModal,
toggleHistoryModal,
upgradeToLatest,
upgradeStatus,
deviceIcons,
connectRtty,
deleteDevice,
deleteStatus,
}) => {
const columns = [
{ key: 'deviceType', label: '', filter: false, sorter: false, _style: { width: '1%' } },
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
{ key: 'firmware', label: t('firmware.revision') },
{ key: 'firmware_button', label: '', filter: false, _style: { width: '1%' } },
{ key: 'compatible', label: t('common.type'), filter: false, _style: { width: '13%' } },
{ key: 'txBytes', label: 'Tx', filter: false, _style: { width: '14%' } },
{ key: 'rxBytes', label: 'Rx', filter: false, _style: { width: '14%' } },
{ key: 'ipAddress', label: t('IP'), _style: { width: '10%' } },
{ key: 'twoG', label: t('2G'), _style: { width: '10%' } },
{ key: 'fiveG', label: t('5G'), _style: { width: '10%' } },
{ key: 'actions', label: t('actions.actions'), _style: { width: '10%' } },
];
const hideTooltips = () => ReactTooltip.hide();
const escFunction = (event) => {
if (event.keyCode === 27) {
hideTooltips();
}
};
const getShortRevision = (revision) => {
if (revision.includes(' / ')) {
return revision.split(' / ')[1];
}
return revision;
};
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}, []);
const getFirmwareButton = (latest, device) => {
const tooltipId = createUuid();
let text = t('firmware.unknown_firmware_status');
let upgradeText = t('firmware.upgrade_to_latest');
let icon = <CIcon name="cil-arrow-circle-top" content={cilArrowCircleTop} />;
let color = 'secondary';
if (latest !== undefined) {
text = t('firmware.newer_firmware_available');
color = 'warning';
if (latest) {
icon = <CIcon name="cil-check-circle" content={cilCheckCircle} />;
text = t('firmware.latest_version_installed');
upgradeText = t('firmware.reinstall_latest');
color = 'success';
}
}
return (
<div>
<CButton size="sm" color={color} data-tip data-for={tooltipId} data-event="click">
{icon}
</CButton>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.firmwareTooltip, 'tooltipLeft'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left + tooltipWidth * 0.25;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{text}
<CButtonClose
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody>
<CRow>
<CCol>
<LoadingButton
variant="outline"
label={upgradeText}
isLoadingLabel={t('firmware.upgrading')}
isLoading={upgradeStatus.loading}
action={() => upgradeToLatest(device)}
block
disabled={
upgradeStatus.loading && upgradeStatus.serialNumber === device.serialNumber
}
/>
</CCol>
<CCol>
<CButton
block
variant="outline"
color="primary"
onClick={() => {
toggleFirmwareModal(device);
}}
>
{t('firmware.choose_custom')}
</CButton>
</CCol>
<CCol>
<CButton
block
variant="outline"
color="primary"
onClick={() => {
toggleHistoryModal(device);
}}
>
{t('firmware.history_title')}
</CButton>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</div>
);
};
const deleteButton = (serialNumber) => {
const tooltipId = createUuid();
return (
<>
<CPopover content={t('common.delete_device')}>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1 d-inline"
data-tip
data-for={tooltipId}
data-event="click"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-trash" content={cilTrash} size="sm" />
</CButton>
</CPopover>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left - tooltipWidth * 0.25;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{t('common.device_delete', { serialNumber })}
<CButtonClose
className="p-0 mb-1"
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody>
<CRow>
<CCol>
<LoadingButton
data-toggle="dropdown"
variant="outline"
color="danger"
label={t('common.confirm')}
isLoadingLabel={t('user.deleting')}
isLoading={deleteStatus.loading}
action={(e) => {
e.target.parentNode.parentNode.parentNode.parentNode.classList.remove('show');
hideTooltips();
deleteDevice(serialNumber);
}}
block
disabled={deleteStatus.loading}
/>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</>
);
};
return (
<>
<CCard className="m-0 p-0">
<CCardHeader className="p-0">
<div className="float-left" style={{ width: '400px' }}>
{searchBar}
</div>
</CCardHeader>
<CCardBody className="p-0">
<CDataTable
addTableClasses="ignore-overflow table-sm"
items={devices ?? []}
fields={columns}
hover
border
loading={loading}
scopedSlots={{
deviceType: (item) => (
<td className="align-middle text-center">
<DeviceBadge t={t} device={item} deviceIcons={deviceIcons} />
</td>
),
serialNumber: (item) => (
<td className="text-center align-middle">
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
{item.serialNumber}
</CLink>
</td>
),
firmware: (item) => (
<td className="align-middle">
<CPopover
content={item.firmware ? item.firmware : t('common.na')}
placement="top"
>
<div style={{ width: 'calc(10vw)' }} className="text-truncate align-middle">
{getShortRevision(item.firmware)}
</div>
</CPopover>
</td>
),
firmware_button: (item) => (
<td className="text-center align-middle">
{item.firmwareInfo
? getFirmwareButton(item.firmwareInfo.latest, item)
: getFirmwareButton(undefined, item)}
</td>
),
compatible: (item) => (
<td className="align-middle">
<CPopover
content={item.compatible ? item.compatible : t('common.na')}
placement="top"
>
<div style={{ width: 'calc(10vw)' }} className="text-truncate align-middle">
{item.compatible}
</div>
</CPopover>
</td>
),
txBytes: (item) => <td className="align-middle">{cleanBytesString(item.txBytes)}</td>,
rxBytes: (item) => <td className="align-middle">{cleanBytesString(item.rxBytes)}</td>,
ipAddress: (item) => (
<td className="align-middle">
<CPopover
content={`${item.locale !== '' ? `${item.locale} - ` : ''}${item.ipAddress}`}
placement="top"
>
<div style={{ width: 'calc(8vw)' }} className="text-truncate align-middle">
{item.locale !== '' && item.ipAddress !== '' && (
<ReactCountryFlag
style={{ width: '24px', height: '24px' }}
countryCode={item?.locale}
svg
/>
)}
{` ${item.ipAddress}`}
</div>
</CPopover>
</td>
),
twoG: (item) => <td className="align-middle">{item.associations_2G ?? 0}</td>,
fiveG: (item) => <td className="align-middle">{item.associations_5G ?? 0}</td>,
actions: (item) => (
<td className="text-center align-middle">
<div role="group" className="justify-content-center" style={{ width: '190px' }}>
<CPopover content={t('actions.connect')}>
<CButton
className="mx-1 d-inline"
color="primary"
variant="outline"
shape="square"
size="sm"
onClick={() => connectRtty(item.serialNumber)}
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-terminal" content={cilTerminal} size="sm" />
</CButton>
</CPopover>
{deleteButton(item.serialNumber)}
<CPopover content={t('configuration.details')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1 d-inline"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-search" content={cilSearch} size="sm" />
</CButton>
</CLink>
</CPopover>
<CPopover content={t('common.refresh_device')}>
<CButton
onClick={() => refreshDevice(item.serialNumber)}
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1 d-inline"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-sync" content={cilSync} size="sm" />
</CButton>
</CPopover>
</div>
</td>
),
}}
/>
<div className="d-flex flex-row pl-3">
<div className="pr-3">
<ReactPaginate
previousLabel="← Previous"
nextLabel="Next →"
pageCount={pageCount}
onPageChange={updatePage}
forcePage={Number(currentPage)}
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
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>
<div style={{ width: '100px' }} className="px-2">
<CSelect
custom
defaultValue={devicesPerPage}
onChange={(e) => updateDevicesPerPage(e.target.value)}
disabled={loading}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</CSelect>
</div>
</div>
</CCardBody>
</CCard>
</>
);
};
DeviceListTable.propTypes = {
currentPage: PropTypes.oneOf(['string', 'number']),
devices: PropTypes.instanceOf(Array).isRequired,
searchBar: PropTypes.node.isRequired,
updateDevicesPerPage: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
updatePage: PropTypes.func.isRequired,
devicesPerPage: PropTypes.string.isRequired,
refreshDevice: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
toggleFirmwareModal: PropTypes.func.isRequired,
toggleHistoryModal: PropTypes.func.isRequired,
upgradeToLatest: PropTypes.func.isRequired,
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
deviceIcons: PropTypes.instanceOf(Object).isRequired,
connectRtty: PropTypes.func.isRequired,
deleteDevice: PropTypes.func.isRequired,
deleteStatus: PropTypes.instanceOf(Object).isRequired,
};
DeviceListTable.defaultProps = {
currentPage: '0',
};
export default React.memo(DeviceListTable);

View File

@@ -0,0 +1,30 @@
.firmwareTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 400px;
}
.deleteTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
width: 200px;
}
.tooltipHeader {
padding-left: 5px;
padding-right: 10px;
border-top-left-radius: 1rem !important;
border-top-right-radius: 1rem !important;
}

View File

@@ -6,7 +6,9 @@ import { getItem, setItem } from 'utils/localStorageHelper';
import DeviceSearchBar from 'components/DeviceSearchBar';
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
import FirmwareHistoryModal from 'components/FirmwareHistoryModal';
import { DeviceListTable, useAuth, useToast } from 'ucentral-libs';
import { useGlobalWebSocket } from 'contexts/WebSocketProvider';
import { useAuth, useToast } from 'ucentral-libs';
import Table from './Table';
import meshIcon from '../../assets/icons/Mesh.png';
import apIcon from '../../assets/icons/AP.png';
import internetSwitch from '../../assets/icons/Switch.png';
@@ -16,8 +18,10 @@ const DeviceList = () => {
const { t } = useTranslation();
const { addToast } = useToast();
const history = useHistory();
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,
});
@@ -35,6 +39,7 @@ const DeviceList = () => {
deviceType: '',
serialNumber: '',
});
const { lastMessage } = useGlobalWebSocket();
const deviceIcons = {
meshIcon,
@@ -55,6 +60,7 @@ const DeviceList = () => {
const getDeviceInformation = (selectedPage = page, devicePerPage = devicesPerPage) => {
setLoading(true);
setOverrides({});
const options = {
headers: {
@@ -354,10 +360,31 @@ const DeviceList = () => {
});
};
const displayDevices = () =>
devices.map((device) => ({
...device,
connected:
overrides[device.serialNumber] !== undefined
? overrides[device.serialNumber]
: device.connected,
}));
useEffect(() => {
getCount();
}, []);
useEffect(() => {
if (deviceToRefresh) refreshDevice(deviceToRefresh.serial);
}, [deviceToRefresh?.timestamp]);
useEffect(() => {
if (lastMessage && lastMessage.type === 'DEVICE') {
const { serialNumber: msgSerial, timestamp } = lastMessage;
if (timestamp !== deviceToRefresh?.timestamp)
setDeviceToRefresh({ serial: msgSerial, timestamp });
}
}, [lastMessage, deviceToRefresh]);
useEffect(() => {
if (upgradeStatus.result !== undefined) {
addToast({
@@ -377,11 +404,11 @@ const DeviceList = () => {
return (
<div>
<DeviceListTable
<Table
currentPage={page}
t={t}
searchBar={<DeviceSearchBar />}
devices={devices}
devices={displayDevices()}
loading={loading}
updateDevicesPerPage={updateDevicesPerPage}
devicesPerPage={devicesPerPage}

View File

@@ -1,18 +1,17 @@
/* eslint-disable-rule prefer-destructuring */
import React, { useState, useEffect } from 'react';
import {
CWidgetDropdown,
CRow,
CCol,
CCardHeader,
CCardBody,
CCollapse,
CButton,
CDataTable,
CCard,
CCardBody,
CPopover,
CFormText,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilTrash } from '@coreui/icons';
import { cilSync, cilTrash } from '@coreui/icons';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import { dateToUnix } from 'utils/helper';
@@ -29,7 +28,9 @@ const DeviceLogs = () => {
const [loading, setLoading] = useState(false);
const [logs, setLogs] = useState([]);
const [start, setStart] = useState('');
const [startError, setStartError] = useState(false);
const [end, setEnd] = useState('');
const [endError, setEndError] = useState(false);
const [logLimit, setLogLimit] = useState(25);
const [loadingMore, setLoadingMore] = useState(false);
const [showLoadingMore, setShowLoadingMore] = useState(true);
@@ -40,11 +41,25 @@ const DeviceLogs = () => {
};
const modifyStart = (value) => {
try {
new Date(value).toISOString();
setStartError(false);
setStart(value);
} catch (e) {
setStart('');
setStartError(true);
}
};
const modifyEnd = (value) => {
try {
new Date(value).toISOString();
setEndError(false);
setEnd(value);
} catch (e) {
setEnd('');
setEndError(true);
}
};
const showMoreLogs = () => {
@@ -167,25 +182,49 @@ const DeviceLogs = () => {
return (
<div>
<CWidgetDropdown
className="m-0"
inverse="true"
color="gradient-info"
header={t('device_logs.title')}
footerSlot={
<div className="pb-1 px-3">
<CRow className="mb-3">
<CCol>
{t('common.from')}
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
</CCol>
<CCol>
{t('common.to')}
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard>
<div className="overflow-auto" style={{ height: '250px' }}>
<CCard className="m-0">
<CCardHeader className="dark-header">
<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={getLogs} disabled={startError || endError}>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={end ? new Date(end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={start ? new Date(start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
<div className="px-2">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm" color="danger">
<CIcon name="cil-trash" content={cilTrash} />
</CButton>
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody className="p-1">
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
<CDataTable
addTableClasses="ignore-overflow table-sm"
border
@@ -214,7 +253,7 @@ const DeviceLogs = () => {
toggleDetails(index);
}}
>
<CIcon name="cilList" size="md" />
<CIcon name="cilList" />
</CButton>
</td>
),
@@ -240,18 +279,8 @@ const DeviceLogs = () => {
</div>
)}
</div>
</CCardBody>
</CCard>
</div>
}
>
<div className="text-right float-right">
<CPopover content={t('common.delete')}>
<CButton onClick={toggleDeleteModal} size="sm">
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
</CButton>
</CPopover>
</div>
</CWidgetDropdown>
<DeleteLogModal
serialNumber={deviceSerialNumber}
object="logs"

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,11 +1,11 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs';
import { checkIfJson } from 'utils/helper';
import { useAuth } from 'ucentral-libs';
import { toJson } from 'utils/helper';
import DeviceSearchBarInput from './Input';
const DeviceSearchBar = () => {
const { t } = useTranslation();
const DeviceSearchBar = ({ action }) => {
const history = useHistory();
const { currentToken, endpoints } = useAuth();
const [socket, setSocket] = useState(null);
@@ -13,8 +13,9 @@ const DeviceSearchBar = () => {
const [waitingSearch, setWaitingSearch] = useState('');
const search = (value) => {
if (socket) {
if (socket.readyState === WebSocket.OPEN) {
if (value.length > 0 && value.match('^[a-fA-F0-9]+$')) {
if (value.length > 1 && value.match('^[a-fA-F0-9-*]+$')) {
setWaitingSearch('');
socket.send(
JSON.stringify({ command: 'serial_number_search', serial_prefix: value.toLowerCase() }),
@@ -22,12 +23,13 @@ const DeviceSearchBar = () => {
} 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 = () => {
@@ -43,12 +45,10 @@ const DeviceSearchBar = () => {
};
socket.onmessage = (event) => {
if (checkIfJson(event.data)) {
const result = JSON.parse(event.data);
if (result.command === 'serial_number_search' && result.serialNumbers) {
const result = toJson(event.data);
if (result && result.serialNumbers) {
setResults(result.serialNumbers);
}
}
};
if (waitingSearch.length > 0) {
@@ -60,12 +60,28 @@ const DeviceSearchBar = () => {
}, [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} />;
return (
<DeviceSearchBarInput
search={search}
results={results}
history={history}
action={action}
isDisabled={endpoints.owgw === undefined}
/>
);
};
DeviceSearchBar.propTypes = {
action: PropTypes.func,
};
DeviceSearchBar.defaultProps = {
action: null,
};
export default DeviceSearchBar;

View File

@@ -1,100 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { DeviceStatusCard as Card, useDevice, useAuth, useToast } from 'ucentral-libs';
const DeviceStatusCard = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [lastStats, setLastStats] = useState(null);
const [status, setStatus] = useState(null);
const [deviceConfig, setDeviceConfig] = useState(null);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const getDevice = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}`, options)
.then((response) => {
setDeviceConfig(response.data);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
});
};
const getData = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
const lastStatsRequest = axiosInstance.get(
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(
deviceSerialNumber,
)}/statistics?lastOnly=true`,
options,
);
const statusRequest = axiosInstance.get(
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}/status`,
options,
);
Promise.all([lastStatsRequest, statusRequest])
.then(([newStats, newStatus]) => {
setLastStats(newStats.data);
setStatus(newStatus.data);
})
.catch(() => {
setError(true);
})
.finally(() => {
setLoading(false);
});
};
const refresh = () => {
getData();
getDevice();
};
useEffect(() => {
setError(false);
if (deviceSerialNumber) {
getDevice();
getData();
}
}, [deviceSerialNumber]);
return (
<Card
t={t}
loading={loading}
error={error}
deviceSerialNumber={deviceSerialNumber}
getData={refresh}
deviceConfig={deviceConfig}
status={status}
lastStats={lastStats}
/>
);
};
export default DeviceStatusCard;

View File

@@ -0,0 +1,154 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import {
CButton,
CModal,
CModalHeader,
CModalTitle,
CModalBody,
CPopover,
CRow,
CCol,
CLabel,
CTextarea,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { useAuth, useToast } from 'ucentral-libs';
import { cilSave, cilX } from '@coreui/icons';
import axiosInstance from 'utils/axiosInstance';
const EditBlacklistModal = ({ show, toggle, serialNumber, refresh }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { addToast } = useToast();
const { endpoints, currentToken } = useAuth();
const [reason, setReason] = useState('');
const getBlacklistInfo = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, {
headers,
})
.then((response) => {
setReason(response.data.reason);
setLoading(false);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
setLoading(false);
toggle();
});
};
const save = () => {
setLoading(true);
const parameters = {
reason,
};
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.put(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, parameters, { headers })
.then(() => {
addToast({
title: t('common.success'),
body: t('device.success_edit_blacklist'),
color: 'success',
autohide: true,
});
toggle();
if (refresh) refresh();
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('device.error_edit_blacklist', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
if (show) getBlacklistInfo();
}, [show, serialNumber]);
return (
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('device.edit_blacklist')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.add')}>
<CButton
color="primary"
variant="outline"
className="ml-2"
onClick={save}
disabled={loading || reason === ''}
>
<CIcon content={cilSave} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<CRow>
<CLabel col sm="3">
{t('common.reason')}
</CLabel>
<CCol sm="9" className="pt-2">
<CTextarea
name="reason"
id="reason"
rows="3"
type="text"
required
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</CCol>
</CRow>
</CModalBody>
</CModal>
);
};
EditBlacklistModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
serialNumber: PropTypes.string,
refresh: PropTypes.func,
};
EditBlacklistModal.defaultProps = {
serialNumber: '',
refresh: null,
};
export default EditBlacklistModal;

View File

@@ -0,0 +1,163 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import {
CForm,
CInput,
CLabel,
CCol,
CFormGroup,
CInvalidFeedback,
CFormText,
CRow,
CTextarea,
} from '@coreui/react';
import { CopyToClipboardButton } from 'ucentral-libs';
const EditDefaultConfigurationForm = ({
t,
disable,
fields,
updateField,
updateFieldWithKey,
deviceTypes,
editing,
}) => {
const [typeOptions, setTypeOptions] = useState([]);
const [chosenTypes, setChosenTypes] = useState([]);
const parseOptions = () => {
const options = [{ value: '*', label: 'All' }];
const newOptions = deviceTypes.map((option) => ({
value: option,
label: option,
}));
options.push(...newOptions);
setTypeOptions(options);
setChosenTypes([]);
const newChosenTypes = fields.modelIds.value.map((dType) => ({
value: dType,
label: dType === '*' ? 'All' : dType,
}));
setChosenTypes(newChosenTypes);
};
const typeOnChange = (chosenArray) => {
const allIndex = chosenArray.findIndex((el) => el.value === '*');
// If the All option was chosen before, we take it out of the array
if (allIndex === 0 && chosenTypes.length > 0) {
const newResults = chosenArray.slice(1);
setChosenTypes(newResults);
updateFieldWithKey('modelIds', {
value: newResults.map((el) => el.value),
error: false,
notEmpty: true,
});
} else if (allIndex > 0) {
setChosenTypes([{ value: '*', label: 'All' }]);
updateFieldWithKey('modelIds', { value: ['*'], error: false, notEmpty: true });
} else if (chosenArray.length > 0) {
setChosenTypes(chosenArray);
updateFieldWithKey('modelIds', {
value: chosenArray.map((el) => el.value),
error: false,
notEmpty: true,
});
} else {
setChosenTypes([]);
updateFieldWithKey('modelIds', { value: [], error: false, notEmpty: true });
}
};
useEffect(() => {
parseOptions();
}, [deviceTypes, fields.name.value]);
return (
<CForm>
<CFormGroup row className="pb-3">
<CLabel col htmlFor="name">
{t('user.name')}
</CLabel>
<CCol sm="7" className="pt-2">
{fields.name.value}
</CCol>
</CFormGroup>
<CFormGroup row className="pb-3">
<CLabel col htmlFor="description">
{t('user.description')}
</CLabel>
<CCol sm="7">
<CInput
id="description"
type="text"
required
value={fields.description.value}
onChange={updateField}
invalid={fields.description.error}
disabled={disable || !editing}
maxLength="50"
/>
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
</CFormGroup>
<CRow className="pb-3">
<CLabel col htmlFor="deviceTypes">
<div>{t('configuration.supported_device_types')}:</div>
</CLabel>
<CCol sm="7">
<Select
isMulti
closeMenuOnSelect={false}
id="deviceTypes"
options={typeOptions}
onChange={typeOnChange}
value={chosenTypes}
className={`basic-multi-select ${fields.modelIds.error ? 'border-danger' : ''}`}
classNamePrefix="select"
isDisabled={disable || !editing}
/>
<CFormText hidden={!fields.modelIds.error} color="danger">
{t('configuration.need_device_type')}
</CFormText>
</CCol>
</CRow>
<div className="pb-3">
{t('configuration.title')}
<CopyToClipboardButton t={t} size="sm" content={fields.configuration.value} />
</div>
<CRow className="pb-3">
<CCol>
<CTextarea
style={{ overflowY: 'scroll', height: '500px' }}
id="configuration"
type="text"
required
value={fields.configuration.value}
onChange={updateField}
invalid={fields.configuration.error}
disabled={disable || !editing}
/>
<CFormText hidden={!fields.configuration.error} color="danger">
{t('common.required')}
</CFormText>
</CCol>
</CRow>
</CForm>
);
};
EditDefaultConfigurationForm.propTypes = {
t: PropTypes.func.isRequired,
disable: PropTypes.bool.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
updateFieldWithKey: PropTypes.func.isRequired,
deviceTypes: PropTypes.instanceOf(Array).isRequired,
editing: PropTypes.bool.isRequired,
};
export default EditDefaultConfigurationForm;

View File

@@ -0,0 +1,243 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { CModal, CModalHeader, CModalTitle, CModalBody, CButton, CPopover } from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX, cilSave, cilPencil } from '@coreui/icons';
import { useToast, useFormFields, useAuth } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { useTranslation } from 'react-i18next';
import { checkIfJson } from 'utils/helper';
import Form from './Form';
const initialForm = {
name: {
value: '',
error: false,
required: true,
},
description: {
value: '',
error: false,
},
modelIds: {
value: [],
error: false,
notEmpty: true,
},
configuration: {
value: '',
error: false,
required: true,
},
};
const EditConfigurationModal = ({ show, toggle, refresh, configId }) => {
const { t } = useTranslation();
const { addToast } = useToast();
const { currentToken, endpoints } = useAuth();
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialForm);
const [loading, setLoading] = useState(false);
const [deviceTypes, setDeviceTypes] = useState([]);
const [editing, setEditing] = useState(false);
const getConfig = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owgw}/api/v1/default_configuration/${configId}`, options)
.then((response) => {
const newConfig = {};
for (const key of Object.keys(response.data)) {
if (key in initialForm) {
newConfig[key] = {
...initialForm[key],
value: response.data[key],
};
}
}
newConfig.configuration.value = JSON.stringify(response.data.configuration, null, 2);
setFormFields(newConfig);
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('configuration.error_fetching_config', {
error: e.response?.data?.ErrorDescription,
}),
color: 'danger',
autohide: true,
});
toggle();
});
};
const getDeviceTypes = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.owfms}/api/v1/firmwares?deviceSet=true`, {
headers,
})
.then((response) => {
setDeviceTypes([...response.data.deviceTypes]);
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
const validation = () => {
let success = true;
for (const [key, field] of Object.entries(fields)) {
if (field.required && field.value === '') {
updateField(key, { error: true });
success = false;
break;
}
if (field.notEmpty && field.value.length === 0) {
updateField(key, { error: true, notEmpty: true });
success = false;
break;
}
}
if (!checkIfJson(fields.configuration.value)) {
updateField('configuration', { error: true });
success = false;
}
return success;
};
const save = () => {
if (validation()) {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
const parameters = {
name: fields.name.value,
description: fields.description.value,
modelIds: fields.modelIds.value,
configuration: fields.configuration.value,
};
axiosInstance
.put(`${endpoints.owgw}/api/v1/default_configuration/${configId}`, parameters, options)
.then(() => {
if (refresh !== null) refresh();
toggle();
addToast({
title: t('common.success'),
body: t('configuration.success_update'),
color: 'success',
autohide: true,
});
})
.catch((e) => {
addToast({
title: t('common.error'),
body: t('configuration.error_update', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
}
};
const toggleEditing = () => {
if (editing) getConfig();
setEditing(!editing);
};
useEffect(() => {
if (show) {
setEditing(false);
getConfig();
getDeviceTypes();
}
}, [show]);
return (
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('configuration.edit_configuration')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.save')}>
<CButton
color="primary"
variant="outline"
className="ml-2"
onClick={save}
disabled={!editing}
>
<CIcon content={cilSave} />
</CButton>
</CPopover>
<CPopover content={t('common.edit')}>
<CButton
color="primary"
variant="outline"
className="ml-2"
onClick={toggleEditing}
disabled={editing}
>
<CIcon content={cilPencil} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody className="px-5">
<Form
t={t}
disable={loading}
fields={fields}
editing={editing}
updateField={updateFieldWithId}
updateFieldWithKey={updateField}
deviceTypes={deviceTypes}
show={show}
/>
</CModalBody>
</CModal>
);
};
EditConfigurationModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
refresh: PropTypes.func,
configId: PropTypes.string,
};
EditConfigurationModal.defaultProps = {
refresh: null,
configId: '',
};
export default EditConfigurationModal;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CCardBody, CCol, CInput, CRow } from '@coreui/react';
import { prettyDate, cleanBytesString } from 'utils/helper';
const FirmwareDetailsForm = ({ t, fields, updateFieldsWithId, editing }) => (
<CCardBody className="p-1">
<CRow>
<CCol sm="2">{t('firmware.release')}</CCol>
<CCol sm="4">{fields.release.value}</CCol>
<CCol sm="2">{t('common.created')}</CCol>
<CCol sm="4">{prettyDate(fields.created.value)}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="2">{t('firmware.image_date')}</CCol>
<CCol sm="4">{prettyDate(fields.imageDate.value)}</CCol>
<CCol sm="2">{t('firmware.size')}</CCol>
<CCol sm="4">{cleanBytesString(fields.size.value)}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="2">{t('firmware.image')}</CCol>
<CCol sm="4">{fields.image.value}</CCol>
<CCol sm="2">{t('firmware.revision')}</CCol>
<CCol sm="4">{fields.revision.value}</CCol>
</CRow>
<CRow className="my-3">
<CCol sm="2">URI</CCol>
<CCol sm="4">{fields.uri.value}</CCol>
<CCol sm="2" className="mt-2">
{t('user.description')}
</CCol>
<CCol sm="4">
{editing ? (
<CInput
id="description"
value={fields.description.value}
onChange={updateFieldsWithId}
maxLength="50"
/>
) : (
<p className="mt-2 mb-0">{fields.description.value}</p>
)}
</CCol>
</CRow>
</CCardBody>
);
FirmwareDetailsForm.propTypes = {
t: PropTypes.func.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateFieldsWithId: PropTypes.func.isRequired,
editing: PropTypes.bool.isRequired,
};
export default FirmwareDetailsForm;

View File

@@ -16,13 +16,8 @@ import {
import CIcon from '@coreui/icons-react';
import { cilPencil, cilSave, cilX } from '@coreui/icons';
import axiosInstance from 'utils/axiosInstance';
import {
useFormFields,
useAuth,
useToast,
FirmwareDetailsForm,
DetailedNotesTable,
} from 'ucentral-libs';
import { useFormFields, useAuth, useToast, DetailedNotesTable } from 'ucentral-libs';
import Form from './Form';
const initialState = {
created: {
@@ -237,12 +232,7 @@ const EditFirmwareModal = ({ show, toggle, firmwareId, refreshTable }) => {
<CTabContent>
<CTabPane active={index === 0} className="pt-2">
{index === 0 ? (
<FirmwareDetailsForm
t={t}
fields={firmware}
updateFieldsWithId={updateWithId}
editing={editing}
/>
<Form t={t} fields={firmware} updateFieldsWithId={updateWithId} editing={editing} />
) : null}
</CTabPane>
<CTabPane active={index === 1}>

View File

@@ -1,229 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { useUser, EditUserModal as Modal, useAuth, useToast } from 'ucentral-libs';
const initialState = {
Id: {
value: '',
error: false,
editable: false,
},
changePassword: {
value: false,
error: false,
editable: true,
},
currentPassword: {
value: '',
error: false,
editable: true,
},
email: {
value: '',
error: false,
editable: false,
},
description: {
value: '',
error: false,
editable: true,
},
name: {
value: '',
error: false,
editable: true,
},
userRole: {
value: 'accounting',
error: false,
editable: true,
},
notes: {
value: [],
editable: false,
},
};
const EditUserModal = ({ show, toggle, userId, getUsers, policies }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const [initialUser, setInitialUser] = useState({});
const [editing, setEditing] = useState(false);
const [user, updateWithId, updateWithKey, setUser] = useUser(initialState);
const getUser = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.owsec}/api/v1/user/${userId}`, options)
.then((response) => {
const newUser = {};
for (const key of Object.keys(response.data)) {
if (key in initialState && key !== 'currentPassword') {
newUser[key] = {
...initialState[key],
value: response.data[key],
};
}
}
setInitialUser({ ...initialState, ...newUser });
setUser({ ...initialState, ...newUser });
})
.catch(() => {
addToast({
title: t('common.error'),
body: t('user.error_retrieving'),
color: 'danger',
autohide: true,
});
toggle();
});
};
const toggleEditing = () => {
if (editing) {
getUser();
}
setEditing(!editing);
};
const updateUser = () => {
setLoading(true);
const parameters = {
id: userId,
};
let newData = false;
for (const key of Object.keys(user)) {
if (user[key].editable && user[key].value !== initialUser[key].value) {
if (key === 'currentPassword' && user[key].length < 8) {
updateWithKey('currentPassword', {
error: true,
});
newData = false;
break;
} else if (key === 'changePassword') {
parameters[key] = user[key].value === 'on';
newData = true;
} else {
parameters[key] = user[key].value;
newData = true;
}
}
}
const newNotes = [];
for (let i = 0; i < user.notes.value.length; i += 1) {
if (user.notes.value[i].new) newNotes.push({ note: user.notes.value[i].note });
}
parameters.notes = newNotes;
if (newData || newNotes.length > 0) {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.put(`${endpoints.owsec}/api/v1/user/${userId}`, parameters, options)
.then(() => {
addToast({
title: t('user.update_success_title'),
body: t('user.update_success'),
color: 'success',
autohide: true,
});
getUsers();
toggle();
})
.catch((e) => {
addToast({
title: t('user.update_failure_title'),
body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
color: 'danger',
autohide: true,
});
getUser();
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
addToast({
title: t('user.update_success_title'),
body: t('user.update_success'),
color: 'success',
autohide: true,
});
getUsers();
toggle();
}
};
const addNote = (currentNote) => {
const newNotes = [...user.notes.value];
newNotes.unshift({
note: currentNote,
new: true,
created: new Date().getTime() / 1000,
createdBy: '',
});
updateWithKey('notes', { value: newNotes });
};
useEffect(() => {
if (userId) {
getUser();
}
}, [userId]);
useEffect(() => {
if (show) {
getUser();
setEditing(false);
}
}, [show]);
return (
<Modal
t={t}
user={user}
updateUserWithId={updateWithId}
saveUser={updateUser}
loading={loading}
policies={policies}
show={show}
toggle={toggle}
editing={editing}
toggleEditing={toggleEditing}
addNote={addNote}
/>
);
};
EditUserModal.propTypes = {
userId: PropTypes.string.isRequired,
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
getUsers: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(EditUserModal);

View File

@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CModal,
CModalBody,
CModalHeader,
CModalTitle,
CSpinner,
CPopover,
CButton,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilX } from '@coreui/icons';
const EventQueueModal = ({ t, show, toggle, loading, result }) => (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('commands.event_queue')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody className="text-center">
{loading ? (
<CSpinner color="primary" size="lg" />
) : (
<pre className="ignore text-left">{JSON.stringify(result, null, 4)}</pre>
)}
</CModalBody>
</CModal>
);
EventQueueModal.propTypes = {
t: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
result: PropTypes.instanceOf(Object).isRequired,
};
export default EventQueueModal;

View File

@@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { EventQueueModal as Modal, useAuth, useDevice, useToast } from 'ucentral-libs';
import { useAuth, useDevice, useToast } from 'ucentral-libs';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import Modal from './Modal';
const EventQueueModal = ({ show, toggle }) => {
const { t } = useTranslation();
@@ -33,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,338 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CCard,
CCardBody,
CCardHeader,
CCol,
CDataTable,
CPopover,
CRow,
CSpinner,
CWidgetIcon,
} from '@coreui/react';
import { CChartBar, CChartHorizontalBar, CChartPie } from '@coreui/react-chartjs';
import { cilClock, cilHappy, cilMeh, cilFrown, cilBirthdayCake, cilInfo } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { FormattedDate } from 'ucentral-libs';
import styles from './index.module.scss';
const getLatestColor = (percent = 0) => {
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
if (numberPercent >= 90) return 'success';
if (numberPercent > 60) return 'warning';
return 'danger';
};
const getLatestIcon = (percent = 0) => {
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
if (numberPercent >= 90) return <CIcon width={36} name="cil-happy" content={cilHappy} />;
if (numberPercent > 60) return <CIcon width={36} name="cil-meh" content={cilMeh} />;
return <CIcon width={36} name="cil-frown" content={cilFrown} />;
};
const FirmwareDashboard = ({ t, data, loading }) => {
const columns = [
{ key: 'endpoint', label: t('common.endpoint'), filter: false, sorter: false },
{ key: 'devices', label: t('common.devices') },
{ key: 'percent', label: '' },
];
return (
<div style={{ position: 'relative' }}>
<div style={{ opacity: loading ? '20%' : '100%' }}>
<CRow className="mt-3">
<CCol>
<CWidgetIcon
text={t('common.last_dashboard_refresh')}
header={data.snapshot ? <FormattedDate date={data.snapshot} size="lg" /> : <h2>-</h2>}
color="info"
iconPadding={false}
>
<CIcon width={36} name="cil-clock" content={cilClock} />
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={t('common.up_to_date')}
header={<h2>{data.latestSoftwareRate}</h2>}
color={getLatestColor(data.latestSoftwareRate)}
iconPadding={false}
>
{getLatestIcon(data.latestSoftwareRate)}
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={
<div>
<div className="float-left">{t('common.devices')}</div>
<div className="float-left ml-2">
<CPopover content={t('device.firmware_count_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
}
header={<h2>{data.numberOfDevices}</h2>}
color="primary"
iconPadding={false}
>
<CIcon width={36} name="cil-router" />
</CWidgetIcon>
</CCol>
<CCol>
<CWidgetIcon
text={
<div>
<div className="float-left">{t('firmware.average_age')}</div>
<div className="float-left ml-2">
<CPopover content={t('firmware.age_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
}
header={<h2>{data.averageFirmwareAge}</h2>}
color="dark"
iconPadding={false}
>
<CIcon width={36} content={cilBirthdayCake} />
</CWidgetIcon>
</CCol>
</CRow>
<CRow>
<CCol>
<CCard>
<CCardHeader className="dark-header">{t('common.firmware_installed')}</CCardHeader>
<CCardBody>
<CChartPie
datasets={data.firmwareDistribution.datasets}
labels={data.firmwareDistribution.labels}
options={{
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader className="dark-header">
<div>
<div className="float-left">{t('common.devices_using_latest')}</div>
<div className="float-left ml-2">
<CPopover content={t('firmware.latest_explanation')}>
<CIcon content={cilInfo} />
</CPopover>
</div>
</div>
</CCardHeader>
<CCardBody>
<CChartBar
datasets={data.latest.datasets}
labels={data.latest.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
scales: {
yAxes: [
{
ticks: {
maxTicksLimit: 5,
beginAtZero: true,
stepSize: 1,
},
},
],
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader className="dark-header">Unknown Firmware</CCardHeader>
<CCardBody>
<CChartHorizontalBar
datasets={data.unknownFirmwares.datasets}
labels={data.unknownFirmwares.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
scales: {
xAxes: [
{
ticks: {
maxTicksLimit: 5,
beginAtZero: true,
stepSize: 1,
},
},
],
yAxes: [
{
ticks: {
callback: (value) => value.split(' ')[0],
},
},
],
},
}}
/>
</CCardBody>
</CCard>
</CCol>
</CRow>
<CRow>
<CCol>
<CCard>
<CCardHeader className="dark-header">{t('common.device_status')}</CCardHeader>
<CCardBody>
<CChartPie
datasets={data.status.datasets}
labels={data.status.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) => `${ds.datasets[0].data[item.index]} devices`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader className="dark-header">{t('firmware.device_types')}</CCardHeader>
<CCardBody>
<CChartPie
datasets={data.deviceType.datasets}
labels={data.deviceType.labels}
options={{
tooltips: {
callbacks: {
title: (item, ds) => ds.labels[item[0].index],
label: (item, ds) =>
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
},
},
legend: {
display: true,
position: 'right',
},
}}
/>
</CCardBody>
</CCard>
</CCol>
<CCol>
<CCard>
<CCardHeader className="dark-header">OUIs</CCardHeader>
<CCardBody>
<CChartHorizontalBar
datasets={data.ouis.datasets}
labels={data.ouis.labels}
options={{
tooltips: {
mode: 'index',
intersect: false,
},
hover: {
mode: 'index',
intersect: false,
},
legend: {
display: false,
position: 'right',
},
scales: {
xAxes: [
{
ticks: {
maxTicksLimit: 5,
beginAtZero: true,
stepSize: 1,
},
},
],
yAxes: [
{
ticks: {
callback: (value) => value.split(' ')[0],
},
},
],
},
}}
/>
</CCardBody>
</CCard>
</CCol>
</CRow>
<CRow>
<CCol>
<CCard>
<CCardHeader className="dark-header">{t('common.endpoints')}</CCardHeader>
<CCardBody>
<CDataTable
addTableClasses="table-sm"
items={data.endpoints ?? []}
fields={columns}
hover
border
/>
</CCardBody>
</CCard>
</CCol>
<CCol />
<CCol />
</CRow>
</div>
{loading ? (
<div className={styles.centerContainer}>
<CSpinner className={styles.spinner} />
</div>
) : null}
</div>
);
};
FirmwareDashboard.propTypes = {
t: PropTypes.func.isRequired,
data: PropTypes.instanceOf(Object).isRequired,
loading: PropTypes.bool.isRequired,
};
export default React.memo(FirmwareDashboard);

View File

@@ -0,0 +1,10 @@
.centerContainer {
position: absolute;
top: 5%;
right: 50%;
}
.spinner {
height: 50px;
width: 50px;
}

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FirmwareDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
import { useAuth, COLOR_LIST } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import Dashboard from './Dashboard';
const FirmwareDashboard = () => {
const { t } = useTranslation();
@@ -64,9 +65,12 @@ const FirmwareDashboard = () => {
const statusColors = [];
const statusLabels = [];
const totalDevices = parsedData.status.reduce((acc, point) => acc + point.value, 0);
parsedData.statusDevices = {};
parsedData.numberOfDevices = totalDevices;
for (const point of parsedData.status) {
statusDs.push(Math.round((point.value / totalDevices) * 100));
statusDs.push(point.value);
statusLabels.push(point.tag);
parsedData[point.value] = point.value;
let color = '';
switch (point.tag) {
case 'connected':

View File

@@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CDataTable } from '@coreui/react';
import { prettyDate } from 'utils/helper';
const FirmwareHistoryModal = ({ t, loading, data }) => {
const columns = [
{ key: 'date', label: '#', _style: { width: '20%' } },
{ key: 'fromRelease', label: t('firmware.from_release'), sorter: false },
{ key: 'toRelease', label: t('firmware.to_release'), sorter: false },
];
return (
<CDataTable
addTableClasses="ignore-overflow table-sm"
fields={columns}
items={data}
hover
border
loading={loading}
sorter
sorterValue={{ column: 'radio', asc: true }}
scopedSlots={{
date: (item) => <td>{prettyDate(item.upgraded)}</td>,
}}
/>
);
};
FirmwareHistoryModal.propTypes = {
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.instanceOf(Array).isRequired,
};
export default React.memo(FirmwareHistoryModal);

View File

@@ -10,7 +10,8 @@ import {
CModalFooter,
CModalTitle,
} from '@coreui/react';
import { FirmwareHistoryTable, useAuth } from 'ucentral-libs';
import { useAuth } from 'ucentral-libs';
import Modal from './Modal';
const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
const { t } = useTranslation();
@@ -51,7 +52,7 @@ const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
</CModalTitle>
</CModalHeader>
<CModalBody>
<FirmwareHistoryTable t={t} loading={loading} data={data} />
<Modal t={t} loading={loading} data={data} />
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>

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

@@ -12,4 +12,4 @@ DeviceStatisticsChart.propTypes = {
chart: PropTypes.instanceOf(Object).isRequired,
};
export default DeviceStatisticsChart;
export default React.memo(DeviceStatisticsChart);

View File

@@ -1,11 +1,11 @@
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';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth, useDevice, CopyToClipboardButton } from 'ucentral-libs';
const LatestStatisticsModal = ({ show, toggle }) => {
const { t } = useTranslation();
@@ -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();
@@ -51,7 +62,10 @@ const LatestStatisticsModal = ({ show, toggle }) => {
</div>
</CModalHeader>
<CModalBody>
<pre className="ignore">{JSON.stringify(latestStats, null, 2)}</pre>
<div style={{ textAlign: 'right' }}>
<CopyToClipboardButton t={t} size="lg" content={latestStatsString} />
</div>
<pre className="ignore">{latestStatsString}</pre>
</CModalBody>
</CModal>
);

View File

@@ -1,37 +1,105 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
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 { unixToTime, capitalizeFirstLetter } from 'utils/helper';
import eventBus from 'utils/eventBus';
import { useAuth } from 'ucentral-libs';
import {
capitalizeFirstLetter,
datesSameDay,
dateToUnix,
prettyDate,
unixToTime,
} from 'utils/helper';
import DeviceStatisticsChart from './DeviceStatisticsChart';
const StatisticsChartList = () => {
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) => {
const sortedData = data.sort((a, b) => {
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);
}
// 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),
);
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(unixToTime(log.recorded));
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;
@@ -57,22 +125,45 @@ const StatisticsChartList = () => {
}
// 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?.tx_bytes ? Math.floor(inter.counters.tx_bytes / 1024) : 0,
inter.counters ? Math.floor(inter.counters.tx_bytes / 1024) : 0,
);
interfaceList[interfaceTypes[inter.name]][1].data.push(
inter.counters?.rx_bytes ? Math.floor(inter.counters.rx_bytes / 1024) : 0,
inter.counters ? Math.floor(inter.counters.rx_bytes / 1024) : 0,
);
}
}
}
const options = {
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',
group: 'txrx',
},
stroke: {
curve: 'smooth',
@@ -84,8 +175,8 @@ const StatisticsChartList = () => {
fontSize: '15px',
},
},
categories,
tickAmount: 20,
categories: newCategories,
tickAmount: areSameDay ? 15 : 10,
},
yaxis: {
labels: {
@@ -105,61 +196,98 @@ const StatisticsChartList = () => {
},
};
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 newOptions = {
interfaceList,
settings: options,
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) {
setStatOptions(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 getStatistics = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
params: {
serialNumber: '24f5a207a130',
},
};
axiosInstance
.get(
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics?newest=true&limit=50`,
options,
)
.then((response) => {
transformIntoDataset(response.data.data);
})
.catch(() => {});
};
useEffect(() => {
if (deviceSerialNumber) {
getStatistics();
}
}, [deviceSerialNumber]);
useEffect(() => {
eventBus.on('refreshInterfaceStatistics', () => getStatistics());
return () => {
eventBus.remove('refreshInterfaceStatistics');
};
}, []);
const getInterface = useCallback(() => {
if (error === 'error') {
return (
<div>
{statOptions.interfaceList.map((data) => {
<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(
(inter) => inter[0].titleName === section,
);
if (interfaceToShow) {
const options = {
data,
data: interfaceToShow,
options: {
...statOptions.settings,
...statOptions.interfaceOptions,
title: {
text: capitalizeFirstLetter(data[0].titleName),
text: capitalizeFirstLetter(interfaceToShow[0].titleName),
align: 'left',
style: {
fontSize: '25px',
@@ -172,9 +300,92 @@ const StatisticsChartList = () => {
<DeviceStatisticsChart chart={options} />
</div>
);
}
return <p>N/A</p>;
}, [statOptions, section, error]);
const getStatistics = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
params: {},
};
let extraParams = '';
if (time.start !== null && time.end !== null) {
const utcStart = new Date(time.start).toISOString();
const utcEnd = new Date(time.end).toISOString();
options.params.startDate = dateToUnix(utcStart);
options.params.endDate = dateToUnix(utcEnd);
options.params.limit = 10000;
} else {
extraParams = '?newest=true&limit=50';
}
axiosInstance
.get(
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics${extraParams}`,
options,
)
.then((response) => {
transformIntoDataset(response.data.data);
})
.catch(() => {})
.finally(() => setLoading(false));
};
useEffect(() => {
if (deviceSerialNumber) {
getStatistics();
}
}, [deviceSerialNumber, time.refreshId]);
if (loading) {
return (
<div className="text-center">
<CSpinner size="xl" />
</div>
);
}
return (
<div>
{section !== 'memory' && !loading && getInterface()}
{section === 'memory' &&
!loading &&
statOptions.memory.map((data) => {
const options = {
data,
options: {
...statOptions.memoryOptions,
title: {
text: capitalizeFirstLetter(data[0].titleName),
align: 'left',
style: {
fontSize: '25px',
},
},
},
};
return (
<div key={createUuid()}>
<DeviceStatisticsChart chart={options} section={section} />
</div>
);
})}
</div>
);
};
StatisticsChartList.propTypes = {
deviceSerialNumber: PropTypes.string.isRequired,
setOptions: PropTypes.func.isRequired,
section: PropTypes.string.isRequired,
time: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(StatisticsChartList);

View File

@@ -1,30 +1,109 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CCard, CCardHeader, CCardBody, CPopover, CButton } from '@coreui/react';
import { v4 as createUuid } from 'uuid';
import {
CCard,
CCardHeader,
CCardBody,
CPopover,
CButton,
CSelect,
CFormText,
} 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 eventBus from 'utils/eventBus';
import LifetimeStatsmodal from 'components/LifetimeStatsModal';
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 [showLifetimeModal, setShowLifetimeModal] = useState(false);
const [options, setOptions] = useState([]);
const [section, setSection] = useState('');
const [startError, setStartError] = useState(false);
const [endError, setEndError] = useState(false);
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 toggleLifetimeModal = () => {
setShowLifetimeModal(!showLifetimeModal);
const modifyStart = (value, refresh = true) => {
try {
new Date(value).toISOString();
setStartError(false);
if (refresh) setTime({ ...time, refreshId: createUuid(), start: value, isChosen: true });
else setTime({ ...time, start: value, isChosen: true });
} catch (e) {
setStartError(true);
}
};
const modifyEnd = (value, refresh = true) => {
try {
new Date(value).toISOString();
setEndError(false);
if (refresh) setTime({ ...time, refreshId: createUuid(), end: value, isChosen: true });
else setTime({ ...time, end: value, isChosen: true });
} catch (e) {
setEndError(true);
}
};
const refresh = () => {
eventBus.dispatch('refreshInterfaceStatistics', { message: 'Refresh interface statistics' });
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">
@@ -32,15 +111,51 @@ 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}>
<CButton
size="sm"
color="info"
onClick={handleRefreshClick}
disabled={startError || endError}
>
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
<div className="pl-2">
<CButton size="sm" color="info" onClick={toggleLifetimeModal}>
Lifetime Statistics
</CButton>
<DatePicker
includeTime
onChange={(date) => modifyEnd(date)}
value={time.end ? new Date(time.end) : undefined}
/>
<CFormText color="danger" hidden={!endError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
To:
<div className="pl-2">
<DatePicker
includeTime
onChange={(date) => modifyStart(date)}
value={time.start ? new Date(time.start) : undefined}
/>
<CFormText color="danger" hidden={!startError}>
{t('common.invalid_date_explanation')}
</CFormText>
</div>
From:
<div className="px-2">
<CSelect
custom
value={section}
disabled={options.length === 0}
onChange={(e) => setSection(e.target.value)}
>
{options.map((opt) => (
<option value={opt.value} key={createUuid()}>
{opt.label}
</option>
))}
</CSelect>
</div>
<div className="pl-2">
<CButton size="sm" color="info" onClick={toggleLatestModal}>
@@ -50,11 +165,15 @@ const DeviceStatisticsCard = () => {
</div>
</CCardHeader>
<CCardBody className="p-1">
<StatisticsChartList />
<StatisticsChartList
deviceSerialNumber={deviceSerialNumber}
setOptions={setOptions}
section={section}
time={time}
/>
</CCardBody>
</CCard>
<LatestStatisticsmodal show={showLatestModal} toggle={toggleLatestModal} />
<LifetimeStatsmodal show={showLifetimeModal} toggle={toggleLifetimeModal} />
</div>
);
};

View File

@@ -1,48 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import axiosInstance from 'utils/axiosInstance';
import { useTranslation } from 'react-i18next';
import { LifetimeStatsModal as Modal, useAuth, useDevice } from 'ucentral-libs';
const LifetimeStatsModal = ({ show, toggle }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const [loading, setLoading] = useState(false);
const [data, setData] = useState({});
const getData = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics?lifetime=true`,
options,
)
.then((response) => {
setData(response.data);
})
.catch(() => {})
.finally(() => setLoading(false));
};
useEffect(() => {
if (show) getData();
}, [show]);
return <Modal t={t} loading={loading} show={show} toggle={toggle} data={data} />;
};
LifetimeStatsModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
};
export default LifetimeStatsModal;

Some files were not shown because too many files have changed in this diff Show More