Compare commits

...

171 Commits

Author SHA1 Message Date
TIP Automation User
a4743b6db5 Chg: update image tag in helm values to v4.0.0 2025-05-02 15:40:36 +00:00
TIP Automation User
b3880f7e7e Chg: update image tag in helm values to v4.0.0-RC1 2025-04-24 21:02:49 +00:00
i-chvets
30fffdfe52 Merge pull request #228 from Telecominfraproject/version_update
WIFI-14521: fix: Version update - release 4.0.0
2025-04-24 16:39:17 -04:00
Ivan Chvets
c8d6540ca6 fix: Version update - release 4.0.0
Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
2025-04-24 16:36:43 -04:00
i-chvets
2b2f08c231 Merge pull request #229 from Telecominfraproject/WIFI-14521-ci-changes
WIFI-14521: Update to ubuntu-latest for GH runner
2025-04-24 16:18:46 -04:00
Carsten Schafer
0cfed90a7b WIFI-14521: Update to ubuntu-latest for GH runner
Signed-off-by: Carsten Schafer <Carsten.Schafer@kinarasystems.com>
2025-04-24 15:54:46 -04:00
Carsten Schafer
01008dc1aa Merge pull request #226 from Telecominfraproject/version_update
fix: release 3.2.1 version update
2024-12-10 15:36:59 -05:00
Ivan Chvets
26b90cfdba fix: release 3.2.1 version update
https://telecominfraproject.atlassian.net/browse/WIFI-14165

Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
2024-12-10 15:34:09 -05:00
i-chvets
b218051104 Merge pull request #224 from Telecominfraproject/OLS-516-feat-cable-diagnostics-ui
Ols 516 feat cable diagnostics UI
2024-12-05 09:12:28 -05:00
Sebastian Rubina
a2fa93938f feat: cable diagnostics ui
https://telecominfraproject.atlassian.net/browse/OLS-516
Signed-off-by: Sebastian Rubina
<sebastian.rubina@kinarasystems.com>

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2024-12-04 16:41:06 -05:00
TIP Automation User
c220d11dd0 Chg: update image tag in helm values to v3.2.0
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2024-12-04 16:41:01 -05:00
TIP Automation User
40d533ecc5 Chg: update image tag in helm values to v3.2.0-RC1
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2024-12-04 16:40:40 -05:00
jaspreetsachdev
d1a1c96e74 Merge pull request #223 from Telecominfraproject/version_update
WIFI-14165: release 3.2 version update
2024-09-30 21:02:13 -04:00
Ivan Chvets
1a18985c0d fix: release 3.2 version update
https://telecominfraproject.atlassian.net/browse/WIFI-14165

Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
2024-09-30 20:56:34 -04:00
Charles Bourque
8eede7b559 Merge pull request #222 from stephb9959/main
[OLS-106] Add new asterfusion images
2024-06-06 12:01:57 -04:00
Charles
caab40b08e [OLS-106] Add new asterfusion images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-06 12:00:29 -04:00
Charles Bourque
18fa320b19 Merge pull request #221 from stephb9959/main
[OLS-106] Add new asterfusion images
2024-06-06 11:56:32 -04:00
Charles
6f9f6638d6 [OLS-106] Add new asterfusion images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-06 11:55:56 -04:00
Charles Bourque
5688e2f7bc Merge pull request #220 from stephb9959/main
[OLS-51] Added RTTY for OLS switches
2024-06-04 09:14:01 -04:00
Charles
4738097178 [OLS-51] Added RTTY for OLS switches
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-04 09:13:05 -04:00
Charles Bourque
591ecc3664 Merge pull request #219 from stephb9959/main
[OLS-42] Telemetry duration display fix
2024-06-04 09:06:45 -04:00
Charles
b9089a39ac [OLS-42] Telemetry duration display fix
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-04 09:06:16 -04:00
Charles Bourque
b7bdf89d37 Merge pull request #218 from stephb9959/main
[WIFI-13803] Added fingerprint column to wifi analysis
2024-06-04 08:56:08 -04:00
Charles
849ea9f7b2 [WIFI-13803] Added fingerprint column to wifi analysis
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-04 08:55:35 -04:00
Charles Bourque
bd737ef563 Merge pull request #216 from stephb9959/main
[WIFI-13515] Supporting deviceTypes in lowercase
2024-03-15 17:53:25 +01:00
Charles
e250bd38f8 [WIFI-13515] Supporting deviceTypes in lowercase
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-03-15 17:51:59 +01:00
Charles Bourque
7083da702a Merge pull request #215 from stephb9959/main
[WIFI-13515] Supporting deviceTypes in lowercase
2024-03-15 17:23:33 +01:00
Charles
3d01c20339 [WIFI-13515] Supporting deviceTypes in lowercase
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-03-15 17:22:28 +01:00
Charles Bourque
3b74649206 Merge pull request #214 from stephb9959/main
[WIFI-13455] New Edgecore switch images
2024-03-04 11:21:29 +01:00
Charles
a10f0c992e [WIFI-13455] New Edgecore switch images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-03-04 11:17:46 +01:00
Charles Bourque
32974620c4 Merge pull request #213 from stephb9959/main
[WIFI-13446] Port tables not showing all ports
2024-02-27 16:43:02 +01:00
Charles
0781e3ad8e [WIFI-13446] Port tables not showing all ports
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-02-27 16:42:32 +01:00
Carsten Schafer
0ce107eea0 Merge pull request #212 from Telecominfraproject/WIFI-13357-Be-able-to-set-ingressClassName-for-all-the-component-helm-charts-as-the-annotation-is-no-longer-supported
Set ingress class name if requested
2024-02-11 09:41:58 -05:00
Carsten Schafer
73e3efd92f Set ingress class name if requested
Signed-off-by: Carsten Schafer <Carsten.Schafer@kinarasystems.com>
2024-02-09 15:29:11 -05:00
Charles Bourque
69bff8d8fe Merge pull request #211 from stephb9959/main
[WIFI-13380] Cybertan model images
2024-02-06 15:53:56 +01:00
Charles
22b223f82f [WIFI-13380] Cybertan model images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-02-06 15:53:04 +01:00
Charles Bourque
7b0d43c8b8 Merge pull request #210 from stephb9959/main
[WIFI-13380] Cybertan model images
2024-02-06 09:50:27 +01:00
Charles
7c64fb7a11 [WIFI-13380] Cybertan model images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-02-06 09:49:55 +01:00
Charles Bourque
61f8b69f02 Merge pull request #209 from stephb9959/main
[WIFI-13317] New CIG and Edgecore pictures
2024-01-17 09:39:56 +01:00
Charles
c32fedeb4c [WIFI-13317] New CIG and Edgecore pictures
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-01-17 09:39:25 +01:00
Charles Bourque
4ba3bed742 Merge pull request #208 from stephb9959/main
[WIFI-13315] Wi-Fi analysis fixes
2024-01-16 19:17:52 +01:00
Charles
810318b584 [WIFI-13315] Wi-Fi analysis fixes
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-01-16 19:17:03 +01:00
Charles Bourque
863fda3ef3 Merge pull request #207 from stephb9959/main
[WIFI-13281] Add support for OLS
2024-01-11 12:58:05 -05:00
Charles
deb7715ea1 [WIFI-13282] Add support for OLS
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-01-11 12:57:27 -05:00
Charles
adaebb17e7 [WIFI-13282] Add support for OLS
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-01-11 12:56:20 -05:00
Charles Bourque
e3f6ab43ff Merge pull request #206 from stephb9959/main
[WIFI-13256] Now displaying warnings if a device is blacklisted
2024-01-04 14:12:15 -05:00
Charles
cf977b7612 [WIFI-13256] Now displaying warnings if a device is blacklisted
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-01-04 14:11:02 -05:00
Charles Bourque
fedb60fc8f Merge pull request #205 from stephb9959/main
[WIFI-13257] Fixed configure notification when command is pending
2024-01-02 12:55:50 -05:00
Charles
f8ddf88b8c [WIFI-13257] Fixed configure notification when command is pending
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-01-02 12:55:19 -05:00
Charles Bourque
301581da63 Merge pull request #204 from stephb9959/main
[WIFI-11925] Fixed firmware upgrade result handling
2023-12-18 12:52:25 -05:00
Charles
88cb945760 [WIFI-11925] Fixed firmware upgrade result handling
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-12-18 12:51:57 -05:00
Charles Bourque
c61d0052a9 Merge pull request #203 from stephb9959/main
[WIFI-13170] Advanced system page
2023-12-04 17:34:07 +00:00
Charles
147c3a1153 [WIFI-13170] Advanced system page
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-12-04 17:32:46 +00:00
Charles Bourque
e9f1e4d8da Merge pull request #202 from stephb9959/main
[WIFI-13170] Advanced system page
2023-11-21 14:48:11 +00:00
Charles
f3a995f68f [WIFI-13170] Advanced system page
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-11-21 14:47:05 +00:00
Charles Bourque
a967163d28 Merge pull request #201 from stephb9959/main
[WIFI-13153] Added IP addresses to wifi analysis
2023-11-13 12:06:49 +02:00
Charles
d3514213ca [WIFI-13153] Added IP addresses to wifi analysis
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-11-13 12:06:18 +02:00
Charles Bourque
a55341f406 Merge pull request #200 from stephb9959/main
[WIFI-13136] Display WiFi scan new station count and channel utilization values
2023-11-08 12:22:51 +02:00
Charles
1c9a5bfa18 [WIFI-13136] Display WiFi scan new station count and channel utilization values
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-11-08 12:22:10 +02:00
Charles
179900fab0 [WIFI-13136] Display WiFi scan new station count and channel utilization values
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-11-08 12:19:40 +02:00
Charles Bourque
9011e30521 Merge pull request #199 from stephb9959/main
[WIFI-13104] Now displaying lastContact and certificateExpiry on disc…
2023-10-31 11:14:51 +00:00
Charles
418f4ce576 [WIFI-13104] Now displaying lastContact and certificateExpiry on disconnected devices
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-10-31 11:14:08 +00:00
Charles Bourque
9eb65237f9 Merge pull request #198 from SebastianRubina/new_branch
[WIFI-13097] Fixed issue with Navbar Overlap
2023-10-18 19:59:58 +01:00
Sebastian Rubina
89a667569b Changing Version Number
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2023-10-18 14:52:33 -04:00
Sebastian Rubina
b87091a33a WIFI-13097 - Signed-off-by: Sebastian Rubina sebastian.rubina@kinarasystems.com
Signed-off-by: Sebastian Rubina sebastian.rubina@kinarasystems.com
2023-10-18 12:23:58 -04:00
Charles Bourque
d9a659acbc Merge pull request #196 from stephb9959/main
[WIFI-13005] Firmware modal copy button fix
2023-10-12 14:42:34 +01:00
Charles
ec8347fd7d [WIFI-13005] Firmware modal copy button fix
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-10-12 14:42:01 +01:00
Charles Bourque
b161729c46 Merge pull request #195 from stephb9959/main
[WIFI-12948] Fixed view configuration modal cache
2023-09-20 14:17:14 +01:00
Charles
2194a7fc23 [WIFI-12948] Fixed view configuration modal cache
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-09-20 14:16:41 +01:00
Charles Bourque
03c6471e97 Merge pull request #194 from stephb9959/main
[WIFI-12885] New Monitoring and Default Firmware pages
2023-08-25 16:56:25 +02:00
Charles
be52ed7d44 [WIFI-12885] New Monitoring and Default Firmware pages
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-08-25 16:55:31 +02:00
Charles Bourque
3afc9db5d3 Merge pull request #193 from stephb9959/main
[WIFI-12709] Added new HFCL device images
2023-07-07 09:13:39 +02:00
Charles
30d882e1c0 [WIFI-12709] Added new HFCL device images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-07-07 09:13:00 +02:00
Charles Bourque
4836279b77 Merge pull request #192 from stephb9959/main
[WIFI-12664] Fixed firmware list dates in firmware upgrade modal
2023-06-13 01:13:00 +07:00
Charles Bourque
653cd758f4 Merge pull request #190 from stephb9959/main
[WIFI-12651] Added dev release toggle to firmware upgrade modal
2023-06-06 14:24:02 +07:00
Charles Bourque
3f9478de30 Merge pull request #189 from stephb9959/main
[WIFI-12614] Dynamic VLAN support fix
2023-05-18 08:59:33 +02:00
Charles Bourque
244692e766 Merge pull request #188 from stephb9959/main
[WIFI-12614] Dynamic VLAN support
2023-05-17 18:04:17 +02:00
Charles Bourque
ae0c529fca Merge pull request #187 from stephb9959/main
[WIFI-12613] Display reboot logs on device page
2023-05-17 10:28:48 +02:00
Charles Bourque
356188a350 Merge pull request #186 from stephb9959/main
[WIFI-12612] Add support for connectReason
2023-05-17 10:11:29 +02:00
Charles Bourque
549627a355 Merge pull request #185 from stephb9959/main
[WIFI-12603] Fallback if country code is contained in device type
2023-05-15 19:27:27 +02:00
Charles Bourque
fab4467bfd Merge pull request #184 from stephb9959/main
[WIFI-12585] Fix entity button on device page
2023-05-10 10:19:35 +02:00
Charles Bourque
871efc88b5 Merge pull request #183 from stephb9959/main
[WIFI-12585] Fix entity button on device page
2023-05-10 09:46:44 +02:00
Charles Bourque
caa1fd4d9b Merge pull request #182 from stephb9959/main
[WIFI-12576] Cache fixes
2023-05-03 18:08:27 +02:00
Charles Bourque
a33740c372 Merge pull request #181 from stephb9959/main
[WIFI-12574] Theme improvements
2023-05-03 09:58:34 +02:00
Charles Bourque
bcd9c692e6 Merge pull request #180 from stephb9959/main
[WIFI-12501] Devices table column reordering
2023-05-02 11:43:39 +02:00
Charles Bourque
4bbfbb82bc Merge pull request #179 from stephb9959/main
[WIFI-12515] Using simulated value directly instead of certificate
2023-04-19 17:14:31 +02:00
Charles Bourque
d4aff8067e Merge pull request #178 from stephb9959/main
[WIFI-12515] Display simulated status in device table
2023-04-18 13:54:17 +02:00
Charles Bourque
a1889c88d3 Merge pull request #177 from stephb9959/main
[WIFI-12515] Display simulated status in device table
2023-04-18 11:47:51 +02:00
Charles Bourque
745e76db79 Merge pull request #176 from stephb9959/main
[WIFI-12441] Added export button to device table
2023-04-18 11:21:11 +02:00
Charles Bourque
1c05d8df28 Merge pull request #175 from stephb9959/main
[WIFI-12441] Added export button to device table
2023-04-18 11:06:02 +02:00
Charles Bourque
8a92912035 Merge pull request #174 from stephb9959/main
[WIFI-12436] Fixing crash when certain values are missing in device table
2023-04-13 13:57:59 +02:00
Charles Bourque
4cb4fe53a5 Merge pull request #173 from stephb9959/main
[WIFI-12506] Added radius search and radius clients tile
2023-04-12 10:44:13 +02:00
Charles Bourque
eb48d77636 Merge pull request #172 from stephb9959/main
[WIFI-12437] Improved commonly used device actions accessibility
2023-04-10 11:04:31 +02:00
Charles Bourque
8781c78c15 Merge pull request #171 from stephb9959/main
[WIFI-12435] [WIFI-12436] Device table added functionality and styling fixes
2023-04-10 10:52:05 +02:00
Charles Bourque
039e641046 Merge pull request #169 from stephb9959/main
[WIFI-12418] Memory chart display hidden automatically
2023-03-20 17:16:36 +01:00
Charles Bourque
b3053f32b2 Merge pull request #168 from stephb9959/main
[WIFI-12413] Added toast on download trace/script result error
2023-03-17 10:24:15 +01:00
Charles Bourque
98562fd967 Merge pull request #167 from stephb9959/main
[WIFI-12375] Download command results fix
2023-03-13 19:19:31 +01:00
Charles Bourque
573ecbd58d Merge pull request #166 from stephb9959/main
[WIFI-12364] Deferred scripts command history fix
2023-03-09 11:35:06 +01:00
Charles Bourque
e9d16ee172 Merge pull request #165 from stephb9959/main
[WIFI-12360] Custom script run fix
2023-03-08 10:37:25 +01:00
Charles Bourque
cf17f03ae0 Merge pull request #164 from stephb9959/main
[WIFI-12335] Display gateway tx/rx values
2023-02-25 10:10:47 +01:00
Charles Bourque
e287705e88 Merge pull request #163 from stephb9959/main
[WIFI-12285] Add support for FMS database refreshes
2023-02-09 16:56:49 +01:00
Charles Bourque
2698993a6d Merge pull request #162 from stephb9959/main
[WIFI-12270] Now displaying information related to restricted device in dev mode
2023-02-07 20:27:12 +01:00
Charles Bourque
d7957b85ae Merge pull request #161 from stephb9959/main
[WIFI-12261] Added system secrets on the system page
2023-02-03 16:54:21 +01:00
Charles Bourque
ea0e7340cc Merge pull request #160 from stephb9959/main
[WIFI-12257] Display GPS location on device page
2023-02-01 19:52:28 +01:00
Charles Bourque
566dbbb157 Merge pull request #159 from stephb9959/main
[WIFI-11239] Now sending signature on firmware upgrade as URL param
2023-01-30 12:53:50 +01:00
Charles Bourque
908faa491b Merge pull request #158 from stephb9959/main
[WIFI-12226] Interface stats Y-axis now only 2 decimals or less
2023-01-27 10:40:29 +01:00
Charles Bourque
016ac336b9 Merge pull request #157 from stephb9959/main
[WIFI-12223] User table state fix, with label correction and API logic update
2023-01-25 21:26:04 +01:00
Charles Bourque
1838029d22 Merge pull request #156 from stephb9959/main
[WIFI-12067] Added crash logs to device details page
2023-01-06 14:55:06 -05:00
Charles Bourque
b1cfa6db19 Merge pull request #155 from stephb9959/main
[WIFI-12031] Added confirmation modal to reboot process
2023-01-05 14:06:40 -05:00
Charles Bourque
8c676eb965 Merge pull request #154 from stephb9959/main
[WIFI-10957] Updated statistics to use counters-aggregate if available
2022-12-14 14:36:17 -05:00
Charles Bourque
1808206e74 Merge pull request #153 from stephb9959/main
[WIFI-11936] Serial number in device table now real link
2022-12-14 09:00:51 -05:00
Charles Bourque
42d274e988 Merge pull request #152 from stephb9959/main
[WIFI-11959] Yuncore model images added
2022-12-12 16:49:20 -05:00
Charles Bourque
d006b89efd Merge pull request #151 from stephb9959/main
[WIFI-11958] Device restriction details display
2022-12-12 16:38:23 -05:00
Charles Bourque
8d23168a87 Merge pull request #150 from stephb9959/main
[WIFI-11957] Added device search bar to device page top-bar
2022-12-12 15:02:15 -05:00
Charles Bourque
31a37ae506 Merge pull request #149 from stephb9959/main
[WIFI-11875] Commands custom timeframes popups now not hiding the sav…
2022-12-12 12:12:27 -05:00
Charles Bourque
b829003711 Merge pull request #148 from stephb9959/main
[WIFI-10957] Device statistics now using SSID counters when displayin…
2022-12-12 12:06:31 -05:00
Charles Bourque
0e8df4441d Merge pull request #147 from stephb9959/main
[WIFI-11875] Custom timeframes for commands/deviceslogs/commands now …
2022-12-12 10:57:44 -05:00
Charles Bourque
14c88280f5 Merge pull request #146 from stephb9959/main
[WIFI-11936] Devices table links now using real links with href
2022-12-12 09:45:48 -05:00
Charles Bourque
02095595c6 Merge pull request #145 from stephb9959/main
[WIFI-11874] Made lastStats, capabilities and configuration modals ta…
2022-12-09 09:00:10 -05:00
Charles Bourque
b69e7e4ddf Merge pull request #144 from stephb9959/main
[WIFI-11875] Added timestamps selection for device commands, logs and…
2022-12-05 13:04:16 -05:00
Charles Bourque
33dedbbfa3 Merge pull request #143 from stephb9959/main
[WIFI-11874] Telemetry results now using full page height
2022-12-05 09:24:46 -05:00
Charles Bourque
3b7dad989f Merge pull request #142 from stephb9959/main
[WIFI-11873] Navbar menu hidden by device page bar fix
2022-12-05 09:07:25 -05:00
Charles Bourque
538c6b5233 Merge pull request #141 from stephb9959/main
[WIFI-11542] Script result display and download fixes
2022-12-01 19:23:38 +00:00
Charles Bourque
5c7f683d16 Merge pull request #140 from stephb9959/main
[WIFI-11868] Fixed FMS dashboard average age
2022-12-01 18:08:40 +00:00
Charles Bourque
628e4fa873 Merge pull request #139 from stephb9959/main
[WIFI-11866] User role and user edit fixes
2022-12-01 16:35:53 +00:00
Charles Bourque
89ee99f98d Merge pull request #138 from stephb9959/main
[WIFI-11542] AP Scripts
2022-12-01 16:13:04 +00:00
Charles Bourque
d21f55b476 Merge pull request #137 from stephb9959/main
[WIFI-11858] Sticky device page top bar
2022-12-01 08:52:15 +00:00
Charles Bourque
09e3327e94 Merge pull request #136 from stephb9959/main
[WIFI-11760] Potential fix for duplicate error fetching device notifi…
2022-11-28 10:27:48 +00:00
Charles Bourque
0aed1ba04f Merge pull request #135 from stephb9959/main
[WIFI-11753] Fixed JSON displays to deal with wide strings
2022-11-28 09:47:40 +00:00
Charles Bourque
b52308df80 Merge pull request #134 from stephb9959/main
[WIFI-11749] Limiting device search bar input text length
2022-11-28 09:26:42 +00:00
Charles Bourque
6273020127 Merge pull request #133 from stephb9959/main
[WIFI-11730] & [WIFI-11742]
2022-11-28 09:10:28 +00:00
Johann Hoffmann
95963eb0be Merge pull request #132 from Telecominfraproject/WIFI-11419-patch-workflows
[WIFI-11419] Patch workflows with regard to deprecated Github actions commands
2022-11-25 15:25:22 +01:00
Charles Bourque
7ac82d4ad9 Merge pull request #131 from stephb9959/main
[WIFI-11728] Added 6G associations to device table
2022-11-24 09:31:30 +00:00
Johann Hoffmann
c23cce672c Update checkout action version
Signed-off-by: Johann Hoffmann <johann.hoffmann@mailbox.org>
2022-11-22 11:55:57 +01:00
Charles Bourque
d39b4b3624 Merge pull request #130 from stephb9959/main
[WIFI-11251] Now fetching device statistics in batches of 100
2022-11-20 18:09:06 +00:00
Charles Bourque
86f2ffa61f Merge pull request #129 from stephb9959/main
[WIFI-11700] Fix for non-lowercase serial numbers in device page
2022-11-20 10:16:46 +00:00
Charles Bourque
7d72ad0f37 Merge pull request #128 from stephb9959/main
[WIFI-11594] Fixed hidden columns in wifi analysis
2022-11-14 16:03:19 +00:00
Charles Bourque
c21bf5b87d Merge pull request #127 from stephb9959/main
[WIFI-11584]  Reconnecting WebSocket when focusing on search bar
2022-11-13 10:13:03 +00:00
Charles Bourque
d80d5557c8 Merge pull request #126 from stephb9959/main
[WIFI-11545] Fixed event queue API
2022-11-11 13:34:50 +00:00
Charles Bourque
d72867da35 Merge pull request #125 from stephb9959/main
[WIFI-11543] Added API keys to profile page
2022-11-11 12:50:07 +00:00
Charles Bourque
11e2bf4cbb Merge pull request #124 from stephb9959/main
[WIFI-11566] Fixed uses of useClipboard
2022-11-10 10:21:57 +00:00
Charles Bourque
58f8a02557 Merge pull request #123 from stephb9959/main
[WIFI-11564] Add logs page
2022-11-09 17:36:48 +00:00
Charles Bourque
f9e08d53af Merge pull request #122 from stephb9959/main
[WIFI-11563] Added import file button to configure modal
2022-11-09 12:45:27 +00:00
Charles Bourque
8132012534 Merge pull request #121 from stephb9959/main
[WIFI-11472] Fixed crash on missing device types
2022-11-03 14:53:24 +00:00
Dmitry Dunaev
7312980453 Merge pull request #120 from Telecominfraproject/feature/wifi-11464--entrypoint
[WIFI-11464] Fix: entrypoint script
2022-11-03 11:30:47 +03:00
Dmitry Dunaev
8c20d41d89 [WIFI-11464] Fix: entrypoint script
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-11-03 10:33:52 +03:00
Dmitry Dunaev
d50d53ac1f Merge pull request #119 from Telecominfraproject/feature/wifi-11451--build-fix
[WIFI-11451] Fix: Dockerfile
2022-11-02 14:56:50 +03:00
Dmitry Dunaev
b0d7ab2e81 [WIFI-11451] Fix: Dockerfile
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-11-02 14:56:18 +03:00
Charles Bourque
91223b7518 Merge pull request #118 from stephb9959/main
[WIFI-11454] Display restricted devices and certificate expiry date
2022-11-02 10:33:48 +00:00
Charles Bourque
5170ea81e7 Merge pull request #117 from stephb9959/main
[WIFI-11455] Fix to fetch env file for docker build
2022-11-01 16:54:27 +00:00
Charles Bourque
2229e8cb7d Merge pull request #116 from stephb9959/main
[WIFI-11392] Refetch all endpoints on System page
2022-10-30 12:56:05 +00:00
Charles Bourque
187065098b Merge pull request #115 from stephb9959/main
[WIFI-11223] Migrating to prov-ui style/libraries
2022-10-27 12:35:04 +01:00
Charles Bourque
2fc93fa819 Merge pull request #113 from stephb9959/main
[WIFI-10931] Fixed using ws websockets when using http GW endpoint
2022-10-10 10:02:47 +01:00
Dmitry Dunaev
0f40c4cd49 Fix: Helm image for main branch
Signed-off-by: Dmitry Dunaev <dmitry@opsfleet.com>
2022-09-30 16:44:39 +03: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 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 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
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 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 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 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 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
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 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 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 Bourque
6c437459ca Merge pull request #98 from stephb9959/main
2.6.29
2022-06-29 20:51:47 +01:00
131 changed files with 5331 additions and 650 deletions

1
.env
View File

@@ -1 +0,0 @@
VITE_UCENTRALSEC_URL="https://ucentral.dpaas.arilia.com:16001"

View File

@@ -20,13 +20,13 @@ defaults:
jobs:
docker:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
env:
DOCKER_REGISTRY_URL: tip-tip-wlan-cloud-ucentral.jfrog.io
DOCKER_REGISTRY_USERNAME: ucentral
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: Telecominfraproject/.github
path: github
@@ -56,7 +56,7 @@ jobs:
- docker
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: Telecominfraproject/.github
path: github

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout actions repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: Telecominfraproject/.github
path: github

View File

@@ -11,13 +11,13 @@ defaults:
jobs:
helm-package:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
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
uses: actions/checkout@v3
with:
path: wlan-cloud-ucentralgw-ui

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@
.env.production.local
npm-debug.log*
.vscode/settings.json

View File

@@ -1,6 +1,8 @@
FROM node:14-alpine3.11 AS build
FROM node:18.7.0-alpine3.15 AS build
COPY package.json package-lock.json /
WORKDIR /app
COPY package.json package-lock.json /app/
RUN npm install
@@ -8,8 +10,8 @@ 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/
COPY --from=build /app/build/ /usr/share/nginx/html/
COPY --from=build docker-entrypoint.d/40-generate-config.sh /docker-entrypoint.d/40-generate-config.sh
COPY --from=build /app/docker-entrypoint.d/40-generate-config.sh /docker-entrypoint.d/40-generate-config.sh

View File

@@ -1,6 +1,32 @@
#!/bin/ash
# Check if variables are set
export DEFAULT_OWSEC_URL="${DEFAULT_OWSEC_URL:-https://ucentral.dpaas.arilia.com:16001}"
export ALLOW_OWSEC_CHANGE="${ALLOW_OWSEC_CHANGE:-false}"
echo '{"DEFAULT_UCENTRALSEC_URL": "'$DEFAULT_UCENTRALSEC_URL'","ALLOW_UCENTRALSEC_CHANGE": '$ALLOW_UCENTRALSEC_CHANGE'}' > /usr/share/nginx/html/config.json
ENV_CONFIG_PATH=/usr/share/nginx/html/env-config.js
# Recreate config file
rm -rf $ENV_CONFIG_PATH
touch $ENV_CONFIG_PATH
# Add assignment
echo "window._env_ = {" >> $ENV_CONFIG_PATH
# Read each line in .env file
# Each line represents key=value pairs
env | grep REACT_ | while read -r line || [[ -n "$line" ]];
do
echo $line
# Split env variables by character `=`
if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
fi
# Read value of current variable if exists as Environment variable
value=$(printf '%s\n' "${!varname}")
# Otherwise use value from .env file
[[ -z $value ]] && value=${varvalue}
# Append configuration property to JS file
echo " $varname: \"$value\"," >> $ENV_CONFIG_PATH
done
echo "}" >> $ENV_CONFIG_PATH

View File

@@ -17,7 +17,9 @@ metadata:
{{- end }}
spec:
{{- if $ingressValue.className }}
ingressClassName: {{ $ingressValue.className }}
{{- end }}
{{- if $ingressValue.tls }}
tls:
{{- range $ingressValue.tls }}

View File

@@ -8,7 +8,7 @@ fullnameOverride: ""
images:
owgwui:
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
tag: main
tag: v4.0.0
pullPolicy: Always
services:
@@ -75,5 +75,4 @@ podAnnotations: {}
# Application
public_env_variables:
DEFAULT_UCENTRALSEC_URL: https://ucentral.dpaas.arilia.com:16001
ALLOW_UCENTRALSEC_CHANGE: false
REACT_APP_UCENTRALSEC_URL: https://ucentral.dpaas.arilia.com:16001

1018
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.10.0(49)",
"version": "4.0.0",
"description": "",
"private": true,
"main": "index.tsx",
@@ -15,8 +15,10 @@
"author": "",
"license": "ISC",
"dependencies": {
"@chakra-ui/anatomy": "^2.1.1",
"@chakra-ui/icons": "^2.0.18",
"@chakra-ui/react": "^2.3.6",
"@chakra-ui/styled-system": "^2.9.0",
"@chakra-ui/theme-tools": "^2.0.12",
"@chakra-ui/utils": "^2.0.14",
"@emotion/react": "^11.10.6",
@@ -58,7 +60,7 @@
"react-virtualized-auto-sizer": "^1.0.15",
"react-window": "^1.8.9",
"source-map-explorer": "^2.5.3",
"typescript": "^4.8.4",
"typescript": "^5.0.4",
"uuid": "^9.0.0",
"vite": "^4.2.1",
"yup": "^0.32.11",
@@ -92,6 +94,7 @@
"lint-staged": "^13.2.1",
"prettier": "^2.8.7",
"vite-plugin-pwa": "^0.14.7",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.2.0"
},
"browserslist": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 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: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -223,6 +223,7 @@
"day": "Tag",
"days": "Tage",
"default": "Standard",
"defaults": "Standardeinstellungen",
"description": "Beschreibung",
"details": "Einzelheiten",
"device_details": "Gerätedetails",
@@ -268,6 +269,7 @@
"map": "Karte",
"max": "Max",
"min": "MINDEST",
"miscellaneous": "Verschiedenes",
"mode": "Modus",
"model": "Modell",
"modified": "Geändert",
@@ -702,11 +704,32 @@
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
},
"firmware": {
"confirm_default_data": "Bitte bestätigen Sie die untenstehenden Informationen und klicken Sie auf „Bestätigen“, sobald Sie bereit sind, den Vorgang zu starten",
"create_success": "Neue Standard-Firmware-Einstellungen erstellt!",
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
"default_created_error_one": "{{count}} Fehler beim Versuch, eine neue Einstellung zu erstellen",
"default_created_error_other": "{{count}} Fehler beim Versuch, eine neue Einstellung zu erstellen",
"default_created_one": "{{count}} Standard-Firmware-Einstellung erstellt",
"default_created_other": "{{count}} Standard-Firmware-Einstellungen erstellt",
"default_found_one": "Für den Gerätetyp {{count}} wurde eine gültige Revision gefunden",
"default_found_other": "Gültige Revisionen für {{count}} Gerätetypen gefunden",
"default_mass_delete_success_one": " {{count}} Standard-Firmware-Einstellung gelöscht!",
"default_mass_delete_success_other": " {{count}} Standard-Firmware-Einstellungen gelöscht!",
"default_not_found_one": "Keine gültigen Firmware-Versionen für den Gerätetyp {{count}} ",
"default_not_found_other": "Keine gültigen Firmware-Versionen für {{count}} Gerätetypen",
"default_title": "",
"default_update_success": "Standard-Firmware für {{deviceType}}aktualisiert!",
"delete_success": "Standard-Firmware-Einstellung gelöscht!",
"edit_default_title": "Dies ist die aktuelle Firmware, die als Mindestversion für neue APs vom Typ {{deviceType}}verwendet wird. Wenn ein neuer {{deviceType}} AP eine Verbindung zum Gateway herstellt, wird er automatisch auf diese Version aktualisiert.",
"fetching_defaults": "Alle verfügbaren Firmware für ausgewählte Gerätetypen werden abgerufen...",
"last_db_update_modal": "Firmware-Datenbank",
"last_db_update_title": "Datenbank",
"one": "Firmware",
"select_default_device_types": "Bitte wählen Sie alle Gerätetypen aus, auf die Sie diese neue Standard-Firmware-Regel anwenden möchten. Wenn Sie den gewünschten Gerätetyp nicht finden können, bedeutet dies, dass bereits eine Regel angewendet wurde.",
"select_default_revision": "Sie können jetzt die Mindestversion auswählen, auf die Ihre Gerätetypen abzielen sollen",
"start_db_update": "Datenbankaktualisierung starten",
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern",
"update_success": "Standard-Firmware-Informationen gespeichert!"
},
"footer": {
"powered_by": "Unterstützt von",
@@ -715,6 +738,7 @@
"form": {
"captive_web_root_explanation": "Bitte verwenden Sie nur .tar-Dateien (keine komprimierten Dateien wie z. B. .targz)",
"certificate_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN CERTIFICATE-----“ beginnt und mit „-----END CERTIFICATE-----“ endet.",
"invalid_alphanumeric_with_dash": "Akzeptierte Zeichen. sind nur alphanumerisch (Buchstaben & Zahlen)",
"invalid_cidr": "Ungültige CIDR-IPv4-Adresse. Beispiel: 192.168.0.1/12",
"invalid_email": "Ungültige E-Mail",
"invalid_file_content": "Ungültiger Dateiinhalt, bitte bestätigen Sie, dass es sich um ein gültiges Format handelt",
@@ -741,7 +765,11 @@
"invalid_static_ipv4_e": "Ungültige Adresse, dieser Bereich ist für Experimente reserviert (Klasse E). Das erste Oktett sollte 223 oder niedriger sein",
"invalid_third_party": "Ungültige Drittanbieter-JSON-Zeichenfolge. Bitte bestätigen Sie, dass Ihr Wert ein gültiges JSON ist",
"key_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN PRIVATE KEY-----“ beginnt und mit „-----END PRIVATE KEY-----“ endet.",
"max_length": "Maximale Länge von {{max}} Zeichen.",
"max_value": "Maximalwert von {{max}}",
"min_length": "Mindestlänge von {{min}} Zeichen.",
"min_max_string": "Der Wert muss eine Länge zwischen {{min}} (einschließlich) und {{max}} (einschließlich) haben.",
"min_value": "Mindestwert von {{min}}",
"missing_interface_upstream": "Sie müssen mindestens eine Upstream-Schnittstelle haben. Im Moment sind alle Ihre Schnittstellen nachgelagert",
"new_email_to_notify": "Neue E-Mail zur Benachrichtigung",
"new_phone_to_notify": "Neues Telefon zu benachrichtigen",
@@ -883,6 +911,11 @@
"one": "Benachrichtigung",
"other": "Benachrichtigungen"
},
"openroaming": {
"pool_strategy": "Pool-Strategie",
"radius_endpoint_one": "Radiusendpunkt",
"radius_endpoint_other": "Radiusendpunkte"
},
"operator": {
"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar",
"delete_operator": "Betreiber löschen",
@@ -948,6 +981,27 @@
"title": "Beschränkungen",
"tty": "TTY-Zugriff"
},
"roaming": {
"account_created": "Neues Konto erstellt!",
"account_deleted": "Konto gelöscht!",
"account_one": "Konto",
"account_other": "Konten",
"certificate_deleted": "Zertifikat gelöscht!",
"certificate_one": "Zertifikat",
"certificate_other": "Zertifikate",
"city": "Stadt",
"common_name": "Gemeinsamen Namen",
"country": "Land",
"global_reach": "Globale Reichweite",
"global_reach_account_id": "Konto-ID",
"invalid_certificate": "Ungültiges Zertifikat",
"invalid_key": "Ungültiger privater Schlüssel",
"location_details_title": "Ort",
"organization": "Organisation",
"private_key": "Privat Schlüssel",
"province": "Provinz",
"state": "Zustand"
},
"rrm": {
"algorithm": "Algorithmus",
"algorithm_other": "Algorithmen",
@@ -1008,6 +1062,9 @@
"current_live_devices": "Aktuelle Live-Geräte",
"currently_running_one": "Derzeit wird {{count}} Simulation ausgeführt",
"currently_running_other": "Derzeit laufen {{count}} Simulationen",
"delete_devices_confirm": "Sind Sie sicher, dass Sie alle Geräte und deren Statistiken vom Gateway entfernen möchten? Diese Aktion ist nicht rückgängig zu machen",
"delete_devices_loading": "Dieser Vorgang kann bis zu 5 Minuten dauern",
"delete_simulation_devices": "Geräte löschen",
"delete_success": "Gelöschte Simulation!",
"duration": "Dauer",
"error_devices": "Fehler Geräte",
@@ -1066,6 +1123,7 @@
"title": "Abonnenten"
},
"system": {
"advanced": "Erweitert",
"backend_logs": "Back-End-Protokolle",
"configuration": "Aufbau",
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",

View File

@@ -223,6 +223,7 @@
"day": "Day",
"days": "Days",
"default": "Default",
"defaults": "Defaults",
"description": "Description",
"details": "Details",
"device_details": "Device Details",
@@ -268,6 +269,7 @@
"map": "Map",
"max": "Max",
"min": "Min",
"miscellaneous": "Miscellaneous",
"mode": "Mode",
"model": "Model",
"modified": "Modified",
@@ -702,11 +704,32 @@
"venues_under_root": "Venues cannot be created directly under the root entity"
},
"firmware": {
"confirm_default_data": "Please confirm the information below and click 'Confirm' once you are ready to start the process",
"create_success": "Created new default firmware settings!",
"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes",
"default_created_error_one": "{{count}} error while trying to create new setting",
"default_created_error_other": "{{count}} errors while trying to create new setting",
"default_created_one": "{{count}} default firmware setting created",
"default_created_other": "{{count}} default firmware settings created",
"default_found_one": "Found valid revision for {{count}} device type",
"default_found_other": "Found valid revisions for {{count}} device types",
"default_mass_delete_success_one": "Deleted {{count}} default firmware setting!",
"default_mass_delete_success_other": "Deleted {{count}} default firmware settings!",
"default_not_found_one": "No valid firmware versions for {{count}} device type",
"default_not_found_other": "No valid firmware versions for {{count}} device types",
"default_title": "Default Firmware",
"default_update_success": "Updated default firmware for {{deviceType}}!",
"delete_success": "Deleted default firmware setting!",
"edit_default_title": "This is the current firmware that is used as the minimum version for new APs of type {{deviceType}}. If a new {{deviceType}} AP connects to the gateway, it will be automatically upgraded to this version.",
"fetching_defaults": "Fetching all available firmware for selected device types...",
"last_db_update_modal": "Firmware Database",
"last_db_update_title": "Database",
"one": "Firmware",
"select_default_device_types": "Please select all device types that you want to target with this new default firmware rule. If you cannot find your desired device type, it means they already have an applied rule.",
"select_default_revision": "You can now select the minimum revision you want your device types to target",
"start_db_update": "Start Database Update",
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete"
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete",
"update_success": "Saved default firmware information!"
},
"footer": {
"powered_by": "Powered By",
@@ -715,6 +738,7 @@
"form": {
"captive_web_root_explanation": "Please use .tar files only (no compressed files like .targz, for example)",
"certificate_file_explanation": "Please use a .pem file that starts with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"",
"invalid_alphanumeric_with_dash": "Accepted chars. are only alphanumeric (letters & numbers)",
"invalid_cidr": "Invalid CIDR IPv4 address. Example: 192.168.0.1/12",
"invalid_email": "Invalid Email",
"invalid_file_content": "Invalid file content, please confirm that it is of the valid format",
@@ -741,7 +765,11 @@
"invalid_static_ipv4_e": "Invalid address, this range reserved for experiments (class E). The first octet should be 223 or lower",
"invalid_third_party": "Invalid Third Party JSON string. Please confirm that your value is a valid JSON",
"key_file_explanation": "Please use a .pem file that starts with \"-----BEGIN PRIVATE KEY-----\" and ends with \"-----END PRIVATE KEY-----\"",
"max_length": "Maximum length of {{max}} chars.",
"max_value": "Maximum value of {{max}}",
"min_length": "Minimum length of {{min}} chars.",
"min_max_string": "Value needs to be of a length between {{min}} (inclusive) and {{max}} (inclusive)",
"min_value": "Minimum value of {{min}}",
"missing_interface_upstream": "You need to have at least one upstream interface. At the moment, all your interfaces are downstream",
"new_email_to_notify": "New email to notify",
"new_phone_to_notify": "New phone to notify",
@@ -883,6 +911,11 @@
"one": "Notification",
"other": "Notifications"
},
"openroaming": {
"pool_strategy": "Pool Strategy",
"radius_endpoint_one": "Radius Endpoint",
"radius_endpoint_other": "Radius Endpoints"
},
"operator": {
"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible",
"delete_operator": "Delete Operator",
@@ -948,6 +981,27 @@
"title": "Restrictions",
"tty": "TTY Access"
},
"roaming": {
"account_created": "New account created!",
"account_deleted": "Deleted account!",
"account_one": "Account",
"account_other": "Accounts",
"certificate_deleted": "Deleted certificate!",
"certificate_one": "Certificate",
"certificate_other": "Certificates",
"city": "City",
"common_name": "Common Name",
"country": "Country",
"global_reach": "GlobalReach",
"global_reach_account_id": " Account ID",
"invalid_certificate": "Invalid certificate",
"invalid_key": "Invalid private key",
"location_details_title": "Location",
"organization": "Organization",
"private_key": "Private Key",
"province": "Province",
"state": "State"
},
"rrm": {
"algorithm": "Algorithm",
"algorithm_other": "Algorithms",
@@ -1008,6 +1062,9 @@
"current_live_devices": "Current Live Devices",
"currently_running_one": "There is currently {{count}} simulation running",
"currently_running_other": "There are currently {{count}} simulations running",
"delete_devices_confirm": "Are you sure you want to remove all devices and their statistics from the gateway? This action is not reversible",
"delete_devices_loading": "This process may take up to 5 minutes",
"delete_simulation_devices": "Delete Devices",
"delete_success": "Deleted Simulation!",
"duration": "Duration",
"error_devices": "Error Devices",
@@ -1066,6 +1123,7 @@
"title": "Subscribers"
},
"system": {
"advanced": "Advanced",
"backend_logs": "Back-End Logs",
"configuration": "Configuration",
"could_not_retrieve": "Error: could not retrieve {{name}} system information",

View File

@@ -223,6 +223,7 @@
"day": "Día",
"days": "días",
"default": "Defecto",
"defaults": "Valores predeterminados",
"description": "Descripción",
"details": "Detalles",
"device_details": "Detalles del dispositivo",
@@ -268,6 +269,7 @@
"map": "Mapa",
"max": "Max",
"min": "Min",
"miscellaneous": "Diverso",
"mode": "Modo",
"model": "Modelo",
"modified": "Modificado",
@@ -702,11 +704,32 @@
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
},
"firmware": {
"confirm_default_data": "Confirme la información a continuación y haga clic en 'Confirmar' una vez que esté listo para comenzar el proceso",
"create_success": "¡Se crearon nuevas configuraciones de firmware predeterminadas!",
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
"default_created_error_one": "{{count}} error al intentar crear una nueva configuración",
"default_created_error_other": "{{count}} errores al intentar crear una nueva configuración",
"default_created_one": "{{count}} configuración de firmware predeterminada creada",
"default_created_other": "{{count}} ajustes de firmware predeterminados creados",
"default_found_one": "Se encontró una revisión válida para el tipo de dispositivo {{count}} ",
"default_found_other": "Se encontraron revisiones válidas para {{count}} tipos de dispositivos",
"default_mass_delete_success_one": "¡Se eliminó {{count}} configuración de firmware predeterminada!",
"default_mass_delete_success_other": "¡Se eliminaron {{count}} configuraciones de firmware predeterminadas!",
"default_not_found_one": "No hay versiones de firmware válidas para el tipo de dispositivo {{count}} ",
"default_not_found_other": "No hay versiones de firmware válidas para {{count}} tipos de dispositivos",
"default_title": "",
"default_update_success": "¡Firmware predeterminado actualizado para {{deviceType}}!",
"delete_success": "¡Configuración de firmware predeterminada eliminada!",
"edit_default_title": "Este es el firmware actual que se utiliza como versión mínima para los nuevos AP de tipo {{deviceType}}. Si un nuevo AP {{deviceType}} se conecta a la puerta de enlace, se actualizará automáticamente a esta versión.",
"fetching_defaults": "Obteniendo todo el firmware disponible para los tipos de dispositivos seleccionados...",
"last_db_update_modal": "Base de datos de firmware",
"last_db_update_title": "Base de datos",
"one": "Firmware",
"select_default_device_types": "Seleccione todos los tipos de dispositivos a los que desea apuntar con esta nueva regla de firmware predeterminada. Si no puede encontrar el tipo de dispositivo deseado, significa que ya tienen una regla aplicada.",
"select_default_revision": "Ahora puede seleccionar la revisión mínima a la que desea que se dirijan sus tipos de dispositivos",
"start_db_update": "Iniciar actualización de la base de datos",
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse",
"update_success": "¡Información de firmware predeterminada guardada!"
},
"footer": {
"powered_by": "energizado por",
@@ -715,6 +738,7 @@
"form": {
"captive_web_root_explanation": "Utilice únicamente archivos .tar (no archivos comprimidos como .targz, por ejemplo)",
"certificate_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN CERTIFICATE-----\" y termine con \"-----END CERTIFICATE-----\"",
"invalid_alphanumeric_with_dash": "Caracteres aceptados. son solo alfanuméricos (letras y números)",
"invalid_cidr": "Dirección IPv4 CIDR no válida. Ejemplo: 192.168.0.1/12",
"invalid_email": "Email inválido",
"invalid_file_content": "Contenido de archivo no válido, confirme que tiene un formato válido",
@@ -741,7 +765,11 @@
"invalid_static_ipv4_e": "Dirección no válida, este rango reservado para experimentos (clase E). El primer octeto debe ser 223 o inferior",
"invalid_third_party": "Cadena JSON de terceros no válida. Confirme que su valor es un JSON válido",
"key_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN PRIVATE KEY-----\" y termine con \"-----END PRIVATE KEY-----\"",
"max_length": "Longitud máxima de {{max}} caracteres.",
"max_value": "Valor máximo de {{max}}",
"min_length": "Longitud mínima de {{min}} caracteres.",
"min_max_string": "El valor debe tener una longitud entre {{min}} (inclusive) y {{max}} (inclusive)",
"min_value": "Valor mínimo de {{min}}",
"missing_interface_upstream": "Debe tener al menos una interfaz ascendente. Por el momento, todas sus interfaces están en sentido descendente",
"new_email_to_notify": "Nuevo correo electrónico para notificar",
"new_phone_to_notify": "Nuevo teléfono para avisar",
@@ -883,6 +911,11 @@
"one": "Notificación",
"other": "Notificaciones"
},
"openroaming": {
"pool_strategy": "Estrategia de piscina",
"radius_endpoint_one": "Punto final del radio",
"radius_endpoint_other": "Puntos finales de radio"
},
"operator": {
"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.",
"delete_operator": "Eliminar operador",
@@ -948,6 +981,27 @@
"title": "Las restricciones",
"tty": "Acceso TTY"
},
"roaming": {
"account_created": "¡Nueva cuenta creada!",
"account_deleted": "¡Cuenta eliminada!",
"account_one": "Cuenta",
"account_other": "Cuentas",
"certificate_deleted": "Certificado eliminado!",
"certificate_one": "Certificado",
"certificate_other": "Certificados",
"city": "ciudad",
"common_name": "Nombre común",
"country": "País",
"global_reach": "Alcance global",
"global_reach_account_id": "ID de cuenta ",
"invalid_certificate": "Certificado inválido",
"invalid_key": "Clave privada no válida",
"location_details_title": "Ubicación",
"organization": "Organización",
"private_key": "Llave privada",
"province": "Provincia",
"state": "Estado"
},
"rrm": {
"algorithm": "Algoritmo",
"algorithm_other": "Algoritmos",
@@ -1008,6 +1062,9 @@
"current_live_devices": "Dispositivos activos actuales",
"currently_running_one": "Actualmente hay {{count}} simulación en ejecución",
"currently_running_other": "Actualmente hay {{count}} simulaciones ejecutándose",
"delete_devices_confirm": "¿Está seguro de que desea eliminar todos los dispositivos y sus estadísticas de la puerta de enlace? Esta acción no es reversible",
"delete_devices_loading": "Este proceso puede tardar hasta 5 minutos.",
"delete_simulation_devices": "BORRAR DISPOSITIVOS",
"delete_success": "¡Simulación eliminada!",
"duration": "Duración",
"error_devices": "Dispositivos de error",
@@ -1066,6 +1123,7 @@
"title": "Suscriptores"
},
"system": {
"advanced": "Avanzado",
"backend_logs": "Registros de back-end",
"configuration": "Configuración",
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",

View File

@@ -223,6 +223,7 @@
"day": "journée",
"days": "Journées",
"default": "Défaut",
"defaults": "Valeurs par défaut",
"description": "La description",
"details": "Détails",
"device_details": "Détails de l'appareil",
@@ -268,6 +269,7 @@
"map": "Carte",
"max": "Max",
"min": "Min",
"miscellaneous": "Divers",
"mode": "Mode",
"model": "Modèle",
"modified": "Modifié",
@@ -702,11 +704,32 @@
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
},
"firmware": {
"confirm_default_data": "Veuillez confirmer les informations ci-dessous et cliquez sur \"Confirmer\" une fois que vous êtes prêt à démarrer le processus",
"create_success": "Création de nouveaux paramètres de firmware par défaut !",
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
"default_created_error_one": "{{count}} erreur lors de la tentative de création d'un nouveau paramètre",
"default_created_error_other": "{{count}} erreurs lors de la tentative de création d'un nouveau paramètre",
"default_created_one": "{{count}} paramètre de micrologiciel par défaut créé",
"default_created_other": "{{count}} paramètres de micrologiciel par défaut créés",
"default_found_one": "Révision valide trouvée pour le type d'appareil {{count}} ",
"default_found_other": "Révisions valides trouvées pour {{count}} types d'appareils",
"default_mass_delete_success_one": "Paramètre de micrologiciel par défaut {{count}} supprimé !",
"default_mass_delete_success_other": " {{count}} paramètres de micrologiciel par défaut supprimés !",
"default_not_found_one": "Aucune version de micrologiciel valide pour le type d'appareil {{count}} ",
"default_not_found_other": "Aucune version de micrologiciel valide pour {{count}} types d'appareils",
"default_title": "",
"default_update_success": "Firmware par défaut mis à jour pour {{deviceType}} !",
"delete_success": "Paramètre de micrologiciel par défaut supprimé !",
"edit_default_title": "Il s'agit du micrologiciel actuel utilisé comme version minimale pour les nouveaux points d'accès de type {{deviceType}}. Si un nouveau point d'accès {{deviceType}} se connecte à la passerelle, il sera automatiquement mis à niveau vers cette version.",
"fetching_defaults": "Récupération de tous les micrologiciels disponibles pour les types d'appareils sélectionnés...",
"last_db_update_modal": "Base de données du micrologiciel",
"last_db_update_title": "Base de données",
"one": "Micrologiciel",
"select_default_device_types": "Veuillez sélectionner tous les types d'appareils que vous souhaitez cibler avec cette nouvelle règle de micrologiciel par défaut. Si vous ne trouvez pas le type d'appareil souhaité, cela signifie qu'une règle est déjà appliquée.",
"select_default_revision": "Vous pouvez maintenant sélectionner la révision minimale que vous souhaitez que vos types d'appareils ciblent",
"start_db_update": "Démarrer la mise à jour de la base de données",
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes",
"update_success": "Informations sur le micrologiciel par défaut enregistrées !"
},
"footer": {
"powered_by": "Alimenté par",
@@ -715,6 +738,7 @@
"form": {
"captive_web_root_explanation": "Veuillez utiliser uniquement des fichiers .tar (pas de fichiers compressés comme .targz, par exemple)",
"certificate_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN CERTIFICATE-----\" et se termine par \"-----END CERTIFICATE-----\"",
"invalid_alphanumeric_with_dash": "Caractères acceptés. sont uniquement alphanumériques (lettres et chiffres)",
"invalid_cidr": "Adresse IPv4 CIDR non valide. Exemple : 192.168.0.1/12",
"invalid_email": "Email Invalide",
"invalid_file_content": "Contenu de fichier non valide, veuillez confirmer qu'il est au format valide",
@@ -741,7 +765,11 @@
"invalid_static_ipv4_e": "Adresse invalide, cette plage est réservée aux expérimentations (classe E). Le premier octet doit être 223 ou moins",
"invalid_third_party": "Chaîne JSON tierce non valide. Veuillez confirmer que votre valeur est un JSON valide",
"key_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN PRIVATE KEY-----\" et se termine par \"-----END PRIVATE KEY-----\"",
"max_length": "Longueur maximale de {{max}} caractères.",
"max_value": "Valeur maximale de {{max}}",
"min_length": "Longueur minimale de {{min}} caractères.",
"min_max_string": "La valeur doit être d'une longueur comprise entre {{min}} (inclus) et {{max}} (inclus)",
"min_value": "Valeur minimale de {{min}}",
"missing_interface_upstream": "Vous devez avoir au moins une interface en amont. Pour le moment, toutes vos interfaces sont en aval",
"new_email_to_notify": "Nouvel e-mail à notifier",
"new_phone_to_notify": "Nouveau téléphone à notifier",
@@ -883,6 +911,11 @@
"one": "Notification",
"other": "Les notifications"
},
"openroaming": {
"pool_strategy": "Stratégie de pool",
"radius_endpoint_one": "Point final de rayon",
"radius_endpoint_other": "Points de terminaison du rayon"
},
"operator": {
"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible",
"delete_operator": "Supprimer l'opérateur",
@@ -948,6 +981,27 @@
"title": "Restrictions",
"tty": "Accès ATS"
},
"roaming": {
"account_created": "Nouveau compte créé !",
"account_deleted": "Compte supprimé !",
"account_one": "Compte",
"account_other": "Comptes",
"certificate_deleted": "Certificat supprimé !",
"certificate_one": "Certificat",
"certificate_other": "Certificats",
"city": "Ville",
"common_name": "Nom commun",
"country": "Pays",
"global_reach": "Portée mondiale",
"global_reach_account_id": "ID de compte ",
"invalid_certificate": "certificat invalide",
"invalid_key": "Clé privée invalide",
"location_details_title": "Emplacement",
"organization": "Organisation",
"private_key": "Clé privée",
"province": "province",
"state": "Etat"
},
"rrm": {
"algorithm": "Algorithme",
"algorithm_other": "Algorithmes",
@@ -1008,6 +1062,9 @@
"current_live_devices": "Appareils en direct actuels",
"currently_running_one": "Il y a actuellement {{count}} simulation en cours",
"currently_running_other": "Il y a actuellement {{count}} simulations en cours d'exécution",
"delete_devices_confirm": "Voulez-vous vraiment supprimer tous les appareils et leurs statistiques de la passerelle ? Cette action n'est pas réversible",
"delete_devices_loading": "Ce processus peut prendre jusqu'à 5 minutes",
"delete_simulation_devices": "Supprimer des appareils",
"delete_success": "Simulation supprimée !",
"duration": "Durée",
"error_devices": "Périphériques d'erreur",
@@ -1066,6 +1123,7 @@
"title": "Les abonnés"
},
"system": {
"advanced": "Avancée",
"backend_logs": "Journaux principaux",
"configuration": "Configuration",
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",

View File

@@ -223,6 +223,7 @@
"day": "Dia",
"days": "Dias",
"default": "Padrão",
"defaults": "Predefinições",
"description": "Descrição",
"details": "Detalhes",
"device_details": "Detalhes do dispositivo",
@@ -268,6 +269,7 @@
"map": "Mapa",
"max": "máximo",
"min": "minuto",
"miscellaneous": "Diversos",
"mode": "Modo",
"model": "Modelo",
"modified": "Modificado",
@@ -702,11 +704,32 @@
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
},
"firmware": {
"confirm_default_data": "Confirme as informações abaixo e clique em 'Confirmar' quando estiver pronto para iniciar o processo",
"create_success": "Criou novas configurações de firmware padrão!",
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
"default_created_error_one": "{{count}} erro ao tentar criar uma nova configuração",
"default_created_error_other": "{{count}} erros ao tentar criar uma nova configuração",
"default_created_one": "{{count}} configuração de firmware padrão criada",
"default_created_other": "{{count}} configurações de firmware padrão criadas",
"default_found_one": "Revisão válida encontrada para {{count}} tipo de dispositivo",
"default_found_other": "Foram encontradas revisões válidas para {{count}} tipos de dispositivo",
"default_mass_delete_success_one": "Configuração de firmware padrão {{count}} excluída!",
"default_mass_delete_success_other": "Excluídas {{count}} configurações de firmware padrão!",
"default_not_found_one": "Nenhuma versão de firmware válida para {{count}} tipo de dispositivo",
"default_not_found_other": "Nenhuma versão de firmware válida para {{count}} tipos de dispositivo",
"default_title": "",
"default_update_success": "Firmware padrão atualizado para {{deviceType}}!",
"delete_success": "Configuração de firmware padrão excluída!",
"edit_default_title": "Este é o firmware atual usado como versão mínima para novos APs do tipo {{deviceType}}. Se um novo AP {{deviceType}} se conectar ao gateway, ele será atualizado automaticamente para esta versão.",
"fetching_defaults": "Buscando todo o firmware disponível para os tipos de dispositivos selecionados...",
"last_db_update_modal": "banco de dados de firmware",
"last_db_update_title": "base de dados",
"one": "Firmware",
"select_default_device_types": "Selecione todos os tipos de dispositivos que deseja segmentar com esta nova regra de firmware padrão. Se você não conseguir encontrar o tipo de dispositivo desejado, significa que eles já têm uma regra aplicada.",
"select_default_revision": "Agora você pode selecionar a revisão mínima para a qual deseja que seus tipos de dispositivo sejam direcionados",
"start_db_update": "Iniciar atualização do banco de dados",
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída",
"update_success": "Informações de firmware padrão salvas!"
},
"footer": {
"powered_by": "Distribuído por",
@@ -715,6 +738,7 @@
"form": {
"captive_web_root_explanation": "Por favor, use apenas arquivos .tar (sem arquivos compactados como .targz, por exemplo)",
"certificate_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN CERTIFICATE-----\" e termine com \"-----END CERTIFICATE-----\"",
"invalid_alphanumeric_with_dash": "Caracteres aceitos. são apenas alfanuméricos (letras e números)",
"invalid_cidr": "Endereço CIDR IPv4 inválido. Exemplo: 192.168.0.1/12",
"invalid_email": "E-mail inválido",
"invalid_file_content": "Conteúdo de arquivo inválido. Confirme se está no formato válido",
@@ -741,7 +765,11 @@
"invalid_static_ipv4_e": "Endereço inválido, este intervalo é reservado para experimentos (classe E). O primeiro octeto deve ser 223 ou inferior",
"invalid_third_party": "String JSON de terceiros inválida. Confirme se seu valor é um JSON válido",
"key_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN PRIVATE KEY-----\" e termine com \"-----END PRIVATE KEY-----\"",
"max_length": "Comprimento máximo de {{max}} caracteres.",
"max_value": "Valor máximo de {{max}}",
"min_length": "Comprimento mínimo de {{min}} caracteres.",
"min_max_string": "O valor precisa ter um comprimento entre {{min}} (inclusive) e {{max}} (inclusive)",
"min_value": "Valor mínimo de {{min}}",
"missing_interface_upstream": "Você precisa ter pelo menos uma interface upstream. No momento, todas as suas interfaces estão downstream",
"new_email_to_notify": "Novo e-mail para notificar",
"new_phone_to_notify": "Novo telefone para notificar",
@@ -883,6 +911,11 @@
"one": "Notificação",
"other": "Notificações"
},
"openroaming": {
"pool_strategy": "Estratégia de pool",
"radius_endpoint_one": "Ponto final do raio",
"radius_endpoint_other": "Pontos finais de raio"
},
"operator": {
"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível",
"delete_operator": "Excluir operador",
@@ -948,6 +981,27 @@
"title": "RESTRIÇÕES",
"tty": "Acesso TTY"
},
"roaming": {
"account_created": "Nova conta criada!",
"account_deleted": "Conta excluída!",
"account_one": "Conta",
"account_other": "Contas",
"certificate_deleted": "Certificado excluído!",
"certificate_one": "Certificado",
"certificate_other": "Certificados",
"city": "Cidade",
"common_name": "Nome comum",
"country": "País",
"global_reach": "Alcance global",
"global_reach_account_id": "ID da conta",
"invalid_certificate": "Certificado inválido",
"invalid_key": "Chave privada inválida",
"location_details_title": "Localização",
"organization": "Organização",
"private_key": "Chave privada",
"province": "província",
"state": "Estado"
},
"rrm": {
"algorithm": "Algoritmo",
"algorithm_other": "Algoritmos",
@@ -1008,6 +1062,9 @@
"current_live_devices": "Dispositivos ativos atuais",
"currently_running_one": "Atualmente, há {{count}} simulação em execução",
"currently_running_other": "Existem atualmente {{count}} simulações em execução",
"delete_devices_confirm": "Tem certeza de que deseja remover todos os dispositivos e suas estatísticas do gateway? Esta ação não é reversível",
"delete_devices_loading": "Este processo pode levar até 5 minutos",
"delete_simulation_devices": "Apagar dispositivos",
"delete_success": "Simulação excluída!",
"duration": "Duração",
"error_devices": "Dispositivos de Erro",
@@ -1066,6 +1123,7 @@
"title": "Inscritos"
},
"system": {
"advanced": "Avançado",
"backend_logs": "Registros de back-end",
"configuration": "Configuração",
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",

View File

@@ -4,18 +4,22 @@ import '@tanstack/react-table';
declare module '@tanstack/table-core' {
interface ColumnMeta<TData extends RowData, TValue> {
ref?: React.MutableRefObject<HTMLTableCellElement | null>;
customMinWidth?: string;
anchored?: boolean;
stopPropagation?: boolean;
alwaysShow?: boolean;
anchored?: boolean;
hasPopover?: boolean;
customMaxWidth?: string;
customMinWidth?: string;
customWidth?: string;
isMonospace?: boolean;
isCentered?: boolean;
columnSelectorOptions?: {
label?: string;
};
rowContentOptions?: {
style?: React.CSSProperties;
};
headerOptions?: {
tooltip?: string;
};

View File

@@ -54,6 +54,7 @@ const DeviceActionDropdown = ({
}: Props) => {
const { t } = useTranslation();
const toast = useToast();
const deviceType = device?.deviceType ?? 'ap';
const connectColor = useColorModeValue('blackAlpha', 'gray');
const addEventListeners = useControllerStore((state) => state.addEventListeners);
const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({
@@ -205,7 +206,7 @@ const DeviceActionDropdown = ({
isDisabled={isDisabled}
onClick={handleOpenScan}
colorScheme="teal"
hidden={isCompact}
hidden={isCompact || deviceType !== 'ap'}
/>
</Tooltip>
<Menu>
@@ -221,7 +222,7 @@ const DeviceActionDropdown = ({
<Portal>
<MenuList maxH="315px" overflowY="auto">
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact}>
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact || deviceType !== 'ap'}>
{t('controller.configure.title')}
</MenuItem>
<MenuItem onClick={handleConnectClick} hidden={!isCompact}>
@@ -239,7 +240,7 @@ const DeviceActionDropdown = ({
<MenuItem onClick={handleUpdateToLatest} hidden>
{t('premium.toolbox.upgrade_to_latest')}
</MenuItem>
<MenuItem onClick={handleOpenScan} hidden={!isCompact}>
<MenuItem onClick={handleOpenScan} hidden={!isCompact || deviceType !== 'ap'}>
{t('commands.wifiscan')}
</MenuItem>
</MenuList>

View File

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

View File

@@ -26,7 +26,7 @@ export const ResponsiveTag = React.memo(({ label, icon, tooltip, isCompact, ...p
return (
<Tooltip label={tooltip ?? label}>
<Tag size="lg" colorScheme="blue" {...props}>
<TagLeftIcon boxSize="18px" as={icon} />
<TagLeftIcon boxSize="18px" as={icon} mt={-0.5} />
<TagLabel>{label}</TagLabel>
</Tag>
</Tooltip>

View File

@@ -24,7 +24,6 @@ export const DataGridCellRow = <TValue extends object>({
backgroundColor: hoveredRowBg,
}}
onClick={onClick}
borderRight="1px solid gray"
>
{row.getVisibleCells().map((cell) => (
<Td
@@ -55,6 +54,7 @@ export const DataGridCellRow = <TValue extends object>({
: undefined
}
border="0.5px solid gray"
style={cell.column.columnDef.meta?.rowContentOptions?.style}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Td>

View File

@@ -8,7 +8,7 @@ export type DataGridHeaderRowProps<TValue extends object> = {
};
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
<Tr p={0} borderRight="1px solid gray">
<Tr p={0}>
{headerGroup.headers.map((header) => (
<Th
color="gray.400"

View File

@@ -40,13 +40,16 @@ export type DataGridOptions<TValue extends object> = {
onRowClick?: (row: TValue) => (() => void) | undefined;
refetch?: () => void;
showAsCard?: boolean;
hideTablePreferences?: boolean;
hideTableTitleRow?: boolean;
};
export type DataGridProps<TValue extends object> = {
innerTableKey?: string | number;
controller: UseDataGridReturn;
columns: DataGridColumn<TValue>[];
header: {
title: string;
title: string | React.ReactNode;
objectListed: string;
leftContent?: React.ReactNode;
addButton?: React.ReactNode;
@@ -58,6 +61,7 @@ export type DataGridProps<TValue extends object> = {
};
export const DataGrid = <TValue extends object>({
innerTableKey,
controller,
columns,
header,
@@ -149,7 +153,21 @@ export const DataGrid = <TValue extends object>({
...tableOptions,
});
if (isLoading && data.length === 0) {
// If this is a manual DataTable, with a page index that is higher than 0 and higher than the max possible page, we send to index 0
React.useEffect(() => {
if (
options.isManual &&
!isLoading &&
data &&
pagination.pageIndex > 0 &&
options.count !== undefined &&
Math.ceil(options.count / pagination.pageSize) - 1 < pagination.pageIndex
) {
controller.onPaginationChange({ pageIndex: 0, pageSize: pagination.pageSize });
}
}, [options.count, isLoading, pagination, data]);
if (isLoading && !options.showAsCard && data.length === 0) {
return (
<Center>
<Spinner size="xl" />
@@ -160,25 +178,29 @@ export const DataGrid = <TValue extends object>({
return options.showAsCard ? (
<Card>
<CardHeader>
<Heading size="md" my="auto" mr={2}>
{header.title}
</Heading>
{typeof header.title === 'string' ? (
<Heading size="md" my="auto" mr={2}>
{header.title}
</Heading>
) : (
header.title
)}
{header.leftContent}
<Spacer />
<HStack spacing={2}>
{header.otherButtons}
{header.addButton}
{
{options.hideTablePreferences ? null : (
// @ts-ignore
<TableSettingsModal<TValue> controller={controller} columns={columns} />
}
)}
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
</HStack>
</CardHeader>
<CardBody display="flex" flexDirection="column">
<LoadingOverlay isLoading={isLoading}>
<TableContainer minH={minimumHeight}>
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
<Thead>
{table.getHeaderGroups().map((headerGroup) => (
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
@@ -206,7 +228,7 @@ export const DataGrid = <TValue extends object>({
</Card>
) : (
<Box w="100%">
<Flex mb={2}>
<Flex mb={2} hidden={options.hideTableTitleRow}>
<Heading size="md" my="auto" mr={2}>
{header.title}
</Heading>
@@ -215,16 +237,16 @@ export const DataGrid = <TValue extends object>({
<HStack spacing={2}>
{header.otherButtons}
{header.addButton}
{
{options.hideTablePreferences ? null : (
// @ts-ignore
<TableSettingsModal<TValue> controller={controller} columns={columns} />
}
)}
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
</HStack>
</Flex>
<LoadingOverlay isLoading={isLoading}>
<TableContainer minH={minimumHeight}>
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
<Thead>
{table.getHeaderGroups().map((headerGroup) => (
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />

View File

@@ -2,7 +2,8 @@ import * as React from 'react';
import { ColumnDef, PaginationState, SortingColumnDef, SortingState, VisibilityState } from '@tanstack/react-table';
import { useAuth } from 'contexts/AuthProvider';
const getDefaultSettings = (settings?: string) => {
const getDefaultSettings = ({ settings, showAllRows }: { settings?: string; showAllRows?: boolean }) => {
if (showAllRows) return { pageSize: 1000, pageIndex: 0 };
let limit = 10;
let index = 0;
@@ -54,9 +55,10 @@ export type UseDataGridProps = {
tableSettingsId: string;
defaultOrder: string[];
defaultSortBy?: SortingState;
showAllRows?: boolean;
};
export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: UseDataGridProps) => {
export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder, showAllRows }: UseDataGridProps) => {
const orderSetting = `${tableSettingsId}.order`;
const hiddenColumnSetting = `${tableSettingsId}.hiddenColumns`;
const pageSetting = `${tableSettingsId}.page`;
@@ -66,8 +68,9 @@ export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: Us
const [columnOrder, setColumnOrder] = React.useState<string[]>(
getSavedColumnOrder(defaultOrder ?? [], tableSettingsId),
);
const [pageInfo, setPageInfo] = React.useState<PaginationState>(getDefaultSettings(tableSettingsId));
const [pageInfo, setPageInfo] = React.useState<PaginationState>(
getDefaultSettings({ settings: tableSettingsId, showAllRows }),
);
const setNewColumnOrder = React.useCallback(
(newOrder: string[]) => {
setColumnOrder(newOrder);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react';
import { Select } from 'chakra-react-select';
import { CreatableSelect, Select } from 'chakra-react-select';
import PropTypes from 'prop-types';
import isEqual from 'react-fast-compare';
import { useTranslation } from 'react-i18next';
@@ -25,6 +25,7 @@ const propTypes = {
isHidden: PropTypes.bool,
isPortal: PropTypes.bool.isRequired,
definitionKey: PropTypes.string,
isCreatable: PropTypes.bool,
};
const defaultProps = {
@@ -36,6 +37,7 @@ const defaultProps = {
isDisabled: false,
isHidden: false,
definitionKey: null,
isCreatable: false,
};
const FastMultiSelectInput = ({
@@ -50,6 +52,7 @@ const FastMultiSelectInput = ({
isRequired,
isDisabled,
isHidden,
isCreatable,
isPortal,
definitionKey,
}) => {
@@ -61,35 +64,62 @@ const FastMultiSelectInput = ({
{label}
<ConfigurationFieldExplanation definitionKey={definitionKey} />
</FormLabel>
<Select
chakraStyles={{
control: (provided, { isDisabled: isControlDisabled }) => ({
...provided,
borderRadius: '15px',
opacity: isControlDisabled ? '0.8 !important' : '1',
border: '2px solid',
}),
dropdownIndicator: (provided) => ({
...provided,
backgroundColor: 'unset',
border: 'unset',
}),
}}
classNamePrefix={isPortal ? 'chakra-react-select' : ''}
menuPortalTarget={isPortal ? document.body : undefined}
isMulti
closeMenuOnSelect={false}
options={canSelectAll ? [{ value: '*', label: t('common.all') }, ...options] : options}
value={
value?.map((val) => {
if (val === '*') return { value: val, label: t('common.all') };
return options.find((opt) => opt.value === val);
}) ?? []
}
onChange={onChange}
onBlur={onBlur}
isDisabled={isDisabled}
/>
{isCreatable ? (
<CreatableSelect
chakraStyles={{
control: (provided, { isDisabled: isControlDisabled }) => ({
...provided,
borderRadius: '15px',
opacity: isControlDisabled ? '0.8 !important' : '1',
border: '2px solid',
}),
dropdownIndicator: (provided) => ({
...provided,
backgroundColor: 'unset',
border: 'unset',
}),
}}
classNamePrefix={isPortal ? 'chakra-react-select' : ''}
menuPortalTarget={isPortal ? document.body : undefined}
isMulti
closeMenuOnSelect={false}
options={options}
value={value}
onChange={onChange}
onBlur={onBlur}
isDisabled={isDisabled}
/>
) : (
<Select
chakraStyles={{
control: (provided, { isDisabled: isControlDisabled }) => ({
...provided,
borderRadius: '15px',
opacity: isControlDisabled ? '0.8 !important' : '1',
border: '2px solid',
}),
dropdownIndicator: (provided) => ({
...provided,
backgroundColor: 'unset',
border: 'unset',
}),
}}
classNamePrefix={isPortal ? 'chakra-react-select' : ''}
menuPortalTarget={isPortal ? document.body : undefined}
isMulti
closeMenuOnSelect={false}
options={canSelectAll ? [{ value: '*', label: t('common.all') }, ...options] : options}
value={
value?.map((val) => {
if (val === '*') return { value: val, label: t('common.all') };
return options.find((opt) => opt.value === val);
}) ?? []
}
onChange={onChange}
onBlur={onBlur}
isDisabled={isDisabled}
/>
)}
<FormErrorMessage>{error}</FormErrorMessage>
</FormControl>
);

View File

@@ -20,6 +20,7 @@ const propTypes = {
canSelectAll: PropTypes.bool,
isPortal: PropTypes.bool,
definitionKey: PropTypes.string,
isCreatable: PropTypes.bool,
};
const defaultProps = {
@@ -31,6 +32,7 @@ const defaultProps = {
canSelectAll: false,
isPortal: false,
definitionKey: null,
isCreatable: false,
};
const MultiSelectField = ({
@@ -43,25 +45,39 @@ const MultiSelectField = ({
emptyIsUndefined,
canSelectAll,
hasVirtualAll,
isCreatable,
isPortal,
definitionKey,
}) => {
const [{ value }, { touched, error }, { setValue, setTouched }] = useField(name);
const onChange = useCallback((option) => {
const allIndex = option.findIndex((opt) => opt.value === '*');
if (option.length === 0 && emptyIsUndefined) {
setValue(undefined);
} else if (allIndex === 0 && option.length > 1) {
const newValues = option.slice(1);
setValue(newValues.map((val) => val.value));
} else if (allIndex >= 0) {
if (!hasVirtualAll) setValue(['*']);
else setValue(options.map(({ value: v }) => v));
} else if (option.length > 0) setValue(option.map((val) => val.value));
else setValue([]);
setTouched(true);
}, []);
const onChange = useCallback(
(option) => {
if (isCreatable) {
if (typeof option === 'string') {
setValue([...value, option]);
} else {
setValue(option);
}
// setValue([...value, option]);
} else {
const allIndex = option.findIndex((opt) => opt.value === '*');
if (option.length === 0 && emptyIsUndefined) {
setValue(undefined);
} else if (allIndex === 0 && option.length > 1) {
const newValues = option.slice(1);
setValue(newValues.map((val) => val.value));
} else if (allIndex >= 0) {
if (!hasVirtualAll) setValue(['*']);
else setValue(options.map(({ value: v }) => v));
} else if (option.length > 0) setValue(option.map((val) => val.value));
else setValue([]);
setTouched(true);
}
},
[value],
);
const onFieldBlur = useCallback(() => {
setTouched(true);
@@ -82,6 +98,7 @@ const MultiSelectField = ({
isHidden={isHidden}
isPortal={isPortal}
definitionKey={definitionKey}
isCreatable={isCreatable}
/>
);
};

View File

@@ -24,6 +24,11 @@ const chakraStyles: (
placeholder: (provided) => ({
...provided,
lineHeight: '1',
pointerEvents: 'none',
userSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
msUserSelect: 'none',
}),
container: (provided) => ({
...provided,
@@ -31,6 +36,10 @@ const chakraStyles: (
backgroundColor: colorMode === 'light' ? 'white' : 'gray.600',
borderRadius: '15px',
}),
input: (provided) => ({
...provided,
gridArea: '1 / 2 / 4 / 4 !important',
}),
});
interface SearchOption extends OptionBase {
@@ -95,18 +104,43 @@ const GlobalSearchBar = () => {
.then(() => callback([]));
}
if (v.match('^[a-fA-F0-9-*]+$')) {
let result: { label: string; value: string; type: 'serial' }[] = [];
let tryAgain = true;
await store
.searchSerialNumber(v)
.then((res) => {
callback(
res.map((r) => ({
result = res.map((r) => ({
label: r,
value: r,
type: 'serial',
}));
tryAgain = false;
})
.catch(() => {
result = [];
});
if (tryAgain) {
// Wait 1 second and try again
await new Promise((resolve) => setTimeout(resolve, 1000));
await store
.searchSerialNumber(v)
.then((res) => {
result = res.map((r) => ({
label: r,
value: r,
type: 'serial',
})),
);
})
.catch(() => []);
}));
tryAgain = false;
})
.catch(() => {
result = [];
});
}
callback(result);
}
return callback([]);
},

View File

@@ -0,0 +1,257 @@
import React from 'react';
import {
Modal,
Text,
ModalOverlay,
ModalContent,
ModalBody,
Center,
Spinner,
Checkbox,
Stack,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
} from '@chakra-ui/react';
import { PlugsConnected } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { CloseButton } from 'components/Buttons/CloseButton';
import { ResponsiveButton } from 'components/Buttons/ResponsiveButton';
import { ModalHeader } from 'components/Containers/Modal/ModalHeader';
import { useCableDiagnostics } from 'hooks/Network/Devices';
import { ModalProps } from 'models/Modal';
import Button from 'theme/components/button';
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
import { DataGrid } from 'components/DataTables/DataGrid';
export type CableDiagnosticsModalProps = {
modalProps: ModalProps;
serialNumber: string;
port: string;
};
type DiagnosticsRow = {
port: string;
linkStatus: string;
pairA: string;
pairB: string;
pairC: string;
pairD: string;
type: string;
};
type OpticalRow = {
port: string;
vendorName: string;
formFactor: string;
partNumber: string;
serialNumber: string;
temperature: string;
txPower: string;
rxPower: string;
revision: string;
};
export const CableDiagnosticsModal = ({
modalProps: { isOpen, onClose },
serialNumber,
port,
}: CableDiagnosticsModalProps) => {
const { t } = useTranslation();
const [selectedPorts, setSelectedPorts] = React.useState<string[]>([]);
const [diagnosticsResult, setDiagnosticsResult] = React.useState<any>(null);
const { mutateAsync: diagnose, isLoading } = useCableDiagnostics({ serialNumber });
const handlePortToggle = (port: string) => {
setSelectedPorts((prev) => (prev.includes(port) ? prev.filter((p) => p !== port) : [...prev, port]));
};
const handleDiagnose = async () => {
if (port) {
try {
const result = await diagnose([port]);
setDiagnosticsResult(result);
} catch (error) {
console.error('Error diagnosing cable:', error);
}
}
};
const tableController = useDataGrid({
tableSettingsId: 'cable.diagnostics.table',
defaultOrder: ['port', 'linkStatus', 'pairA', 'pairB', 'pairC', 'pairD', 'type'],
showAllRows: true,
});
const columns: DataGridColumn<DiagnosticsRow | OpticalRow>[] = React.useMemo(() => {
const data = diagnosticsResult?.results?.status?.text?.[port];
const isOpticalData = data && 'form-factor' in data;
return isOpticalData
? [
{
id: 'vendorName',
header: 'Vendor Name',
accessorKey: 'vendorName',
},
{
id: 'formFactor',
header: 'Form Factor',
accessorKey: 'formFactor',
},
{
id: 'partNumber',
header: 'Part Number',
accessorKey: 'partNumber',
},
{
id: 'serialNumber',
header: 'Serial Number',
accessorKey: 'serialNumber',
},
{
id: 'temperature',
header: 'Temperature',
accessorKey: 'temperature',
},
{
id: 'txPower',
header: 'TX Power',
accessorKey: 'txPower',
},
{
id: 'rxPower',
header: 'RX Power',
accessorKey: 'rxPower',
},
{
id: 'revision',
header: 'Revision',
accessorKey: 'revision',
},
]
: [
{
id: 'port',
header: 'Port',
accessorKey: 'port',
},
{
id: 'linkStatus',
header: 'Link Status',
accessorKey: 'linkStatus',
},
{
id: 'pairA',
header: 'Pair A',
accessorKey: 'pairA',
},
{
id: 'pairB',
header: 'Pair B',
accessorKey: 'pairB',
},
{
id: 'pairC',
header: 'Pair C',
accessorKey: 'pairC',
},
{
id: 'pairD',
header: 'Pair D',
accessorKey: 'pairD',
},
{
id: 'type',
header: 'Type',
accessorKey: 'type',
},
];
}, [diagnosticsResult]);
const formatDiagnosticsData = (result: any): (DiagnosticsRow | OpticalRow)[] => {
if (!result?.results?.status?.text?.[port]) return [];
const data = result.results.status.text[port];
if (data['form-factor']) {
return [
{
port,
vendorName: data['vendor-name'] || 'N/A',
formFactor: data['form-factor'] || 'N/A',
partNumber: data['part-number'] || 'N/A',
serialNumber: data['serial-number'] || 'N/A',
temperature: data.temperature ? `${data.temperature.toFixed(2)}` : 'N/A',
txPower: data['tx-optical-power'] ? `${data['tx-optical-power']}` : 'N/A',
rxPower: data['rx-optical-power'] ? `${data['rx-optical-power']}` : 'N/A',
revision: data.revision || 'N/A',
},
];
}
return [
{
port,
linkStatus: data['link-status'],
pairA: `${data['pair-A'].meters} (${data['pair-A'].status})`,
pairB: `${data['pair-B'].meters} (${data['pair-B'].status})`,
pairC: `${data['pair-C'].meters} (${data['pair-C'].status})`,
pairD: `${data['pair-D'].meters} (${data['pair-D'].status})`,
type: data.type,
},
];
};
return (
<Modal onClose={onClose} isOpen={isOpen} size="xl">
<ModalOverlay />
<ModalContent maxW="50vw">
<ModalHeader title={t('commands.cable_diagnostics')} right={<CloseButton onClick={onClose} />} />
<ModalBody pb={6}>
{isLoading ? (
<Center my={4} flexDirection="column" gap={4}>
<Spinner size="lg" />
<Text>Please wait...</Text>
<Text fontSize="sm" color="gray.500">
Please do not close this window. This may take a few seconds.
</Text>
</Center>
) : (
<Center flexDirection="column" gap={4}>
<ResponsiveButton
color="blue"
icon={<PlugsConnected size={20} />}
label={`${
diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 ? 'Retake' : 'Start'
} Test for Port ${port}`}
onClick={handleDiagnose}
isLoading={isLoading}
isDisabled={!port}
isCompact={false}
/>
{diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 && (
<DataGrid<DiagnosticsRow | OpticalRow>
controller={tableController}
header={{
title: '',
objectListed: 'Cable Diagnostics',
}}
columns={columns}
isLoading={isLoading}
data={formatDiagnosticsData(diagnosticsResult)}
options={{
isHidingControls: true,
}}
/>
)}
</Center>
)}
</ModalBody>
</ModalContent>
</Modal>
);
};

View File

@@ -30,7 +30,7 @@ export type ConfigureModalProps = {
};
};
export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps) => {
const _ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps) => {
const { t } = useTranslation();
const toast = useToast();
const configure = useConfigureDevice({ serialNumber });
@@ -45,6 +45,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
const onImportConfiguration = () => {
setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
};
const isValid = React.useMemo(() => {
try {
JSON.parse(newConfig);
@@ -58,22 +59,59 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
try {
const config = JSON.parse(newConfig);
configure.mutate(config, {
onSuccess: () => {
toast({
id: `configure-success-${serialNumber}`,
title: t('common.success'),
description: t('controller.configure.success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onSuccess: (data) => {
if (data.errorCode === 0) {
toast({
id: `configure-success-${serialNumber}`,
title: t('common.success'),
description:
data.status === 'pending'
? 'Command is pending! It will execute once the device connects'
: t('controller.configure.success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
modalProps.onClose();
} else if (data.errorCode === 1) {
toast({
id: `configure-warning-${serialNumber}`,
title: 'Warning',
description: `${data?.errorText ?? 'Unknown Warning'}`,
status: 'warning',
duration: 5000,
isClosable: true,
position: 'top-right',
});
modalProps.onClose();
} else {
toast({
id: `config-error-${serialNumber}`,
title: t('common.error'),
description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
modalProps.onClose();
},
});
} catch (e) {}
} catch (e) {
// do nothing
}
};
React.useEffect(() => {
if (modalProps.isOpen) {
getDevice.refetch();
} else {
setNewConfig('');
}
}, [modalProps.isOpen]);
return (
<Modal
{...modalProps}
@@ -124,3 +162,5 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
</Modal>
);
};
export const ConfigureModal = React.memo(_ConfigureModal);

View File

@@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next';
import { Modal } from '../Modal';
import { lowercaseFirstLetter } from 'helpers/stringHelper';
import { useTelemetry } from 'hooks/Network/Telemetry';
import { secondsDuration } from 'helpers/dateFormatting';
export type TelemetryModalProps = {
serialNumber: string;
@@ -146,8 +147,7 @@ const _TelemetryModal = ({ serialNumber, modalProps }: TelemetryModalProps) => {
{t('controller.telemetry.interval')}: {form.interval} {lowercaseFirstLetter(t('common.seconds'))}
</p>
<p>
{t('controller.telemetry.duration')}: {form.interval}{' '}
{lowercaseFirstLetter(t('controller.telemetry.minutes'))}
{t('controller.telemetry.duration')}: {secondsDuration(form.lifetime, t)}
</p>
<p>
{t('controller.telemetry.types')}: {form.types.join(', ')}

View File

@@ -1,18 +1,5 @@
import React from 'react';
import {
Box,
Button,
Heading,
IconButton,
Spacer,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
useColorMode,
} from '@chakra-ui/react';
import { Box, Button, Center, Heading, IconButton, Spacer, useColorMode } from '@chakra-ui/react';
import { JsonViewer } from '@textea/json-viewer';
import { ArrowLeft } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
@@ -20,21 +7,124 @@ import { v4 as uuid } from 'uuid';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { ScanChannel } from 'models/Device';
import { DeviceScanResult, ScanChannel } from 'models/Device';
import { DataGrid } from 'components/DataTables/DataGrid';
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
interface Props {
channelInfo: ScanChannel;
}
const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => {
const ueCell = (ies: DeviceScanResult['ies'], setIes: (ies: DeviceScanResult['ies']) => void) => (
<Button size="sm" colorScheme="blue" onClick={() => setIes(ies)} w="100%">
{ies.length}
</Button>
);
const centerIfUndefinedCell = (v?: string | number, suffix?: string) =>
v !== undefined ? `${v}${suffix ? `${suffix}` : ''}` : <Center>-</Center>;
const ResultCard = ({ channelInfo: { channel, devices } }: Props) => {
const { t } = useTranslation();
const { colorMode } = useColorMode();
const [ies, setIes] = React.useState<{ content: unknown; name: string; type: number }[] | undefined>();
const tableController = useDataGrid({
tableSettingsId: 'wifiscan.devices.table',
defaultOrder: ['ssid', 'signal', 'actions'],
defaultSortBy: [
{
desc: false,
id: 'ssid',
},
],
});
const columns: DataGridColumn<DeviceScanResult>[] = React.useMemo(
(): DataGridColumn<DeviceScanResult>[] => [
{
id: 'ssid',
header: 'SSID',
footer: '',
accessorKey: 'ssid',
meta: {
anchored: true,
alwaysShow: true,
},
},
{
id: 'signal',
header: 'Signal',
footer: '',
accessorKey: 'signal',
cell: (v) => `${v.cell.row.original.signal} db`,
meta: {
anchored: true,
customWidth: '80px',
alwaysShow: true,
rowContentOptions: {
style: {
textAlign: 'right',
},
},
},
},
{
id: 'station',
header: 'UEs',
accessorKey: 'sta_count',
cell: (v) => centerIfUndefinedCell(v.cell.row.original.sta_count),
meta: {
anchored: true,
customWidth: '40px',
alwaysShow: true,
rowContentOptions: {
style: {
textAlign: 'right',
},
},
},
},
{
id: 'utilization',
header: 'Ch. Util.',
accessorKey: 'ch_util',
cell: (v) => centerIfUndefinedCell(v.cell.row.original.ch_util, '%'),
meta: {
anchored: true,
customWidth: '60px',
alwaysShow: true,
headerOptions: {
tooltip: 'Channel Utilization (%)',
},
rowContentOptions: {
style: {
textAlign: 'right',
},
},
},
},
{
id: 'ies',
header: 'Ies',
footer: '',
accessorKey: 'actions',
cell: (v) => ueCell(v.cell.row.original.ies ?? [], setIes),
meta: {
customWidth: '50px',
isCentered: true,
alwaysShow: true,
},
},
],
[t],
);
return (
<Card variant="widget">
<Card>
<CardHeader display="flex">
<Heading size="md" my="auto">
{t('commands.channel')} #{channel} ({devices.length} {t('devices.title')})
{t('commands.channel')} #{channel} ({devices.length}{' '}
{devices.length === 1 ? t('devices.one') : t('devices.title')})
</Heading>
<Spacer />
{ies && (
@@ -49,52 +139,43 @@ const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => {
)}
</CardHeader>
<CardBody>
<Box h="400px" w="100%" overflowY="auto" overflowX="auto" px={0}>
{ies ? (
<Box w="800px">
{ies.map(({ content, name, type }) => (
<Box key={uuid()} my={2}>
<Heading size="sm" mb={2} textDecor="underline">
{name} ({type})
</Heading>
<JsonViewer
rootName={false}
displayDataTypes={false}
enableClipboard
theme={colorMode === 'light' ? undefined : 'dark'}
value={content as object}
style={{ background: 'unset', display: 'unset' }}
/>
</Box>
))}
</Box>
) : (
<Table variant="simple" px={0}>
<Thead>
<Tr>
<Th>SSID</Th>
<Th width="110px" isNumeric>
{t('commands.signal')}
</Th>
<Th w="10px">IEs</Th>
</Tr>
</Thead>
<Tbody>
{devices.map((dev) => (
<Tr key={uuid()}>
<Td>{dev.ssid}</Td>
<Td width="110px">{dev.signal} db</Td>
<Td w="10px">
<Button size="sm" colorScheme="blue" onClick={() => setIes(dev.ies ?? [])}>
{dev.ies?.length ?? 0}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
)}
</Box>
{ies ? (
<Box w="800px">
{ies.map(({ content, name, type }) => (
<Box key={uuid()} my={2}>
<Heading size="sm" mb={2} textDecor="underline">
{name} ({type})
</Heading>
<JsonViewer
rootName={false}
displayDataTypes={false}
enableClipboard
theme={colorMode === 'light' ? undefined : 'dark'}
value={content as object}
style={{ background: 'unset', display: 'unset' }}
/>
</Box>
))}
</Box>
) : (
<DataGrid<DeviceScanResult>
controller={tableController}
header={{
title: '',
objectListed: t('devices.title'),
}}
columns={columns}
data={devices}
options={{
count: devices.length,
onRowClick: (device) => () => setIes(device.ies ?? []),
hideTablePreferences: true,
isHidingControls: true,
minimumHeight: '0px',
hideTableTitleRow: true,
}}
/>
)}
</CardBody>
</Card>
);

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { Alert, Heading, SimpleGrid } from '@chakra-ui/react';
import { Alert, Heading, VStack } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import ResultCard from './ResultCard';
@@ -11,7 +11,7 @@ interface Props {
setCsvData: (data: DeviceScanResult[]) => void;
}
const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
const WifiScanResultDisplay = ({ results, setCsvData }: Props) => {
const { t } = useTranslation();
const scanResults = useMemo(() => {
@@ -54,18 +54,18 @@ const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
return (
<>
{results.errorCode === 1 && (
<Heading size="sm">
<Heading size="md">
<Alert colorScheme="red">{t('commands.wifiscan_error_1')}</Alert>
</Heading>
)}
<Heading size="sm">
<Heading size="md" mb={2}>
{t('commands.execution_time')}: {Math.floor(results.executionTime / 1000)}s
</Heading>
<SimpleGrid minChildWidth="360px" spacing={2}>
<VStack spacing={4} align="stretch">
{scanResults?.scanList.map((channel) => (
<ResultCard key={uuid()} channelInfo={channel} />
))}
</SimpleGrid>
</VStack>
</>
);
};

1
src/custom.d.ts vendored
View File

@@ -8,3 +8,4 @@ declare module '*.png' {
const value: string;
export = value;
}
/// <reference types="vite-plugin-svgr/client" />

View File

@@ -174,12 +174,37 @@ export const useGetEventQueue = () => {
};
const configureDevice = (serialNumber: string) => async (configuration: Record<string, unknown>) =>
axiosGw.post<unknown>(`device/${serialNumber}/configure`, {
when: 0,
UUID: 1,
serialNumber,
configuration,
});
axiosGw
.post<unknown>(`device/${serialNumber}/configure`, {
when: 0,
UUID: 1,
serialNumber,
configuration,
})
.then(
(res) =>
res.data as Partial<{
UUID: string;
attachFile: number;
command: string;
completed: number;
custom: number;
deferred: boolean;
details: Record<string, unknown>;
errorCode: number;
errorText: string;
executed: number;
executionTime: number;
lastTry: number;
results: Record<string, unknown>;
serialNumber: string;
status: string;
submitted: number;
submittedBy: string;
waitingForFile: number;
when: number;
}>,
);
export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) => {
const queryClient = useQueryClient();
@@ -187,6 +212,8 @@ export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) =
return useMutation(configureDevice(serialNumber), {
onSuccess: () => {
queryClient.invalidateQueries(['commands', serialNumber]);
queryClient.invalidateQueries(['device', serialNumber]);
queryClient.invalidateQueries(['devices']);
},
});
};
@@ -248,27 +275,14 @@ const startScript = (data: { serialNumber: string; timeout?: number; [k: string]
})
.then((response: { data: DeviceCommandHistory }) => response.data);
export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
const { t } = useTranslation();
const toast = useToast();
const queryClient = useQueryClient();
return useMutation(startScript, {
onSuccess: () => {
queryClient.invalidateQueries(['commands', serialNumber]);
},
onError: (e) => {
onError: () => {
queryClient.invalidateQueries(['commands', serialNumber]);
if (axios.isAxiosError(e)) {
toast({
id: 'script-error',
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
},
});
};

View File

@@ -3,10 +3,12 @@ import { axiosGw } from 'constants/axiosInstances';
import { useEndpointStatus } from 'hooks/useEndpointStatus';
import { AxiosError } from 'models/Axios';
import { DeviceConfiguration } from 'models/Device';
import { DevicePlatform } from './Devices';
export type DefaultConfigurationResponse = {
configuration: DeviceConfiguration;
created: number;
platform: DevicePlatform;
description: string;
lastModified: number;
modelIds: string[];

View File

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

View File

@@ -9,15 +9,21 @@ import { AxiosError } from 'models/Axios';
import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device';
import { Note } from 'models/Note';
import { PageInfo } from 'models/Table';
import { DeviceCommandHistory } from './Commands';
const getDeviceCount = () =>
axiosGw.get('devices?countOnly=true').then((response) => response.data) as Promise<{ count: number }>;
export const DEVICE_PLATFORMS = ['all', 'ap', 'switch'] as const;
export type DevicePlatform = (typeof DEVICE_PLATFORMS)[number];
export const useGetDeviceCount = ({ enabled }: { enabled: boolean }) => {
const getDeviceCount = (platform: DevicePlatform) =>
axiosGw.get(`devices?countOnly=true&platform=${platform}`).then((response) => response.data) as Promise<{
count: number;
}>;
export const useGetDeviceCount = ({ enabled, platform = 'all' }: { enabled: boolean; platform?: DevicePlatform }) => {
const { t } = useTranslation();
const toast = useToast();
return useQuery(['devices', 'count'], getDeviceCount, {
return useQuery(['devices', 'count', { platform }], () => getDeviceCount(platform), {
enabled,
onError: (e: AxiosError) => {
if (!toast.isActive('inventory-fetching-error'))
@@ -42,13 +48,14 @@ export type DeviceWithStatus = {
associations_2G: number;
associations_5G: number;
associations_6G: number;
blackListed?: boolean;
compatible: string;
connected: boolean;
connectReason?: string;
certificateExpiryDate?: number;
createdTimestamp: number;
devicePassword: string;
deviceType: 'AP' | 'SWITCH' | 'IOT' | 'MESH';
deviceType: 'ap' | 'switch';
entity: string;
firmware: string;
fwUpdatePolicy: string;
@@ -95,25 +102,27 @@ export const getSingleDeviceWithStatus = (serialNumber: string) =>
})
.catch(() => undefined);
const getDevices = (limit: number, offset: number) =>
const getDevices = (limit: number, offset: number, platform: DevicePlatform) =>
axiosGw
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`)
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}&platform=${platform}`)
.then((response) => response.data) as Promise<{ devicesWithStatus: DeviceWithStatus[] }>;
export const useGetDevices = ({
pageInfo,
enabled,
onError,
platform = 'all',
}: {
pageInfo?: PageInfo;
enabled: boolean;
onError?: (e: AxiosError) => void;
platform?: DevicePlatform;
}) => {
const offset = pageInfo?.limit !== undefined ? pageInfo.limit * pageInfo.index : 0;
return useQuery(
['devices', 'all', { limit: pageInfo?.limit, offset }],
() => getDevices(pageInfo?.limit || 0, offset),
['devices', 'all', { limit: pageInfo?.limit, offset, platform }],
() => getDevices(pageInfo?.limit || 0, offset, platform),
{
keepPreviousData: true,
enabled: enabled && pageInfo !== undefined,
@@ -123,22 +132,28 @@ export const useGetDevices = ({
);
};
const getAllDevices = async () => {
const getAllDevices = async (platform: DevicePlatform) => {
let offset = 0;
let devices: DeviceWithStatus[] = [];
let devicesResponse: { devicesWithStatus: DeviceWithStatus[] };
do {
// eslint-disable-next-line no-await-in-loop
devicesResponse = await getDevices(500, offset);
devicesResponse = await getDevices(500, offset, platform);
devices = devices.concat(devicesResponse.devicesWithStatus);
offset += 500;
} while (devicesResponse.devicesWithStatus.length === 500);
return devices;
};
export const useGetAllDevicesWithStatus = ({ onError }: { onError?: (e: AxiosError) => void }) => {
export const useGetAllDevicesWithStatus = ({
onError,
platform = 'all',
}: {
onError?: (e: AxiosError) => void;
platform?: DevicePlatform;
}) => {
const { isReady } = useEndpointStatus('owgw');
return useQuery(['devices', 'all', 'full'], getAllDevices, {
return useQuery(['devices', 'all', 'full', { platform }], () => getAllDevices(platform), {
enabled: isReady && false,
onError,
});
@@ -362,6 +377,40 @@ export const useWifiScanDevice = ({ serialNumber }: { serialNumber: string }) =>
);
};
export const useCableDiagnostics = ({ serialNumber }: { serialNumber: string }) => {
const toast = useToast();
const { t } = useTranslation();
return useMutation(
(ports: string[]): Promise<unknown> =>
axiosGw
.post(`device/${serialNumber}/cable-diagnostics`, {
serial: serialNumber,
ports,
when: 0,
})
.then(({ data }) => data),
{
onSuccess: (data) => {
console.log('Success data: ', data);
},
onError: (e: AxiosError) => {
toast({
id: uuid(),
title: t('common.error'),
description: t('commands.cablediagnostics_error', {
e: e?.response?.data?.ErrorDescription,
}),
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
},
);
};
export const useGetDeviceRtty = ({ serialNumber, extraId }: { serialNumber: string; extraId: string | number }) => {
const { t } = useTranslation();
const toast = useToast();
@@ -431,3 +480,45 @@ export const useUpdateDevice = ({ serialNumber }: { serialNumber: string }) => {
},
});
};
const deleteDeviceBatch = async (pattern: string) => {
if (pattern.length < 6) throw new Error('Pattern must be at least 6 characters long');
axiosGw.delete(`devices?macPattern=${pattern}`);
};
export const useDeleteDeviceBatch = () => {
const queryClient = useQueryClient();
return useMutation(deleteDeviceBatch, {
onSuccess: () => {
queryClient.invalidateQueries(['devices']);
},
});
};
export type PowerCyclePort = {
/** Ex.: Ethernet0 */
name: string;
/** Cycle length in MS. Default is 10 000 */
cycle?: number;
};
export type PowerCycleRequest = {
serial: string;
when: number;
ports: PowerCyclePort[];
};
export const usePowerCycle = () => {
const queryClient = useQueryClient();
return useMutation(
(request: PowerCycleRequest) =>
axiosGw.post(`device/${request.serial}/powercycle`, request).then((res) => res.data as DeviceCommandHistory),
{
onSettled: () => {
queryClient.invalidateQueries(['commands']);
},
},
);
};

View File

@@ -12,7 +12,7 @@ const getAvailableFirmwareBatch = async (deviceType: string, limit: number, offs
.get(`firmwares?deviceType=${deviceType}&limit=${limit}&offset=${offset}`)
.then(({ data }: { data: { firmwares: Firmware[] } }) => data);
const getAllAvailableFirmware = async (deviceType: string) => {
export const getAllAvailableFirmware = async (deviceType: string) => {
const limit = 500;
let offset = 0;
let data: { firmwares: Firmware[] } = { firmwares: [] };
@@ -70,25 +70,66 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe
return useMutation(
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
serialNumber,
when: 0,
keepRedirector,
uri,
signature,
}),
axiosGw
.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
serialNumber,
when: 0,
keepRedirector,
uri,
signature,
})
.then(
(response) =>
response as {
data: {
errorCode: number;
errorText: string;
status: string;
results?: {
status?: {
error?: number;
resultCode?: number;
text?: string;
};
};
};
},
),
{
onSuccess: () => {
toast({
id: `device-upgrade-success-${uuid()}`,
title: t('common.success'),
description: t('commands.firmware_upgrade_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
onSuccess: ({ data }) => {
if (data.errorCode === 0) {
toast({
id: `device-upgrade-success-${uuid()}`,
title: t('common.success'),
description: t('commands.firmware_upgrade_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
} else if (data.errorCode === 1) {
toast({
id: `device-upgrade-warning-${uuid()}`,
title: 'Warning',
description: `${data?.errorText ?? 'Unknown Warning'}`,
status: 'warning',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
} else {
toast({
id: `device-upgrade-error-${uuid()}`,
title: t('common.error'),
description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
},
onError: (e: AxiosError) => {
toast({

View File

@@ -0,0 +1,173 @@
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { axiosGw, axiosOwls } from 'constants/axiosInstances';
import { AtLeast } from 'models/General';
export type Simulation = {
clientInterval: number;
concurrentDeviceS: number;
deviceType: string;
devices: number;
gateway: string;
healthCheckInterval: number;
id: string;
keepAlive: number;
key: string;
macPrefix: string;
minAssociations: number;
maxAssociations: number;
minClients: number;
maxClients: number;
name: string;
reconnectionInterval: number;
simulationLength: number;
stateInterval: number;
threads: number;
};
const getSimulations = () => async () =>
axiosOwls.get(`simulation/*`).then((response) => response.data as { list: Simulation[] });
export const useGetSimulations = () =>
useQuery(['simulations'], getSimulations(), {
keepPreviousData: true,
staleTime: 30000,
});
const getSimulation = (id?: string) => async () =>
axiosOwls.get(`simulation/${id}`).then((response) => response.data as { list: Simulation[] });
export const useGetSimulation = ({ id }: { id?: string }) =>
useQuery(['simulation', id], getSimulation(id), {
keepPreviousData: true,
enabled: id !== undefined,
staleTime: 30000,
});
const createSimulation = async (newSimulation: Partial<Simulation>) => axiosOwls.post(`simulation/0`, newSimulation);
export const useCreateSimulation = () => {
const queryClient = useQueryClient();
return useMutation(createSimulation, {
onSuccess: () => {
queryClient.invalidateQueries(['simulations']);
},
});
};
const updateSimulation = async (newSimulation: AtLeast<Simulation, 'id'>) =>
axiosOwls.put(`simulation/${newSimulation.id}`, newSimulation).then((response) => response.data as Simulation);
export const useUpdateSimulation = () => {
const queryClient = useQueryClient();
return useMutation(updateSimulation, {
onSuccess: (newSimulation) => {
queryClient.setQueryData(['simulation'], newSimulation);
queryClient.invalidateQueries(['simulations']);
},
});
};
const deleteSimulation = async ({ id }: { id: string }) => axiosOwls.delete(`simulation/${id}`);
export const useDeleteSimulation = () => {
const queryClient = useQueryClient();
return useMutation(deleteSimulation, {
onSuccess: () => {
queryClient.invalidateQueries(['simulations']);
},
});
};
const startSimulation = async ({ id }: { id: string }) => axiosOwls.post(`operation/${id}?operation=start`);
export const useStartSimulation = () => {
const queryClient = useQueryClient();
return useMutation(startSimulation, {
onSuccess: () => {
queryClient.invalidateQueries(['simulations', 'status']);
},
});
};
const stopSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) =>
axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=stop`);
export const useStopSimulation = () => {
const queryClient = useQueryClient();
return useMutation(stopSimulation, {
onSuccess: () => {
queryClient.invalidateQueries(['simulations', 'status']);
},
});
};
const cancelSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) =>
axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=cancel`);
export const useCancelSimulation = () => {
const queryClient = useQueryClient();
return useMutation(cancelSimulation, {
onSuccess: () => {
queryClient.invalidateQueries(['simulations', 'status']);
},
});
};
export type SimulationStatus = {
endTime: number;
errorDevices: number;
id: string;
liveDevices: number;
msgsRx: number;
msgsTx: number;
owner: string;
rx: number;
simulationId: string;
startTime: number;
state: 'running' | 'completed' | 'cancelled' | 'none';
timeToFullDevices: number;
tx: number;
};
const getSimulationsStatus = async () =>
axiosOwls.get(`status/*`).then((response) => response.data as SimulationStatus[]);
export const useGetSimulationsStatus = () =>
useQuery(['simulations', 'status'], getSimulationsStatus, {
keepPreviousData: true,
staleTime: Infinity,
});
const getSimulationStatus = async (context: QueryFunctionContext<[string, string, string]>) =>
axiosOwls.get(`status/${context.queryKey[2]}`).then((response) => response.data as SimulationStatus);
export const useGetSimulationStatus = ({ id }: { id: string }) =>
useQuery(['simulations', 'status', id], getSimulationStatus, {
keepPreviousData: true,
staleTime: Infinity,
});
const getSimulationHistory = async (context: QueryFunctionContext<[string, string, string]>) =>
axiosOwls.get(`results/${context.queryKey[2]}`).then((response) => response.data.list as SimulationStatus[]);
export const useGetSimulationHistory = ({ id }: { id: string }) =>
useQuery(['simulations', 'history', id], getSimulationHistory, {
keepPreviousData: true,
enabled: !!id,
});
const deleteSimulationResult = async ({ id }: { id: string }) => axiosOwls.delete(`results/${id}`);
export const useDeleteSimulationResult = () => {
const queryClient = useQueryClient();
return useMutation(deleteSimulationResult, {
onSuccess: () => {
queryClient.invalidateQueries(['simulations', 'history']);
},
});
};
const deleteSimulatedDevices = async () => axiosGw.delete('devices?simulatedDevices=true');
export const useDeleteSimulatedDevices = () => {
const queryClient = useQueryClient();
return useMutation(deleteSimulatedDevices, {
onSuccess: () => {
queryClient.invalidateQueries(['devices']);
},
});
};

View File

@@ -2,7 +2,24 @@ import { useQuery } from '@tanstack/react-query';
import { axiosGw } from 'constants/axiosInstances';
import { AxiosError } from 'models/Axios';
type DeviceInterfaceStatistics = {
export type DeviceLinkState = {
carrier?: number;
counters?: {
collisions: number;
multicast: number;
rx_bytes: number;
rx_dropped: number;
rx_errors: number;
rx_packets: number;
tx_bytes: number;
tx_dropped: number;
tx_errors: number;
tx_packets: number;
};
duplex?: string;
speed?: number;
};
export type DeviceInterfaceStatistics = {
clients: {
ipv4_addresses?: string[];
ipv6_addresses?: string[];
@@ -42,6 +59,7 @@ type DeviceInterfaceStatistics = {
dynamic_vlan?: number;
inactive: number;
ipaddr_v4: string;
fingerprint?: object;
rssi: number;
rx_bytes: number;
rx_duration: number;
@@ -112,11 +130,21 @@ export type DeviceStatistics = {
channel: number;
band?: string[];
channel_width: string;
noise: number;
noise?: number;
phy: string;
receive_ms: number;
transmit_ms: number;
temperature?: number;
tx_power: number;
frequency?: number[];
survey?: {
busy: number;
frequency: number;
noise: number;
time: number;
time_rx: number;
time_tx: number;
}[];
}[];
dynamic_vlans?: {
vid: number;
@@ -138,18 +166,10 @@ export type DeviceStatistics = {
};
'link-state'?: {
downstream: {
eth1?: {
carrier?: number;
duplex?: string;
speed?: number;
};
[key: string]: DeviceLinkState;
};
upstream: {
eth0?: {
carrier?: number;
duplex?: string;
speed?: number;
};
[key: string]: DeviceLinkState;
};
};
'lldp-peers'?: {
@@ -190,6 +210,7 @@ export const useGetDeviceLastStats = ({
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
enabled: serialNumber !== undefined && serialNumber !== '',
staleTime: 1000 * 60,
refetchInterval: 1000 * 60,
onError,
});

View File

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

View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import { useToast } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { isApiError } from 'models/Axios';
export type SuccessNotificationProps = {
description: string;
id?: string;
};
export type ApiErrorNotificationProps = {
e: unknown;
fallbackMessage?: string;
id?: string;
};
export const useNotification = () => {
const { t } = useTranslation();
const toast = useToast();
const successToast = ({ description, id }: SuccessNotificationProps) => {
toast({
id: id ?? uuid(),
title: t('common.success'),
description,
status: 'success',
duration: 3000,
isClosable: true,
position: 'top-right',
});
};
const apiErrorToast = ({ e, id, fallbackMessage }: ApiErrorNotificationProps) => {
if (isApiError(e)) {
toast({
id: id ?? uuid(),
title: t('common.error'),
description: e.response?.data.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
} else {
toast({
id: id ?? uuid(),
title: t('common.error'),
description: fallbackMessage,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
};
const errorToast = ({ description, id }: SuccessNotificationProps) => {
toast({
id: id ?? uuid(),
title: t('common.error'),
description,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
};
return React.useMemo(
() => ({
successToast,
errorToast,
apiErrorToast,
}),
[t],
);
};
export type UseNotificationReturn = ReturnType<typeof useNotification>;

View File

@@ -1,16 +1,6 @@
import * as React from 'react';
import {
Box,
CircularProgress,
CircularProgressLabel,
Flex,
Heading,
Icon,
Text,
Tooltip,
VStack,
} from '@chakra-ui/react';
import { ArrowSquareDown, ArrowSquareUp, Clock } from '@phosphor-icons/react';
import { Flex, Heading, Icon, Text, Tooltip, VStack } from '@chakra-ui/react';
import { ArrowSquareDown, ArrowSquareUp } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { Card } from 'components/Containers/Card';
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
@@ -20,74 +10,26 @@ import { useGetDevicesStats } from 'hooks/Network/Devices';
const SidebarDevices = () => {
const { t } = useTranslation();
const getStats = useGetDevicesStats({});
const [lastTime, setLastTime] = React.useState<Date | undefined>();
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
const time = React.useMemo(() => {
if (lastTime === undefined || lastUpdate === undefined) return null;
const seconds = lastTime.getTime() - lastUpdate.getTime();
return Math.max(0, Math.floor(seconds / 1000));
}, [lastTime, lastUpdate]);
const circleColor = () => {
if (time === null) return 'gray.300';
if (time < 10) return 'green.300';
if (time < 30) return 'yellow.300';
return 'red.300';
};
React.useEffect(() => {
setLastUpdate(new Date());
}, [getStats.data]);
React.useEffect(() => {
const interval = setInterval(() => {
setLastTime(new Date());
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
if (!getStats.data) return null;
return (
<Card p={4}>
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
<CircularProgress
isIndeterminate
color={circleColor()}
position="absolute"
right="6px"
top="6px"
w="unset"
size={6}
thickness="14px"
>
<CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel>
</CircularProgress>
</Tooltip>
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
<Box position="absolute" right="8px" top="8px" w="unset" hidden>
<Clock size={16} />
</Box>
</Tooltip>
<VStack mb={-1}>
<Flex flexDir="column" textAlign="center">
<Heading size="md">{getStats.data.connectedDevices}</Heading>
<Heading size="xs" display="flex" justifyContent="center">
<Text>
{t('common.connected')} {t('devices.title')}{' '}
</Text>{' '}
</Heading>
<Heading size="md">{getStats.data.connectedDevices}</Heading>
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
<Heading size="md" textAlign="center" mt={1}>
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
</Heading>
</Tooltip>
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
<Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}>
<Tooltip hasArrow label="Rx">
<Flex alignItems="center" mr={1}>

View File

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

View File

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

View File

@@ -1,3 +1,6 @@
import { AxiosError as Err } from 'axios';
import { AxiosError as Err, isAxiosError } from 'axios';
export type AxiosError = Err<{ ErrorDescription: string; ErrorCode: number }>;
export const isApiError = (e: unknown): e is AxiosError =>
isAxiosError(e) && (e as AxiosError).response?.data?.ErrorDescription !== undefined;

View File

@@ -3,6 +3,8 @@ import { Note } from './Note';
export interface GatewayDevice {
UUID: number;
blackListed?: boolean;
certificateExpiryDate: number;
compatible: string;
configuration: unknown;
createdTimestamp: number;
@@ -16,6 +18,7 @@ export interface GatewayDevice {
lastConfigurationChange: number;
lastConfigurationDownload: number;
lastFWUpdate: number;
lastRecordedContact: number;
locale: string;
location: string;
macAddress: string;
@@ -112,12 +115,16 @@ interface BssidResult {
bssid: string;
capability: number;
channel: number;
/** Channel Utilization percentage (ex.: 28 -> 28% channel utilization) */
ch_util?: number;
frequency: number;
ht_oper: string;
ies: { content: unknown; name: string; type: number }[];
last_seen: number;
ssid: string;
signal: number;
/** Station count */
sta_count?: number;
tsf: number;
meshid?: string;
vht_oper: string;
@@ -142,20 +149,8 @@ export interface WifiScanResult {
};
}
export interface DeviceScanResult {
bssid: string;
capability: number;
channel: number;
frequency: number;
ht_oper: string;
ies: { content: unknown; name: string; type: number }[];
last_seen: number;
ssid: string;
signal: number | string;
tsf: number;
meshid?: string;
vht_oper: string;
}
export type DeviceScanResult = BssidResult;
export interface ScanChannel {
channel: number;
devices: DeviceScanResult[];

View File

@@ -0,0 +1,93 @@
import * as React from 'react';
import {
Box,
Button,
Center,
Heading,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Text,
} from '@chakra-ui/react';
import { Trash } from '@phosphor-icons/react';
import { Card } from 'components/Containers/Card';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { CardBody } from 'components/Containers/Card/CardBody';
import { DeleteButton } from 'components/Buttons/DeleteButton';
import { useNotification } from 'hooks/useNotification';
import { useDeleteSimulatedDevices } from 'hooks/Network/Simulations';
const AdvancedSystemPage = () => {
const { successToast, apiErrorToast } = useNotification();
const deleteSimulatedDevices = useDeleteSimulatedDevices();
const handleDeleteSimulatedDevices = async () =>
deleteSimulatedDevices.mutateAsync(undefined, {
onSuccess: () => {
successToast({
id: 'delete-simulated-devices',
description: 'Simulated devices deleted!',
});
},
onError: (e) => {
apiErrorToast({
id: 'delete-simulated-devices',
e,
fallbackMessage: 'Error deleting simulated devices',
});
},
});
return (
<Card>
<CardHeader>
<Heading size="md">Operations</Heading>
</CardHeader>
<CardBody>
<Box>
<Heading size="sm">Delete Simulated Devices</Heading>
<Text fontStyle="italic">Delete all simulated devices from the database. This action cannot be undone.</Text>
<Popover>
{({ onClose }) => (
<>
<PopoverTrigger>
<Button colorScheme="red" rightIcon={<Trash size={20} />}>
Delete
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>Confirm</PopoverHeader>
<PopoverBody>
<Text>Are you sure you want to delete all simulated devices?</Text>
<Center mt={4}>
<Button onClick={onClose} mr={1}>
Cancel
</Button>
<DeleteButton
ml={1}
isLoading={deleteSimulatedDevices.isLoading}
onClick={async () => {
await handleDeleteSimulatedDevices();
onClose();
}}
isCompact={false}
/>
</Center>
</PopoverBody>
</PopoverContent>
</>
)}
</Popover>
</Box>
</CardBody>
</Card>
);
};
export default AdvancedSystemPage;

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Box, SimpleGrid, useBoolean, useDisclosure, useToast } from '@chakra-ui/react';
import { Box, Flex, useBoolean, useDisclosure, useToast } from '@chakra-ui/react';
import { Formik, FormikProps } from 'formik';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
@@ -16,6 +16,7 @@ import { useGetDeviceTypes } from 'hooks/Network/Firmware';
import { useFormModal } from 'hooks/useFormModal';
import { useFormRef } from 'hooks/useFormRef';
import { AxiosError } from 'models/Axios';
import { SelectField } from 'components/Form/Fields/SelectField';
const CreateDefaultConfigurationModal = () => {
const { t } = useTranslation();
@@ -68,42 +69,63 @@ const CreateDefaultConfigurationModal = () => {
key={formKey}
validationSchema={DefaultConfigurationSchema(t)}
onSubmit={(data, { setSubmitting, resetForm }) => {
createConfig.mutateAsync(data, {
onSuccess: () => {
toast({
id: `config-create-success`,
title: t('common.success'),
description: t('controller.configurations.create_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
resetForm();
modalProps.onClose();
createConfig.mutateAsync(
{ ...data, modelIds: data.modelIds.map((v) => v.value) },
{
onSuccess: () => {
toast({
id: `config-create-success`,
title: t('common.success'),
description: t('controller.configurations.create_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
resetForm();
modalProps.onClose();
},
onError: (error) => {
const e = error as AxiosError;
toast({
id: `config-create-error`,
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
},
},
onError: (error) => {
const e = error as AxiosError;
toast({
id: `config-create-error`,
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
},
});
);
}}
>
<Box>
<SimpleGrid spacing={4} minChildWidth="200px">
<StringField name="name" label={t('common.name')} isRequired isDisabled={isDisabled} />
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} />
</SimpleGrid>
<Flex mb={4}>
<StringField
name="name"
label={t('common.name')}
isRequired
isDisabled={isDisabled}
maxW="340px"
mr={4}
/>
<SelectField
name="platform"
label="Platform"
options={[
{ label: 'AP', value: 'ap' },
{ label: 'Switch', value: 'switch' },
]}
isRequired
isDisabled={isDisabled}
w="max-content"
/>
</Flex>
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} mb={4} />
<MultiSelectField
name="modelIds"
label={t('controller.dashboard.device_types')}
@@ -114,9 +136,10 @@ const CreateDefaultConfigurationModal = () => {
value: devType,
})) ?? []
}
isCreatable
isRequired
/>
<StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} />
<StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} mt={4} />
</Box>
</Formik>
</Box>

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Box, SimpleGrid, useBoolean, UseDisclosureReturn, useToast } from '@chakra-ui/react';
import { Box, Flex, useBoolean, UseDisclosureReturn, useToast } from '@chakra-ui/react';
import { Formik, FormikProps } from 'formik';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
@@ -15,6 +15,7 @@ import { useGetDeviceTypes } from 'hooks/Network/Firmware';
import { useFormModal } from 'hooks/useFormModal';
import { useFormRef } from 'hooks/useFormRef';
import { AxiosError } from 'models/Axios';
import { SelectField } from 'components/Form/Fields/SelectField';
type Props = {
modalProps: UseDisclosureReturn;
@@ -69,47 +70,69 @@ const EditDefaultConfiguration = ({ modalProps, config }: Props) => {
innerRef={formRef as React.Ref<FormikProps<DefaultConfigurationResponse>>}
initialValues={{
...config,
modelIds: config.modelIds.map((v) => ({ label: v, value: v })),
configuration: JSON.stringify(config.configuration, null, 2),
}}
key={formKey}
validationSchema={DefaultConfigurationSchema(t)}
onSubmit={(data, { setSubmitting, resetForm }) => {
updateConfig.mutateAsync(data, {
onSuccess: () => {
toast({
id: `config-edit-success`,
title: t('common.success'),
description: t('controller.configurations.update_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
resetForm();
modalProps.onClose();
updateConfig.mutateAsync(
{ ...data, modelIds: data.modelIds.map((v) => v.value) },
{
onSuccess: () => {
toast({
id: `config-edit-success`,
title: t('common.success'),
description: t('controller.configurations.update_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
resetForm();
modalProps.onClose();
},
onError: (error) => {
const e = error as AxiosError;
toast({
id: `config-edit-error`,
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
},
},
onError: (error) => {
const e = error as AxiosError;
toast({
id: `config-edit-error`,
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
},
});
);
}}
>
<Box>
<SimpleGrid spacing={4} minChildWidth="200px">
<StringField name="name" label={t('common.name')} isRequired isDisabled={isDisabled} />
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} />
</SimpleGrid>
<Flex mb={4}>
<StringField
name="name"
label={t('common.name')}
isRequired
isDisabled={isDisabled}
maxW="340px"
mr={4}
/>
<SelectField
name="platform"
label="Platform"
options={[
{ label: 'AP', value: 'ap' },
{ label: 'Switch', value: 'switch' },
]}
isRequired
isDisabled
w="max-content"
/>
</Flex>
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} mb={4} />
<MultiSelectField
name="modelIds"
label={t('controller.dashboard.device_types')}
@@ -120,9 +143,16 @@ const EditDefaultConfiguration = ({ modalProps, config }: Props) => {
value: devType,
})) ?? []
}
isCreatable
isRequired
/>
<StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} />
<StringField
name="configuration"
label={t('configurations.one')}
isArea
isDisabled={isDisabled}
mt={4}
/>
</Box>
</Formik>
)}

View File

@@ -58,6 +58,14 @@ const DefaultConfigurationsList = () => {
Cell: ({ cell }) => dateCell(cell.row.original.lastModified),
customWidth: '50px',
},
{
id: 'platform',
Header: 'Platform',
Footer: '',
accessor: 'platform',
Cell: ({ cell }) => cell.row.original.platform.toUpperCase(),
customWidth: '50px',
},
{
id: 'modelIds',
Header: t('controller.dashboard.device_types'),

View File

@@ -6,7 +6,8 @@ export const DefaultConfigurationSchema = (t: (str: string) => string) =>
.shape({
name: Yup.string().required(t('form.required')),
description: Yup.string(),
modelIds: Yup.array().of(Yup.string()).required(t('form.required')).min(1, t('form.required')),
modelIds: Yup.array().of(Yup.object()).required(t('form.required')).min(1, t('form.required')),
platform: Yup.string().oneOf(['ap', 'switch']).required(t('form.required')),
configuration: Yup.string()
.required(t('form.required'))
.test('configuration', t('form.invalid_json'), (v) => testJson(v ?? '')),
@@ -15,5 +16,6 @@ export const DefaultConfigurationSchema = (t: (str: string) => string) =>
name: '',
description: '',
modelIds: [],
platform: 'ap',
configuration: '',
});

View File

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

View File

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

View File

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

View File

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

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