Compare commits

...

127 Commits

Author SHA1 Message Date
Sebastian Rubina
c4aff418ed Merge pull request #233 from Telecominfraproject/WIFI-14521-set-correct-tag-for-main
Set correct tag for helm version
2025-08-05 13:06:11 -04:00
Carsten Schafer
dd5c894b03 Set correct tag for helm version
Signed-off-by: Carsten Schafer <Carsten.Schafer@kinarasystems.com>
2025-08-05 11:51:43 -04:00
Sebastian Rubina
c3256b93c7 Merge pull request #232 from Telecominfraproject/re-enroll-modal
Add device re-enrollment with confirmation modal
2025-07-14 15:50:59 -04:00
Sebastian Rubina
932f1f4a12 Change wording of translation.json.
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-14 15:48:41 -04:00
Sebastian Rubina
db3cbb0b35 Add device re-enrollment with confirmation modal
- Add ReEnrollModal component for user confirmation before re-enrollment
  - Update DeviceActionDropdown to open modal instead of direct action
  - Add modal state management in Device Wrapper component
  - Add translation keys for re-enrollment UI with certificate renewal
  messaging
  - Remove direct useReEnroll hook usage in favor of modal pattern

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-14 15:38:30 -04:00
Sebastian Rubina
c895274ebf Merge pull request #231 from Telecominfraproject/re-enroll-devices
Add device re-enrollment functionality
2025-07-14 13:31:58 -04:00
Sebastian Rubina
a3647bca08 Add device re-enrollment functionality
- Add re-enrollment API hook with mutation handling
  - Add re-enroll option to device action dropdown menu
  - Add translation keys for re-enrollment UI messages

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-14 13:16:26 -04:00
Carsten Schafer
5fbf421d77 Merge pull request #230 from Telecominfraproject/display-certificate-issuer
Display certificate issuer
2025-07-02 13:49:24 -04:00
Carsten Schafer
e09b3ee5f4 Merge branch 'main' into display-certificate-issuer 2025-07-02 11:45:47 -04:00
Sebastian Rubina
855960559d Update package.json version 2025-07-02 11:33:03 -04:00
Sebastian Rubina
4cecfc6fc4 Display Certificate Issuer
- Add label on translation.json
- Add new key on DeviceStatus
- Add column label and data on Summary.txt

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-02 09:49:49 -04:00
Sebastian Rubina
e62d1e4a98 Display Certificate Issuer
- Add label on translation.json
- Add new key on DeviceStatus
- Add column label and data on Summary.txt

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-02 09:49:49 -04:00
Ivan Chvets
6dddba0848 fix: Version update - release 4.0.0
Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-02 09:49:49 -04: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
4a74bfebc4 [WIFI-12664] Fixed firmware list dates in firmware upgrade modal
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-06-12 20:11:34 +02: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
e65f577202 [WIFI-12651] Added dev release toggle to firmware upgrade modal
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-06-06 14:21:50 +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
070a03c73e [WIFI-12614] Dynamic VLAN support fix
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-18 08:59:11 +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
a154fffcce [WIFI-12614] Dynamic VLAN support
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-17 18:03:42 +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
edcca87acf [WIFI-12613] Display reboot logs on device page
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-17 10:28:19 +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
cafb950aa7 [WIFI-12612] Add support for connectReason
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-17 10:10:32 +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
e6307648da [WIFI-12603] Fallback if country code is contained in device type
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-15 19:26:51 +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
37666c5075 [WIFI-12585] Fix entity button on device page
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-10 10:18:55 +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
db5611233b [WIFI-12585] Fix entity button on device page
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-10 09:45:53 +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
be3f5548f4 [WIFI-12576] Cache fixes
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-03 18:07:41 +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
130d71d5a0 [WIFI-12574] Theme improvements
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-03 09:57:44 +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
5947f3362d [WIFI-12501] Devices table column reordering
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-05-02 11:42:21 +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
6f7876d3e7 [WIFI-12515] Using simulated value directly instead of certificate
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-19 17:14:06 +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
eaca70d29b [WIFI-12515] Display simulated status in device table
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-18 13:53:25 +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
53b3926e29 [WIFI-12515] Display simulated status in device table
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-18 11:47:27 +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
82e153c277 [WIFI-12441] Added export button to device table
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-18 11:20:30 +02:00
Charles
b080b73b97 [WIFI-12441] Added export button to device table
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-18 11:19: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
efc80a183b [WIFI-12441] Added export button to device table
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-18 10:58:26 +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
b870cf828a [WIFI-12436] Fixing crash when certain values are missing in device table
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-13 13:56:40 +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
f70992e9a1 [WIFI-12506] Added radius search and radius clients tile
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-12 10:43:35 +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
df1686a2ae [WIFI-12437] Improved commonly used device actions accessibility
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-10 11:04:05 +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
ad5b0ce2a0 [WIFI-12435] [WIFI-12436] Device table added functionality and styling fixes
Signed-off-by: Charles <charles.bourque96@gmail.com>
2023-04-10 10:51:01 +02:00
271 changed files with 13967 additions and 10159 deletions

1
.env
View File

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

View File

@@ -20,7 +20,7 @@ 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

View File

@@ -11,7 +11,7 @@ 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

1
.gitignore vendored
View File

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

View File

@@ -3,3 +3,4 @@ build
dist
node_modules
.github
/helm

View File

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

10111
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.9.0(23)",
"version": "4.1.0",
"description": "",
"private": true,
"main": "index.tsx",
@@ -15,82 +15,87 @@
"author": "",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.11",
"@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.11",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fontsource/inter": "^4.5.14",
"@chakra-ui/utils": "^2.0.14",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@fontsource/inter": "^4.5.15",
"@googlemaps/react-wrapper": "^1.1.35",
"@googlemaps/typescript-guards": "^2.0.3",
"@react-spring/web": "^9.5.5",
"axios": "^1.1.3",
"@hello-pangea/dnd": "^16.2.0",
"@phosphor-icons/react": "^2.0.8",
"@react-spring/web": "^9.7.2",
"@tanstack/react-query": "^4.29.3",
"@tanstack/react-table": "^8.8.5",
"@textea/json-viewer": "^2.16.2",
"axios": "^1.3.5",
"buffer": "^6.0.3",
"chakra-react-select": "^4.3.0",
"chakra-react-select": "^4.6.0",
"chart.js": "^3.9.1",
"dagre": "^0.8.5",
"fast-equals": "^5.0.1",
"formik": "^2.2.9",
"fast-equals": "^4.0.3",
"framer-motion": "^7.6.1",
"i18next": "^22.0.0",
"i18next-browser-languagedetector": "^6.1.8",
"i18next-http-backend": "^1.4.4",
"libphonenumber-js": "^1.10.14",
"phosphor-react": "^1.4.1",
"framer-motion": "^10.12.2",
"i18next": "^22.4.14",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
"libphonenumber-js": "^1.10.26",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-chartjs-2": "^4.3.1",
"chart.js": "^3.9.1",
"react-country-flag": "^3.0.2",
"react-country-flag": "^3.1.0",
"react-csv": "^2.2.2",
"react-datepicker": "^4.8.0",
"react-datepicker": "^4.11.0",
"react-dom": "^18.2.0",
"@textea/json-viewer": "^2.10.0",
"react-fast-compare": "^3.2.0",
"react-i18next": "^11.18.6",
"react-fast-compare": "^3.2.1",
"react-i18next": "^12.2.0",
"react-masonry-css": "^1.0.16",
"@tanstack/react-query": "^4.12.0",
"react-router-dom": "^6.4.2",
"react-router-dom": "^6.10.0",
"react-table": "^7.8.0",
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.8",
"react-virtualized-auto-sizer": "^1.0.15",
"react-window": "^1.8.9",
"source-map-explorer": "^2.5.3",
"vite": "^3.1.8",
"typescript": "^4.8.4",
"typescript": "^5.0.4",
"uuid": "^9.0.0",
"vite": "^4.2.1",
"yup": "^0.32.11",
"zustand": "^4.1.2"
"zustand": "^4.3.7"
},
"devDependencies": {
"@types/google.maps": "^3.51.0",
"@types/node": "^18.11.2",
"@types/react": "^18.0.21",
"@types/google.maps": "^3.52.5",
"@types/node": "^18.15.11",
"@types/react": "^18.0.37",
"@types/react-csv": "^1.1.3",
"@types/react-dom": "^18.0.6",
"@types/react-table": "^7.7.12",
"@types/react-datepicker": "4.8.0",
"@types/uuid": "^8.3.4",
"@types/react-datepicker": "4.10.0",
"@types/react-dom": "^18.0.11",
"@types/react-table": "^7.7.14",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"eslint": "8.25.0",
"vite-tsconfig-paths": "^3.5.1",
"lint-staged": "^13.0.3",
"@vitejs/plugin-react": "^2.1.0",
"vite-plugin-pwa": "^0.13.1",
"prettier": "^2.7.1",
"@types/uuid": "^9.0.1",
"@vitejs/plugin-react": "^3.1.0",
"eslint": "8.38.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-airbnb-typescript-prettier": "^5.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-no-inline-styles": "^1.0.5",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0"
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"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": {
"production": [

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: 32 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: 24 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

@@ -64,6 +64,8 @@
"health": "Gesundheit",
"inactive": "Inaktiv",
"interval": "Intervall",
"last_connected": "Zuletzt verbunden",
"last_connected_tooltip": "Das letzte Mal, wann dieses Gerät mit dem Controller verbunden war. Dies kann verwendet werden, um abzuschätzen, wann ein Gerät getrennt wurde",
"last_connection": "Letzte Verbindung",
"last_contact": "Letzter Kontakt",
"last_disconnection": "Letzte Trennung",
@@ -221,6 +223,7 @@
"day": "Tag",
"days": "Tage",
"default": "Standard",
"defaults": "Standardeinstellungen",
"description": "Beschreibung",
"details": "Einzelheiten",
"device_details": "Gerätedetails",
@@ -241,6 +244,7 @@
"error_download": "Fehler beim Downloadversuch: {{e}}",
"errors": "Fehler",
"exit_fullscreen": "Ausgang",
"export": "Export",
"finished": "Fertig",
"fullscreen": "Vollbildschirm",
"general_error": "Fehler beim Verbinden mit dem Server. Bitte wenden Sie sich an Ihren Administrator.",
@@ -265,6 +269,7 @@
"map": "Karte",
"max": "Max",
"min": "MINDEST",
"miscellaneous": "Verschiedenes",
"mode": "Modus",
"model": "Modell",
"modified": "Geändert",
@@ -356,6 +361,7 @@
"error_pushes_one": "Fehler (könnte an einer fehlerhaften Konfiguration liegen): {{count}}",
"error_pushes_other": "Fehler (können auf eine fehlerhafte Konfiguration zurückzuführen sein): {{count}}",
"expert_name": "Expertenmodus",
"expert_name_explanation": "Sie können den Expertenmodus verwenden, um Ihre Konfiguration direkt zu ändern, einschließlich des Hinzufügens von Werten, die derzeit nicht von der Benutzeroberfläche unterstützt werden. Bitte verwenden Sie dieses Format: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
"explanation": "Erläuterung",
"firewall": "Firewall",
"firmware_upgrade": "Firmware-Aktualisierung",
@@ -522,6 +528,7 @@
"ouis_explanation": "OUIs von Geräten, die sich mit diesem Firmware-Server verbunden haben",
"outdated_one": "Firmware {{count}} Tag alt",
"outdated_other": "Firmware {{count}} Tage alt",
"outdated_unknown": "Firmware unbekannten Alters",
"release": "Veröffentlichung",
"show_dev_releases": "Entwicklerversionen",
"status_explanation": "Verbindungsstatus von Geräten, die sich mit diesem Firmware-Server verbunden haben",
@@ -537,6 +544,16 @@
"queue": {
"title": "Ereigniswarteschlange"
},
"radius": {
"calling_station_id": "Stations",
"disconnect": "Trennen",
"disconnect_success": "Radius-Sitzung getrennt!",
"input_octets": "Eingang",
"output_octets": "Ausgabe",
"radius_clients": "Radius-Kunden",
"session_time": "Sitzungszeit",
"username": "Nutzername"
},
"stats": {
"load": "Belastung (1 | 5 | 15 m.)",
"seconds_ago": " Vor {{s}} Sekunden",
@@ -629,6 +646,7 @@
"notifications": "Gerätebenachrichtigungen",
"one": "Gerät",
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
"reboot_logs": "Neustartprotokolle",
"restricted": "Beschränkt",
"restricted_overriden": "Dies ist ein eingeschränktes Gerät, aber es befindet sich im Entwicklungsmodus. Alle Einschränkungen werden derzeit ignoriert",
"restrictions_overriden_title": "Dev-Modus",
@@ -686,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",
@@ -699,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",
@@ -712,7 +752,7 @@
"invalid_ipv6": "Ungültige IPv6-Adresse (Bsp.: 2001:db8:3333:4444:5555:6666:7777:8888)",
"invalid_json": "Ungültige JSON-Zeichenfolge",
"invalid_lease_time": "Ungültiger Lease-Time-Wert! Sie müssen im digitUnit-Format vorliegen. Zum Beispiel: 6d2h5m, was 6 Tage, 2 Stunden und 5 Minuten bedeutet. Hier sind die akzeptierten Einheiten: m, h, d. Wenn Sie eine Einheit nicht verwenden möchten, lassen Sie sie vollständig weg. Anstatt also 0d2h0m zu sagen, verwenden Sie 2h",
"invalid_mac_uc": "Ungültiger UC-MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
"invalid_mac_uc": "Ungültiger MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
"invalid_password": "Ungültiges Passwort, bitte sehen Sie sich die Passwortrichtlinie an",
"invalid_phone_number": "Ungültige Telefonnummer",
"invalid_phone_numbers": "Mindestens eine der Telefonnummern ist ungültig. Bitte geben Sie sie ohne Symbole und Leerzeichen oder in diesem Format an: +1(123)123-1234",
@@ -725,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",
@@ -867,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",
@@ -932,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",
@@ -990,6 +1060,11 @@
"concurrent_devices": "Gleichzeitige Geräte",
"controller": "Regler",
"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",
@@ -997,6 +1072,7 @@
"infinite": "Unendlich",
"keep_alive": "Bleib am Leben",
"mac_prefix": "MAC-Präfix",
"mac_prefix_length": "Ihr MAC-Präfix muss gültige 6 HEX-Ziffern haben (z. B.: 00112233)",
"max_associations": "max. Verbände",
"max_clients": "Max. Kunden",
"min_associations": "Mindest. Verbände",
@@ -1013,6 +1089,7 @@
"rx_messages": "Rx-Meldungen",
"sim_currently_running": "Simulation \"{{sim}}\" läuft",
"sim_history": "{{sim}} Vorherige Läufe",
"simulated": "Simuliert",
"start": "Simulation starten",
"start_success": "Simulationslauf gestartet!",
"state_interval": "Zustandsintervall",
@@ -1026,6 +1103,7 @@
},
"statistics": {
"last_stats": "Letzte Statistik",
"latest": "Neueste Statistiken",
"memory": "Erinnerung"
},
"subscribers": {
@@ -1045,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",
@@ -1072,14 +1151,24 @@
"version": "Ausführung"
},
"table": {
"columns": "Säulen",
"columns_hidden_one": "{{count}} Spalte ausgeblendet",
"columns_hidden_other": "{{count}} Spalten ausgeblendet",
"display_column": "Anzeige",
"drag_always_show": "Sie können diese Spalte nicht ausblenden, aber ihre Position ändern",
"drag_explanation": "Ziehen und Ablegen zum Neuordnen und Ändern der Spaltensichtbarkeit",
"drag_locked": "Diese Säule ist in ihrer Position arretiert",
"export_current_page": "Nur aktuelle Seite",
"first_page": "Erste Seite",
"go_to_page": "Zur Seite gehen",
"hide_column": "verbergen",
"last_page": "Letzte Seite",
"next_page": "Nächste Seite",
"page": "Seite",
"previous_page": "Vorherige Seite"
"preferences": "Tabelleneinstellungen",
"previous_page": "Vorherige Seite",
"reset": "Einstellungen zurücksetzen",
"settings": "die Einstellungen"
},
"user": {
"email_not_validated": "E-Mail nicht validiert",

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,8 @@
"health": "salud",
"inactive": "inactivo",
"interval": "intervalo",
"last_connected": "Última conexion",
"last_connected_tooltip": "La última vez que se conectó este dispositivo al controlador. Esto se puede usar para estimar cuándo se desconectó un dispositivo",
"last_connection": "Última conexion",
"last_contact": "Último contacto",
"last_disconnection": "Última desconexión",
@@ -221,6 +223,7 @@
"day": "Día",
"days": "días",
"default": "Defecto",
"defaults": "Valores predeterminados",
"description": "Descripción",
"details": "Detalles",
"device_details": "Detalles del dispositivo",
@@ -241,6 +244,7 @@
"error_download": "Error al intentar descargar: {{e}}",
"errors": "Los errores",
"exit_fullscreen": "salida",
"export": "Exportar",
"finished": "terminado",
"fullscreen": "Pantalla Completa",
"general_error": "Error al conectar con el servidor. Consulte a su administrador.",
@@ -265,6 +269,7 @@
"map": "Mapa",
"max": "Max",
"min": "Min",
"miscellaneous": "Diverso",
"mode": "Modo",
"model": "Modelo",
"modified": "Modificado",
@@ -356,6 +361,7 @@
"error_pushes_one": "Error (podría deberse a una mala configuración): {{count}}",
"error_pushes_other": "Errores (pueden deberse a una mala configuración): {{count}}",
"expert_name": "Modo experto",
"expert_name_explanation": "Puede usar el modo experto para modificar directamente su configuración, incluida la adición de valores que actualmente no son compatibles con la interfaz de usuario. Utilice este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
"explanation": "Explicación",
"firewall": "cortafuegos",
"firmware_upgrade": "Actualización de firmware",
@@ -522,6 +528,7 @@
"ouis_explanation": "OUI de dispositivos que se han conectado a este servidor de firmware",
"outdated_one": "Firmware {{count}} día de antigüedad",
"outdated_other": "Firmware de {{count}} días de antigüedad",
"outdated_unknown": "Firmware de antigüedad desconocida",
"release": "Lanzamiento",
"show_dev_releases": "Lanzamientos de desarrollo",
"status_explanation": "Estado de conexión de los dispositivos que se han conectado a este servidor de firmware",
@@ -537,6 +544,16 @@
"queue": {
"title": "Cola de eventos"
},
"radius": {
"calling_station_id": "Estación",
"disconnect": "desconectar",
"disconnect_success": "¡Sesión de radio desconectada!",
"input_octets": "entrada",
"output_octets": "salida",
"radius_clients": "Clientes de radio",
"session_time": "Tiempo de sesión",
"username": "Nombre de usuario"
},
"stats": {
"load": "Carga (1 | 5 | 15 m.)",
"seconds_ago": " Hace {{s}} segundos",
@@ -629,6 +646,7 @@
"notifications": "notificaciones de dispositivos",
"one": "Dispositivo",
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
"reboot_logs": "Reiniciar registros",
"restricted": "Restringido",
"restricted_overriden": "Este es un dispositivo restringido, pero está en modo de desarrollo. Actualmente se ignoran todas las restricciones.",
"restrictions_overriden_title": "MODO DE DESARROLLO",
@@ -686,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",
@@ -699,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",
@@ -712,7 +752,7 @@
"invalid_ipv6": "Dirección IPv6 no válida (ej.: 2001:db8:3333:4444:5555:6666:7777:8888)",
"invalid_json": "Cadena JSON no válida",
"invalid_lease_time": "¡Valor de tiempo de arrendamiento no válido! Deben estar en el formato digitUnit. Por ejemplo: 6d2h5m, lo que significa 6 días, 2 horas y 5 minutos. Estas son las unidades aceptadas: m, h, d. Si no desea utilizar una unidad, omítala por completo. Entonces, en lugar de decir 0d2h0m, usa 2h",
"invalid_mac_uc": "Valor de UC-MAC no válido, por ejemplo: 00:00:5e:00:53:af",
"invalid_mac_uc": "Valor de MAC no válido, por ejemplo: 00:00:5e:00:53:af",
"invalid_password": "Contraseña no válida, consulte la política de contraseñas",
"invalid_phone_number": "Numero de telefono invalido",
"invalid_phone_numbers": "Uno o más de los números de teléfono no son válidos. Proporciónelos sin símbolos ni espacios, o en este formato: +1(123)123-1234",
@@ -725,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",
@@ -867,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",
@@ -932,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",
@@ -990,6 +1060,11 @@
"concurrent_devices": "Dispositivos concurrentes",
"controller": "Controlador",
"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",
@@ -997,6 +1072,7 @@
"infinite": "infinito",
"keep_alive": "Mantener viva",
"mac_prefix": "Prefijo MAC",
"mac_prefix_length": "Su prefijo MAC debe tener 6 dígitos hexadecimales válidos (p. ej.: 00112233)",
"max_associations": "Max. Asociaciones",
"max_clients": "Max. Clientela",
"min_associations": "Min. Asociaciones",
@@ -1013,6 +1089,7 @@
"rx_messages": "Mensajes prescritos",
"sim_currently_running": "Simulación \"{{sim}}\" en curso",
"sim_history": "{{sim}} ejecuciones anteriores",
"simulated": "Simulado",
"start": "Iniciar simulación",
"start_success": "¡Ejecución de simulación iniciada!",
"state_interval": "Intervalo de estado",
@@ -1026,6 +1103,7 @@
},
"statistics": {
"last_stats": "Últimas estadísticas",
"latest": "Últimas estadísticas",
"memory": "Memoria"
},
"subscribers": {
@@ -1045,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}} ",
@@ -1072,14 +1151,24 @@
"version": "Versión"
},
"table": {
"columns": "Columnas",
"columns_hidden_one": "{{count}} columna oculta",
"columns_hidden_other": "{{count}} columnas ocultas",
"display_column": "Monitor",
"drag_always_show": "No puede ocultar esta columna pero puede cambiar su posición",
"drag_explanation": "Arrastre y suelte para reordenar y cambiar la visibilidad de las columnas",
"drag_locked": "Esta columna está bloqueada en su posición.",
"export_current_page": "Solo página actual",
"first_page": "Primera pagina",
"go_to_page": "Ir a la página",
"hide_column": "Esconder",
"last_page": "Ultima pagina",
"next_page": "Siguiente página",
"page": "Página",
"previous_page": "Página anterior"
"preferences": "Preferencias de mesa",
"previous_page": "Página anterior",
"reset": "Reiniciar preferencias",
"settings": "Ajustes"
},
"user": {
"email_not_validated": "correo electrónico no validado",

View File

@@ -64,6 +64,8 @@
"health": "santé",
"inactive": "Inactif",
"interval": "Intervalle",
"last_connected": "Dernière connexion",
"last_connected_tooltip": "La dernière fois que cet appareil a été connecté au contrôleur. Cela peut être utilisé pour estimer quand un appareil s'est déconnecté",
"last_connection": "Dernière connexion",
"last_contact": "Dernier contact",
"last_disconnection": "Dernière déconnexion",
@@ -221,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",
@@ -241,6 +244,7 @@
"error_download": "Erreur lors de la tentative de téléchargement : {{e}}",
"errors": "les erreurs",
"exit_fullscreen": "Sortie",
"export": "Exportation",
"finished": "fini",
"fullscreen": "Plein écran",
"general_error": "Erreur de connexion au serveur. Veuillez consulter votre administrateur.",
@@ -265,6 +269,7 @@
"map": "Carte",
"max": "Max",
"min": "Min",
"miscellaneous": "Divers",
"mode": "Mode",
"model": "Modèle",
"modified": "Modifié",
@@ -356,6 +361,7 @@
"error_pushes_one": "Erreur (peut être due à une mauvaise configuration) : {{count}}",
"error_pushes_other": "Erreurs (peut-être dues à une mauvaise configuration) : {{count}}",
"expert_name": "Mode expert",
"expert_name_explanation": "Vous pouvez utiliser le mode expert pour modifier directement votre configuration, notamment en ajoutant des valeurs qui ne sont pas actuellement prises en charge par l'interface utilisateur. Veuillez utiliser ce format : { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
"explanation": "Explication",
"firewall": "Pare-feu",
"firmware_upgrade": "Mise à jour du firmware",
@@ -522,6 +528,7 @@
"ouis_explanation": "OUI des appareils qui se sont connectés à ce serveur de firmware",
"outdated_one": "Micrologiciel vieux de {{count}} jours",
"outdated_other": "Micrologiciel vieux de {{count}} jours",
"outdated_unknown": "Firmware d'âge inconnu",
"release": "libération",
"show_dev_releases": "Versions de développement",
"status_explanation": "État de connexion des appareils qui se sont connectés à ce serveur de micrologiciel",
@@ -537,6 +544,16 @@
"queue": {
"title": "File d'attente d'événements"
},
"radius": {
"calling_station_id": "Station",
"disconnect": "déconnecter",
"disconnect_success": "Session Radius déconnectée !",
"input_octets": "Contribution",
"output_octets": "Sortie",
"radius_clients": "Clients rayon",
"session_time": "Temps de session",
"username": "Nom d'utilisateur"
},
"stats": {
"load": "Charge (1 | 5 | 15 m.)",
"seconds_ago": " Il y a {{s}} secondes",
@@ -629,6 +646,7 @@
"notifications": "notifications de l'appareil",
"one": "Dispositif",
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
"reboot_logs": "Journaux de redémarrage",
"restricted": "Limité",
"restricted_overriden": "Il s'agit d'un appareil restreint, mais il est en mode développement. Toutes les restrictions sont actuellement ignorées",
"restrictions_overriden_title": "Mode développement",
@@ -686,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",
@@ -699,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",
@@ -712,7 +752,7 @@
"invalid_ipv6": "Adresse IPv6 invalide (ex. : 2001:db8:3333:4444:5555:6666:7777:8888)",
"invalid_json": "Chaîne JSON non valide",
"invalid_lease_time": "Valeur de durée de bail non valide ! Ils doivent être au format digitUnit. Par exemple : 6d2h5m, ce qui signifie 6 jours, 2 heures et 5 minutes. Voici les unités acceptées : m, h, d. Si vous ne voulez pas utiliser une unité, omettez-la complètement. Donc au lieu de dire 0d2h0m, utilisez 2h",
"invalid_mac_uc": "Valeur UC-MAC non valide, par exemple : 00:00:5e:00:53:af",
"invalid_mac_uc": "Valeur MAC non valide, par exemple : 00:00:5e:00:53:af",
"invalid_password": "Mot de passe invalide, veuillez consulter la politique de mot de passe",
"invalid_phone_number": "Numéro de téléphone invalide",
"invalid_phone_numbers": "Un ou plusieurs des numéros de téléphone sont invalides. Veuillez les fournir sans symboles ni espaces, ou dans ce format : +1(123)123-1234",
@@ -725,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",
@@ -867,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",
@@ -932,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",
@@ -990,6 +1060,11 @@
"concurrent_devices": "Périphériques simultanés",
"controller": "Manette",
"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",
@@ -997,6 +1072,7 @@
"infinite": "Infini",
"keep_alive": "Rester en vie",
"mac_prefix": "Préfixe MAC",
"mac_prefix_length": "Votre préfixe MAC doit être valide à 6 chiffres HEX (ex. : 00112233)",
"max_associations": "Max. Les associations",
"max_clients": "Max. Clients",
"min_associations": "Min. Les associations",
@@ -1013,6 +1089,7 @@
"rx_messages": "Messages reçus",
"sim_currently_running": "Simulation \"{{sim}}\" en cours",
"sim_history": "{{sim}} courses précédentes",
"simulated": "Simulé",
"start": "Démarrer la simulation",
"start_success": "Lancement de la simulation !",
"state_interval": "Intervalle d'état",
@@ -1026,6 +1103,7 @@
},
"statistics": {
"last_stats": "Dernières statistiques",
"latest": "Dernières statistiques",
"memory": "mémoire"
},
"subscribers": {
@@ -1045,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}} ",
@@ -1072,14 +1151,24 @@
"version": "Version"
},
"table": {
"columns": "Les colonnes",
"columns_hidden_one": "{{count}} Colonne masquée",
"columns_hidden_other": "{{count}} colonnes masquées",
"display_column": "Afficher",
"drag_always_show": "Vous ne pouvez pas masquer cette colonne, mais vous pouvez modifier sa position",
"drag_explanation": "Glisser-déposer pour réorganiser et modifier la visibilité des colonnes",
"drag_locked": "Cette colonne est verrouillée dans sa position",
"export_current_page": "Page actuelle uniquement",
"first_page": "Première page",
"go_to_page": "Aller à la page",
"hide_column": "Cacher",
"last_page": "Dernière page",
"next_page": "Page suivante",
"page": "Page",
"previous_page": "Page précédente"
"preferences": "Préférences de tableau",
"previous_page": "Page précédente",
"reset": "Remettre à zéro les préférences",
"settings": "Réglages"
},
"user": {
"email_not_validated": "Mail non valide",

View File

@@ -64,6 +64,8 @@
"health": "Saúde",
"inactive": "Inativo",
"interval": "intervalo",
"last_connected": "última conexão",
"last_connected_tooltip": "Última vez que este dispositivo foi conectado ao controlador. Isso pode ser usado para estimar quando um dispositivo desconectado",
"last_connection": "última conexão",
"last_contact": "Último contato",
"last_disconnection": "Última desconexão",
@@ -221,6 +223,7 @@
"day": "Dia",
"days": "Dias",
"default": "Padrão",
"defaults": "Predefinições",
"description": "Descrição",
"details": "Detalhes",
"device_details": "Detalhes do dispositivo",
@@ -241,6 +244,7 @@
"error_download": "Erro ao tentar fazer o download: {{e}}",
"errors": "Erros",
"exit_fullscreen": "Saída",
"export": "Exportar",
"finished": "acabado",
"fullscreen": "Tela cheia",
"general_error": "Erro ao se conectar ao servidor. Consulte seu administrador.",
@@ -265,6 +269,7 @@
"map": "Mapa",
"max": "máximo",
"min": "minuto",
"miscellaneous": "Diversos",
"mode": "Modo",
"model": "Modelo",
"modified": "Modificado",
@@ -356,6 +361,7 @@
"error_pushes_one": "Erro (pode ser devido à configuração incorreta): {{count}}",
"error_pushes_other": "Erros (podem ser devido à configuração incorreta): {{count}}",
"expert_name": "MODO EXPERT",
"expert_name_explanation": "Você pode usar o modo especialista para modificar diretamente sua configuração, incluindo a adição de valores que não são atualmente suportados pela interface do usuário. Use este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
"explanation": "Explicação",
"firewall": "Firewall",
"firmware_upgrade": "Atualização de firmware",
@@ -522,6 +528,7 @@
"ouis_explanation": "OUIs de dispositivos que se conectaram a este servidor de firmware",
"outdated_one": "Firmware com {{count}} dias",
"outdated_other": "Firmware com {{count}} dias",
"outdated_unknown": "Firmware de idade desconhecida",
"release": "LANÇAMENTO",
"show_dev_releases": "Lançamentos do desenvolvedor",
"status_explanation": "Status da conexão dos dispositivos que se conectaram a este servidor de firmware",
@@ -537,6 +544,16 @@
"queue": {
"title": "Fila de Eventos"
},
"radius": {
"calling_station_id": "estação",
"disconnect": "Desconectar",
"disconnect_success": "Sessão Radius desconectada!",
"input_octets": "Entrada",
"output_octets": "Saída",
"radius_clients": "Clientes Radius",
"session_time": "Tempo de sessão",
"username": "Nome de usuário"
},
"stats": {
"load": "Carga (1 | 5 | 15 m.)",
"seconds_ago": "{{s}} segundos atrás",
@@ -629,6 +646,7 @@
"notifications": "Notificações do dispositivo",
"one": "Dispositivo",
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
"reboot_logs": "Registros de reinicialização",
"restricted": "Restrito",
"restricted_overriden": "Este é um dispositivo restrito, mas está em modo de desenvolvimento. Todas as restrições são atualmente ignoradas",
"restrictions_overriden_title": "Modo de desenvolvedor",
@@ -686,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",
@@ -699,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",
@@ -712,7 +752,7 @@
"invalid_ipv6": "Endereço IPv6 inválido (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
"invalid_json": "Sequência JSON inválida",
"invalid_lease_time": "Valor de tempo de locação inválido! Eles precisam estar no formato digitUnit. Por exemplo: 6d2h5m, que significa 6 dias, 2 horas e 5 minutos. Aqui estão as unidades aceitas: m, h, d. Se você não quiser usar uma unidade, omita-a completamente. Então, em vez de dizer 0d2h0m, use 2h",
"invalid_mac_uc": "Valor UC-MAC inválido, por exemplo: 00:00:5e:00:53:af",
"invalid_mac_uc": "Valor MAC inválido, por exemplo: 00:00:5e:00:53:af",
"invalid_password": "Senha inválida, consulte a política de senha",
"invalid_phone_number": "Número de telefone inválido",
"invalid_phone_numbers": "Um ou mais números de telefone são inválidos. Forneça-os sem símbolos e espaços ou neste formato: +1(123)123-1234",
@@ -725,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",
@@ -867,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",
@@ -932,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",
@@ -990,6 +1060,11 @@
"concurrent_devices": "Dispositivos Simultâneos",
"controller": "Controlador",
"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",
@@ -997,6 +1072,7 @@
"infinite": "Infinito",
"keep_alive": "Mantenha vivo",
"mac_prefix": "Prefixo MAC",
"mac_prefix_length": "Seu prefixo MAC precisa ter 6 dígitos HEX válidos (ex.: 00112233)",
"max_associations": "Máx. Associações",
"max_clients": "Máx. Clientes",
"min_associations": "Min. Associações",
@@ -1013,6 +1089,7 @@
"rx_messages": "Mensagens Rx",
"sim_currently_running": "Simulação \"{{sim}}\" em andamento",
"sim_history": "{{sim}} execuções anteriores",
"simulated": "Simulado",
"start": "Iniciar simulação",
"start_success": "Corrida de simulação iniciada!",
"state_interval": "Intervalo de estado",
@@ -1026,6 +1103,7 @@
},
"statistics": {
"last_stats": "Últimas estatísticas",
"latest": "Estatísticas mais recentes",
"memory": "Memória"
},
"subscribers": {
@@ -1045,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",
@@ -1072,14 +1151,24 @@
"version": "Versão"
},
"table": {
"columns": "Colunas",
"columns_hidden_one": "{{count}} Coluna oculta",
"columns_hidden_other": "{{count}} Colunas ocultas",
"display_column": "Exibição",
"drag_always_show": "Você não pode ocultar esta coluna, mas pode alterar sua posição",
"drag_explanation": "Arraste e solte para reordenar e alterar a visibilidade da coluna",
"drag_locked": "Esta coluna está travada em sua posição",
"export_current_page": "Somente página atual",
"first_page": "Primeira Página",
"go_to_page": "Vá para página",
"hide_column": "Ocultar",
"last_page": "Última Página",
"next_page": "Próxima página",
"page": "Página",
"previous_page": "Página anterior"
"preferences": "Preferências de Tabela",
"previous_page": "Página anterior",
"reset": "Reiniciar preferências",
"settings": "Definições"
},
"user": {
"email_not_validated": "e-mail não validado",

28
src/@tanstack.react-table.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { BoxProps } from '@chakra-ui/react';
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;
hasPopover?: boolean;
customMaxWidth?: string;
customWidth?: string;
isMonospace?: boolean;
isCentered?: boolean;
columnSelectorOptions?: {
label?: string;
};
rowContentOptions?: {
style?: React.CSSProperties;
};
headerOptions?: {
tooltip?: string;
};
headerStyleProps?: BoxProps;
}
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { Warning } from 'phosphor-react';
import { Warning } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { ThemeProps } from 'models/Theme';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { IconButton, SpaceProps } from '@chakra-ui/react';
import { X } from 'phosphor-react';
import { X } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
export interface CloseButtonProps extends SpaceProps {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, IconButton, Tooltip, useBreakpoint, SpaceProps } from '@chakra-ui/react';
import { Plus } from 'phosphor-react';
import { Plus } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
export interface CreateButtonProps extends SpaceProps {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { Trash } from 'phosphor-react';
import { Trash } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
export interface DeleteButtonProps {

View File

@@ -1,18 +1,17 @@
import React from 'react';
import {
Button,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
Spinner,
Tooltip,
useColorModeValue,
useToast,
} from '@chakra-ui/react';
import axios from 'axios';
import { Wrench } from 'phosphor-react';
import { Barcode, Power, TerminalWindow, WifiHigh, Wrench } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices';
@@ -33,6 +32,7 @@ interface Props {
onOpenTelemetryModal: (serialNumber: string) => void;
onOpenScriptModal: (device: GatewayDevice) => void;
onOpenRebootModal: (serialNumber: string) => void;
onOpenReEnrollModal?: (serialNumber: string) => void;
size?: 'sm' | 'md' | 'lg';
isCompact?: boolean;
}
@@ -50,13 +50,16 @@ const DeviceActionDropdown = ({
onOpenConfigureModal,
onOpenScriptModal,
onOpenRebootModal,
onOpenReEnrollModal,
size,
isCompact = true,
isCompact,
}: 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, isInitialLoading: isRtty } = useGetDeviceRtty({
const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({
serialNumber: device.serialNumber,
extraId: 'inventory-modal',
});
@@ -162,49 +165,95 @@ const DeviceActionDropdown = ({
const handleRebootClick = () => onOpenRebootModal(device.serialNumber);
return (
<Menu>
<Tooltip label={t('common.actions')}>
{size === undefined || isCompact ? (
<>
<Tooltip label={t('commands.connect')}>
<IconButton
aria-label="Connect"
icon={<TerminalWindow size={20} />}
size={size ?? 'sm'}
isDisabled={isDisabled}
isLoading={isRtty}
onClick={handleConnectClick}
colorScheme={connectColor}
hidden={isCompact}
/>
</Tooltip>
<Tooltip label={t('controller.configure.title')}>
<IconButton
aria-label={t('controller.configure.title')}
icon={<Barcode size={20} />}
size={size ?? 'sm'}
isDisabled={isDisabled}
onClick={handleOpenConfigure}
colorScheme="purple"
hidden={isCompact}
/>
</Tooltip>
<Tooltip label={t('commands.reboot')}>
<IconButton
aria-label={t('commands.reboot')}
icon={<Power size={20} />}
size={size ?? 'sm'}
isDisabled={isDisabled}
onClick={handleRebootClick}
colorScheme="green"
hidden={isCompact}
/>
</Tooltip>
<Tooltip label={t('commands.wifiscan')}>
<IconButton
aria-label={t('commands.wifiscan')}
icon={<WifiHigh size={20} />}
size={size ?? 'sm'}
isDisabled={isDisabled}
onClick={handleOpenScan}
colorScheme="teal"
hidden={isCompact || deviceType !== 'ap'}
/>
</Tooltip>
<Menu>
<Tooltip label={t('common.actions')}>
<MenuButton
as={IconButton}
aria-label="Commands"
icon={isRtty ? <Spinner /> : <Wrench size={20} />}
icon={<Wrench size={20} />}
size={size ?? 'sm'}
isDisabled={isDisabled}
ml={2}
/>
) : (
<MenuButton
as={Button}
aria-label="Commands"
rightIcon={isRtty ? <Spinner /> : <Wrench size={20} />}
size={size ?? 'sm'}
isDisabled={isDisabled}
ml={2}
>
{t('common.actions')}
</MenuButton>
)}
</Tooltip>
<Portal>
<MenuList>
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
<MenuItem onClick={handleConnectClick}>{t('commands.connect')}</MenuItem>
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
<MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
<MenuItem onClick={handleUpdateToLatest} hidden>
{t('premium.toolbox.upgrade_to_latest')}
</MenuItem>
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
</MenuList>
</Portal>
</Menu>
</Tooltip>
<Portal>
<MenuList maxH="315px" overflowY="auto">
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact || deviceType !== 'ap'}>
{t('controller.configure.title')}
</MenuItem>
<MenuItem onClick={handleConnectClick} hidden={!isCompact}>
{t('commands.connect')}
</MenuItem>
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
<MenuItem onClick={handleRebootClick} hidden={!isCompact}>
{t('commands.reboot')}
</MenuItem>
{onOpenReEnrollModal && (
<MenuItem onClick={() => onOpenReEnrollModal(device.serialNumber)}>
{t('controller.devices.re_enroll')}
</MenuItem>
)}
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
<MenuItem onClick={handleUpdateToLatest} hidden>
{t('premium.toolbox.upgrade_to_latest')}
</MenuItem>
<MenuItem onClick={handleOpenScan} hidden={!isCompact || deviceType !== 'ap'}>
{t('commands.wifiscan')}
</MenuItem>
</MenuList>
</Portal>
</Menu>
</>
);
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { Pen } from 'phosphor-react';
import { Pen } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
export interface EditButtonProps {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, IconButton, ThemeTypings, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { ArrowsClockwise } from 'phosphor-react';
import { ArrowsClockwise } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
export interface RefreshButtonProps {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { FloppyDisk } from 'phosphor-react';
import { FloppyDisk } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
export interface SaveButtonProps

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { ArrowRight, FloppyDisk } from 'phosphor-react';
import { ArrowRight, FloppyDisk } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
export interface StepButtonProps {

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-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

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
import { Warning } from 'phosphor-react';
import { Warning } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { ThemeProps } from 'models/Theme';

View File

@@ -1,17 +1,57 @@
import React from 'react';
import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
import React, { DOMAttributes } from 'react';
import {
BackgroundProps,
Box,
EffectProps,
InteractivityProps,
LayoutProps,
PositionProps,
SpaceProps,
useColorModeValue,
useStyleConfig,
useToken,
} from '@chakra-ui/react';
export interface CardHeaderProps extends LayoutProps, SpaceProps {
variant?: string;
export interface CardHeaderProps
extends LayoutProps,
SpaceProps,
BackgroundProps,
InteractivityProps,
PositionProps,
EffectProps,
DOMAttributes<HTMLDivElement> {
variant?: 'panel' | 'unstyled';
children: React.ReactNode;
icon?: React.ReactNode;
headerStyle?: {
color: string;
};
}
const _CardHeader: React.FC<CardHeaderProps> = ({ variant, children, ...rest }) => {
// @ts-ignore
const _CardHeader: React.FC<CardHeaderProps> = ({
variant,
children,
icon,
headerStyle = {
color: 'blue',
},
...rest
}) => {
const iconBgcolor = useToken('colors', [`${headerStyle?.color}.500`, `${headerStyle?.color}.300`]);
const bgColor = useToken('colors', [`${headerStyle?.color}.50`, `${headerStyle?.color}.700`]);
const iconColor = useColorModeValue(iconBgcolor[0], iconBgcolor[1]);
const headerBgColor = useColorModeValue(bgColor[0], bgColor[1]);
const styles = useStyleConfig('CardHeader', { variant });
// Pass the computed styles into the `__css` prop
return (
<Box __css={styles} {...rest}>
<Box __css={styles} bgColor={variant === 'unstyled' ? undefined : headerBgColor} {...rest}>
{icon ? (
<Box mr={2} color={headerStyle ? iconColor : undefined} bgColor="unset">
{icon}
</Box>
) : null}
{children}
</Box>
);

View File

@@ -1,22 +1,7 @@
import React from 'react';
import {
BackgroundProps,
Box,
EffectProps,
InteractivityProps,
LayoutProps,
PositionProps,
SpaceProps,
useStyleConfig,
} from '@chakra-ui/react';
import { BackgroundProps, Box, InteractivityProps, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
export interface CardProps
extends LayoutProps,
SpaceProps,
BackgroundProps,
InteractivityProps,
PositionProps,
EffectProps {
export interface CardProps extends LayoutProps, SpaceProps, BackgroundProps, InteractivityProps {
variant?: string;
onClick?: () => void;
className?: string;

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { Box, Heading, IconButton, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
import { ArrowsOut, Info } from 'phosphor-react';
import { ArrowsOut, Info } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { Card } from '../Card';
import { CardBody } from '../Card/CardBody';
@@ -18,7 +18,7 @@ const GraphStatDisplay = ({ chart, title, explanation }: Props) => {
return (
<>
<Card variant="widget" w="100%">
<Card>
<CardHeader>
<Heading mr={2} my="auto" size="md">
{title}

View File

@@ -1,17 +1,26 @@
import React from 'react';
import { Flex, LayoutProps, ModalHeader as Header, SpaceProps, Spacer } from '@chakra-ui/react';
import { HStack, ModalHeader as Header, Spacer, useColorModeValue } from '@chakra-ui/react';
export interface ModalHeaderProps extends LayoutProps, SpaceProps {
export interface ModalHeaderProps {
title: string;
right?: React.ReactNode;
left?: React.ReactNode;
right: React.ReactNode;
}
export const ModalHeader = ({ title, right }: ModalHeaderProps) => (
<Header>
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
const bg = useColorModeValue('blue.50', 'blue.700');
return (
<Header bg={bg}>
{title}
{left ? (
<HStack spacing={2} ml={2}>
{left}
</HStack>
) : null}
<Spacer />
{right}
</Flex>
</Header>
);
</Header>
);
};
export const ModalHeader = React.memo(_ModalHeader);

View File

@@ -0,0 +1,34 @@
import * as React from 'react';
import { As, Icon, Tag, TagLabel, TagLeftIcon, TagProps, Tooltip, useBreakpoint } from '@chakra-ui/react';
export interface ResponsiveTagProps extends TagProps {
label: string;
icon: As<any>;
tooltip?: string;
isCompact?: boolean;
}
export const ResponsiveTag = React.memo(({ label, icon, tooltip, isCompact, ...props }: ResponsiveTagProps) => {
const breakpoint = useBreakpoint();
const isCompactVersion = isCompact || breakpoint === 'base' || breakpoint === 'sm';
if (isCompactVersion) {
return (
<Tooltip label={tooltip ?? label}>
<Tag size="lg" colorScheme="blue" {...props}>
<Icon as={icon} boxSize="18px" />
</Tag>
</Tooltip>
);
}
return (
<Tooltip label={tooltip ?? label}>
<Tag size="lg" colorScheme="blue" {...props}>
<TagLeftIcon boxSize="18px" as={icon} mt={-0.5} />
<TagLabel>{label}</TagLabel>
</Tag>
</Tooltip>
);
});

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { As, Flex, Heading, Icon, Spacer, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { Info } from 'phosphor-react';
import { Info } from '@phosphor-icons/react';
import { Card } from '../Card';
type Props = {
@@ -15,19 +15,19 @@ const SimpleIconStatDisplay = ({ title, description, icon, value, color }: Props
const bgColor = useColorModeValue(color[0], color[1]);
return (
<Card variant="widget" w="100%" p={3}>
<Flex h="70px" w="100%">
<Card p={3} bgColor={bgColor}>
<Flex h="70px" w="100%" color="white">
<Flex direction="column" justifyContent="center">
<Heading size="lg">{value}</Heading>
<Heading size="sm" display="flex">
<Text opacity={0.8}>{title}</Text>
<Text>{title}</Text>
<Tooltip label={description} hasArrow>
<Info style={{ marginLeft: '4px', marginTop: '2px' }} />
</Tooltip>
</Heading>
</Flex>
<Spacer />
<Icon borderRadius="15px" my="auto" as={icon} boxSize="70px" bgColor={bgColor} color="white" />
<Icon borderRadius="15px" my="auto" as={icon} boxSize="70px" color="white" />
</Flex>
</Card>
);

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { Button, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, useBreakpoint } from '@chakra-ui/react';
import { FunnelSimple } from 'phosphor-react';
import { FunnelSimple } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { useAuth } from 'contexts/AuthProvider';

View File

@@ -0,0 +1,64 @@
import * as React from 'react';
import { Td, Tr } from '@chakra-ui/react';
import { Row, flexRender } from '@tanstack/react-table';
export type DataGridCellRowProps<TValue extends object> = {
row: Row<TValue>;
onRowClick: ((row: TValue) => (() => void) | undefined) | undefined;
rowStyle: {
hoveredRowBg: string;
};
};
export const DataGridCellRow = <TValue extends object>({
row,
rowStyle: { hoveredRowBg },
onRowClick,
}: DataGridCellRowProps<TValue>) => {
const onClick = onRowClick ? onRowClick(row.original) : undefined;
return (
<Tr
key={row.id}
_hover={{
backgroundColor: hoveredRowBg,
}}
onClick={onClick}
>
{row.getVisibleCells().map((cell) => (
<Td
px={1}
key={cell.id}
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
minWidth={cell.column.columnDef.meta?.customMinWidth ?? undefined}
maxWidth={cell.column.columnDef.meta?.customMaxWidth ?? undefined}
width={cell.column.columnDef.meta?.customWidth}
textAlign={cell.column.columnDef.meta?.isCentered ? 'center' : undefined}
fontFamily={
cell.column.columnDef.meta?.isMonospace
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
: undefined
}
onClick={
cell.column.columnDef.meta?.stopPropagation || (cell.column.id === 'actions' && onClick)
? (e) => {
e.stopPropagation();
}
: undefined
}
cursor={
!cell.column.columnDef.meta?.stopPropagation && cell.column.id !== 'actions' && onClick
? 'pointer'
: undefined
}
border="0.5px solid gray"
style={cell.column.columnDef.meta?.rowContentOptions?.style}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Td>
))}
</Tr>
);
};

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Box, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip } from '@chakra-ui/react';
import { FunnelSimple } from '@phosphor-icons/react';
import { VisibilityState } from '@tanstack/react-table';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { DataGridColumn } from './useDataGrid';
export type DataGridColumnPickerProps<TValue extends object> = {
columns: DataGridColumn<TValue>[];
columnVisibility: VisibilityState;
toggleVisibility: (id: string) => void;
};
export const DataGridColumnPicker = <TValue extends object>({
columns,
columnVisibility,
toggleVisibility,
}: DataGridColumnPickerProps<TValue>) => {
const { t } = useTranslation();
return (
<Box>
<Menu closeOnSelect={false} isLazy>
<Tooltip label={t('common.columns')} hasArrow>
<MenuButton as={IconButton} icon={<FunnelSimple />} />
</Tooltip>
<MenuList maxH="200px" overflowY="auto">
{columns
.filter((col) => col.id && col.header)
.map((column) => {
const handleClick =
column.id !== undefined ? () => toggleVisibility(column.id as unknown as string) : undefined;
const id = column.id ?? uuid();
let label = column.header?.toString() ?? 'Unrecognized column';
if (column.meta?.columnSelectorOptions?.label) label = column.meta.columnSelectorOptions.label;
return (
<MenuItem
key={id}
as={Checkbox}
isChecked={columnVisibility[id] === undefined || columnVisibility[id]}
onChange={column.meta?.alwaysShow ? undefined : handleClick}
isDisabled={column.meta?.alwaysShow}
>
{label}
</MenuItem>
);
})}
</MenuList>
</Menu>
</Box>
);
};

View File

@@ -0,0 +1,45 @@
import * as React from 'react';
import { Box, Flex, Th, Tooltip, Tr } from '@chakra-ui/react';
import { HeaderGroup, flexRender } from '@tanstack/react-table';
import { DataGridSortIcon } from './SortIcon';
export type DataGridHeaderRowProps<TValue extends object> = {
headerGroup: HeaderGroup<TValue>;
};
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
<Tr p={0}>
{headerGroup.headers.map((header) => (
<Th
color="gray.400"
key={header.id}
colSpan={header.colSpan}
minWidth={header.column.columnDef.meta?.customMinWidth ?? undefined}
maxWidth={header.column.columnDef.meta?.customMaxWidth ?? undefined}
width={header.column.columnDef.meta?.customWidth}
fontSize="sm"
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}
cursor={header.column.getCanSort() ? 'pointer' : undefined}
border="0.5px solid gray"
px={1}
>
<Flex display="flex" alignItems="center">
{header.isPlaceholder ? null : (
<Tooltip label={header.column.columnDef.meta?.headerOptions?.tooltip}>
<Box
overflow="hidden"
whiteSpace="nowrap"
alignContent="center"
width="100%"
{...header.column.columnDef.meta?.headerStyleProps}
>
{flexRender(header.column.columnDef.header, header.getContext())}
</Box>
</Tooltip>
)}
<DataGridSortIcon sortInfo={header.column.getIsSorted()} canSort={header.column.getCanSort()} />
</Flex>
</Th>
))}
</Tr>
);

View File

@@ -0,0 +1,124 @@
import * as React from 'react';
import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
import {
Tooltip,
Flex,
IconButton,
Text,
Select,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
} from '@chakra-ui/react';
import { Table } from '@tanstack/react-table';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { useContainerDimensions } from 'hooks/useContainerDimensions';
type Props<T extends object> = {
table: Table<T>;
isDisabled?: boolean;
};
const DataGridControls = <T extends object>({ table, isDisabled }: Props<T>) => {
const { t } = useTranslation();
const { ref, dimensions } = useContainerDimensions({ precision: 100 });
const isCompact = dimensions.width !== 0 && dimensions.width <= 800;
return (
<Flex ref={ref} justifyContent="space-between" m={4} alignItems="center">
<Flex>
<Tooltip label={t('table.first_page')}>
<IconButton
aria-label="Go to first page"
onClick={() => table.setPageIndex(0)}
isDisabled={isDisabled || !table.getCanPreviousPage()}
icon={<ArrowLeftIcon h={3} w={3} />}
mr={4}
/>
</Tooltip>
<Tooltip label={t('table.previous_page')}>
<IconButton
aria-label="Previous page"
onClick={() => table.previousPage()}
isDisabled={isDisabled || !table.getCanPreviousPage()}
icon={<ChevronLeftIcon h={6} w={6} />}
/>
</Tooltip>
</Flex>
<Flex alignItems="center">
{isCompact ? null : (
<>
<Text flexShrink={0} mr={8}>
{t('table.page')}{' '}
<Text fontWeight="bold" as="span">
{table.getState().pagination.pageIndex + 1}
</Text>{' '}
{t('common.of')}{' '}
<Text fontWeight="bold" as="span">
{table.getPageCount()}
</Text>
</Text>
<Text flexShrink={0}>{t('table.go_to_page')}</Text>{' '}
<NumberInput
ml={2}
mr={8}
w={28}
min={1}
max={table.getPageCount()}
onChange={(_, numberValue) => {
const newPage = numberValue ? numberValue - 1 : 0;
table.setPageIndex(newPage);
}}
value={table.getState().pagination.pageIndex + 1}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</>
)}
<Select
w={32}
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((opt) => (
<option key={uuid()} value={opt}>
{t('common.show')} {opt}
</option>
))}
</Select>
</Flex>
<Flex>
<Tooltip label={t('table.next_page')}>
<IconButton
aria-label="Go to next page"
onClick={() => table.nextPage()}
isDisabled={isDisabled || !table.getCanNextPage()}
icon={<ChevronRightIcon h={6} w={6} />}
/>
</Tooltip>
<Tooltip label={t('table.last_page')}>
<IconButton
aria-label="Go to last page"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
isDisabled={isDisabled || !table.getCanNextPage()}
icon={<ArrowRightIcon h={3} w={3} />}
ml={4}
/>
</Tooltip>
</Flex>
</Flex>
);
};
export default DataGridControls;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Icon } from '@chakra-ui/react';
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
import { SortDirection } from '@tanstack/react-table';
export type DataGridSortIconProps = {
sortInfo: false | SortDirection;
canSort: boolean;
};
export const DataGridSortIcon = ({ sortInfo, canSort }: DataGridSortIconProps) => {
if (canSort) {
if (sortInfo) {
return sortInfo === 'desc' ? (
<Icon ml={1} boxSize={3} as={ArrowDown} />
) : (
<Icon ml={1} boxSize={3} as={ArrowUp} />
);
}
return <Icon ml={1} boxSize={3} as={Circle} />;
}
return null;
};

View File

@@ -0,0 +1,54 @@
import * as React from 'react';
import { Box, Icon, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { Draggable } from '@hello-pangea/dnd';
import { ArrowsDownUp, Lock } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { DataGridColumn } from '../../useDataGrid';
type Props<TValue> = {
draggableId: string;
index: number;
column: DataGridColumn<TValue>;
};
const DraggableColumn = <TValue extends object>({ draggableId, index, column }: Props<TValue>) => {
const { t } = useTranslation();
const isDraggingBackground = useColorModeValue('blue.100', 'blue.600');
const notDraggingBackground = useColorModeValue('gray.50', 'gray.700');
let label = column.header?.toString() ?? 'Unrecognized column';
if (column.meta?.columnSelectorOptions?.label) label = column.meta.columnSelectorOptions.label;
const tooltipLabel = () => {
if (column.meta?.anchored) return t('table.drag_locked');
if (column.meta?.alwaysShow) return t('table.drag_always_show');
return t('table.drag_explanation');
};
return (
<Draggable draggableId={draggableId} index={index} isDragDisabled={column.meta?.anchored}>
{(itemProvided, itemSnapshot) => (
<Tooltip label={tooltipLabel()}>
<Box
ref={itemProvided.innerRef}
{...itemProvided.draggableProps}
{...itemProvided.dragHandleProps}
display="flex"
backgroundColor={itemSnapshot.isDragging ? isDraggingBackground : notDraggingBackground}
px={6}
py={2}
my={2}
borderRadius={15}
cursor={column.meta?.anchored ? 'not-allowed' : undefined}
>
<Icon as={column.meta?.anchored ? Lock : ArrowsDownUp} boxSize={5} ml={0.5} mr={2} my="auto" />
<Text my="auto">{label}</Text>
</Box>
</Tooltip>
)}
</Draggable>
);
};
export default DraggableColumn;

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import { Box, useColorModeValue } from '@chakra-ui/react';
import { Droppable } from '@hello-pangea/dnd';
import { DataGridColumn } from '../../useDataGrid';
import DraggableColumn from './DraggableColumn';
type Props<TValue> = {
items: string[];
columns: DataGridColumn<TValue>[];
droppableId: string;
isDropDisabled?: boolean;
};
const DroppableBox = <TValue extends object>({ items, columns, droppableId, isDropDisabled }: Props<TValue>) => {
const notDraggingBackground = useColorModeValue('gray.200', 'gray.600');
const isDraggingOverBackground = useColorModeValue('blue.300', 'blue.500');
return (
<Droppable droppableId={droppableId} direction="vertical" isCombineEnabled={false} isDropDisabled={isDropDisabled}>
{(provided, snapshot) => (
<Box
ref={provided.innerRef}
backgroundColor={snapshot.isDraggingOver ? isDraggingOverBackground : notDraggingBackground}
padding={2}
borderRadius={15}
>
{items.map((item, index) => {
const found = columns.find((col) => col.id === item);
return found ? <DraggableColumn key={item} draggableId={item} index={index} column={found} /> : null;
})}
{provided.placeholder}
</Box>
)}
</Droppable>
);
};
export default React.memo(DroppableBox);

View File

@@ -0,0 +1,129 @@
import * as React from 'react';
import { Box, Flex, Heading } from '@chakra-ui/react';
import { DragDropContext, DragStart, DropResult } from '@hello-pangea/dnd';
import { useTranslation } from 'react-i18next';
import { DataGridColumn, UseDataGridReturn } from '../../useDataGrid';
import DroppableBox from './DroppableBox';
const reorder = (list: string[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
if (removed) {
result.splice(endIndex, 0, removed);
}
return result;
};
const getShownColumns = <TValue extends object>(columns: DataGridColumn<TValue>[], columnOrder: string[]) => {
const order = [...columnOrder];
for (const col of columns) {
if (!order.includes(col.id)) {
order.push(col.id);
}
}
return order;
};
type Props<TValue extends object> = {
controller: UseDataGridReturn;
shownColumns: DataGridColumn<TValue>[];
hiddenColumns: DataGridColumn<TValue>[];
};
const TableDragDrop = <TValue extends object>({ controller, shownColumns, hiddenColumns }: Props<TValue>) => {
const { t } = useTranslation();
const [shownOrder, setShowOrder] = React.useState(getShownColumns(shownColumns, controller.columnOrder));
const [hiddenOrder, setHiddenOrder] = React.useState(hiddenColumns.map((col) => col.id));
const [currentDraggingColumn, setCurrentDraggingColumn] = React.useState<DataGridColumn<TValue>>();
const handleDragStart = React.useCallback(
(start: DragStart) => {
const foundColumn =
shownColumns.find(({ id }) => id === start.draggableId) ??
hiddenColumns.find(({ id }) => id === start.draggableId);
setCurrentDraggingColumn(foundColumn);
},
[shownColumns, hiddenColumns],
);
const minimumIndex = React.useMemo(() => {
let index = 0;
for (const [i, col] of shownColumns.entries()) {
if (col.meta?.anchored) {
index = i;
}
}
return index + 1;
}, [shownColumns]);
const handleDragEnd = React.useCallback(
(result: DropResult) => {
const { source, destination, draggableId } = result;
if (destination === null) return;
if (source.droppableId === destination.droppableId) {
const newOrder = reorder(shownOrder, source.index, Math.max(destination.index, minimumIndex));
if (destination.droppableId === 'displayed-columns') {
controller.setColumnOrder(newOrder);
setShowOrder(newOrder);
} else setHiddenOrder(newOrder);
}
// This means we are moving from displayed to hidden
else if (source.droppableId === 'displayed-columns') {
// Toggle the column visibility in user preferences
const results = controller.hideColumn(draggableId);
if (results) {
setHiddenOrder([...results.hiddenColumns]);
setShowOrder([...results.columnOrder]);
}
}
// This means we are moving from hidden to displayed
else if (source.droppableId === 'hidden-columns') {
const newOrder = Array.from(shownOrder);
newOrder.splice(Math.max(destination.index, minimumIndex), 0, draggableId);
const results = controller.unhideColumn(draggableId, newOrder);
if (results) {
setHiddenOrder(results.hiddenColumns);
setShowOrder([...results.columnOrder]);
setHiddenOrder([...results.hiddenColumns]);
}
}
setCurrentDraggingColumn(undefined);
},
[shownColumns, hiddenColumns, controller.hideColumn, controller.unhideColumn, minimumIndex],
);
return (
<>
<Heading size="md">{t('table.columns')}</Heading>
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Flex mt={4}>
<Box w="50%" mr={2}>
<Heading size="sm" mb={4}>
Visible ({shownOrder.length})
</Heading>
<DroppableBox droppableId="displayed-columns" items={shownOrder} columns={shownColumns} />
</Box>
<Box ml={2} w="50%">
<Heading size="sm" mb={4}>
Hidden ({hiddenColumns.length})
</Heading>
<DroppableBox
droppableId="hidden-columns"
items={hiddenOrder}
columns={hiddenColumns}
isDropDisabled={currentDraggingColumn?.meta?.alwaysShow}
/>
</Box>
</Flex>
</DragDropContext>
</>
);
};
export default TableDragDrop;

View File

@@ -0,0 +1,57 @@
import * as React from 'react';
import { SettingsIcon } from '@chakra-ui/icons';
import { Box, IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
import { ClockCounterClockwise } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { Modal } from '../../../Modals/Modal';
import { DataGridColumn, UseDataGridReturn } from '../useDataGrid';
import TableDragDrop from './DragDrop';
type Props<TValue extends object> = {
controller: UseDataGridReturn;
columns: DataGridColumn<TValue>[];
};
const TableSettingsModal = <TValue extends object>({ controller, columns }: Props<TValue>) => {
const { t } = useTranslation();
const modalProps = useDisclosure();
return (
<>
<Tooltip label={t('table.preferences')}>
<IconButton
aria-label={t('table.preferences')}
icon={<SettingsIcon weight="bold" />}
onClick={modalProps.onOpen}
/>
</Tooltip>
<Modal
title={t('table.preferences')}
topRightButtons={
<Tooltip label={t('table.reset')}>
<IconButton
aria-label={t('table.reset')}
icon={<ClockCounterClockwise size={20} />}
onClick={controller.resetPreferences}
/>
</Tooltip>
}
options={{
modalSize: 'md',
maxWidth: { sm: '600px', md: '600px', lg: '600px', xl: '600px' },
}}
{...modalProps}
>
<Box w="100%">
<TableDragDrop<TValue>
shownColumns={columns.filter((col) => controller.columnVisibility[col.id] !== false)}
hiddenColumns={columns.filter((col) => controller.columnVisibility[col.id] === false)}
controller={controller}
/>
</Box>
</Modal>
</>
);
};
export default React.memo(TableSettingsModal);

View File

@@ -0,0 +1,273 @@
import React from 'react';
import {
Box,
Center,
Flex,
HStack,
Heading,
LayoutProps,
Spacer,
Spinner,
Table,
TableContainer,
Tbody,
Thead,
useColorModeValue,
} from '@chakra-ui/react';
import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import { useTranslation } from 'react-i18next';
import { RefreshButton } from '../../Buttons/RefreshButton';
import { DataGridCellRow } from './CellRow';
import { DataGridHeaderRow } from './HeaderRow';
import DataGridControls from './Input';
import TableSettingsModal from './TableSettingsModal';
import { DataGridColumn, UseDataGridReturn } from './useDataGrid';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { LoadingOverlay } from 'components/LoadingOverlay';
export type ColumnOptions = {
isSortable?: boolean;
};
export type DataGridOptions<TValue extends object> = {
count?: number;
isFullScreen?: boolean;
isHidingControls?: boolean;
isManual?: boolean;
minimumHeight?: LayoutProps['minH'];
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 | React.ReactNode;
objectListed: string;
leftContent?: React.ReactNode;
addButton?: React.ReactNode;
otherButtons?: React.ReactNode;
};
data?: TValue[];
isLoading?: boolean;
options?: DataGridOptions<TValue>;
};
export const DataGrid = <TValue extends object>({
innerTableKey,
controller,
columns,
header,
data = [],
options = {},
isLoading = false,
}: DataGridProps<TValue>) => {
const { t } = useTranslation();
/*
Table Styling
*/
const textColor = useColorModeValue('gray.700', 'white');
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
const minimumHeight: LayoutProps['minH'] = React.useMemo(() => {
if (options.isFullScreen) {
return { base: 'calc(100vh - 360px)', md: 'calc(100vh - 288px)' };
}
return options.minimumHeight ?? '300px';
}, [options.isFullScreen, options.minimumHeight]);
/*
Table Options
*/
const onRowClick = React.useMemo(() => options.onRowClick, [options.onRowClick]);
const pagination = React.useMemo(
() => ({
pageIndex: controller.pageInfo.pageIndex,
pageSize: controller.pageInfo.pageSize,
}),
[controller.pageInfo.pageIndex, controller.pageInfo.pageSize],
);
const pageCount = React.useMemo(() => {
if (options.isManual && options.count) {
return Math.ceil(options.count / pagination.pageSize);
}
return Math.ceil((data?.length ?? 0) / pagination.pageSize);
}, [options.count, options.isManual, data?.length, pagination.pageSize]);
const tableOptions = React.useMemo(
() => ({
pageCount: pageCount > 0 ? pageCount : 1,
initialState: { sorting: controller.sortBy, pagination },
manualPagination: options.isManual,
manualSorting: options.isManual,
autoResetPageIndex: false,
}),
[options.isManual, controller.sortBy, pageCount],
);
const orderedColumns = React.useMemo(() => {
const order = controller.columnOrder.filter((id) => columns.find((col) => col.id === id));
if (order.length !== columns.length) {
for (const col of columns) {
if (!order.includes(col.id)) {
order.push(col.id);
}
}
}
return columns.slice().sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
}, [columns, controller.columnOrder]);
const table = useReactTable<TValue>({
// react-table base functions
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
// Table State
data,
columns: orderedColumns,
state: {
sorting: controller.sortBy,
columnVisibility: controller.columnVisibility,
pagination,
},
// Change Handlers
onSortingChange: controller.setSortBy,
onPaginationChange: controller.onPaginationChange,
// debugTable: true,
// Table Options
...tableOptions,
});
// 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" />
</Center>
);
}
return options.showAsCard ? (
<Card>
<CardHeader>
{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" key={innerTableKey}>
<Thead>
{table.getHeaderGroups().map((headerGroup) => (
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
))}
</Thead>
<Tbody>
{table.getRowModel().rows.map((row) => (
<DataGridCellRow<TValue> key={row.id} row={row} onRowClick={onRowClick} rowStyle={{ hoveredRowBg }} />
))}
</Tbody>
</Table>
{data?.length === 0 ? (
<Center mt={8}>
<Heading size="md">
{header.objectListed
? t('common.no_obj_found', { obj: header.objectListed })
: t('common.empty_list')}
</Heading>
</Center>
) : null}
</TableContainer>
</LoadingOverlay>
{!options.isHidingControls ? <DataGridControls table={table} isDisabled={isLoading} /> : null}
</CardBody>
</Card>
) : (
<Box w="100%">
<Flex mb={2} hidden={options.hideTableTitleRow}>
<Heading size="md" my="auto" mr={2}>
{header.title}
</Heading>
{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>
</Flex>
<LoadingOverlay isLoading={isLoading}>
<TableContainer minH={minimumHeight}>
<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} />
))}
</Thead>
<Tbody>
{table.getRowModel().rows.map((row) => (
<DataGridCellRow<TValue> key={row.id} row={row} onRowClick={onRowClick} rowStyle={{ hoveredRowBg }} />
))}
</Tbody>
</Table>
{data?.length === 0 ? (
<Center mt={8}>
<Heading size="md">
{header.objectListed ? t('common.no_obj_found', { obj: header.objectListed }) : t('common.empty_list')}
</Heading>
</Center>
) : null}
</TableContainer>
</LoadingOverlay>
{!options.isHidingControls ? <DataGridControls table={table} isDisabled={isLoading} /> : null}
</Box>
);
};

View File

@@ -0,0 +1,198 @@
import * as React from 'react';
import { ColumnDef, PaginationState, SortingColumnDef, SortingState, VisibilityState } from '@tanstack/react-table';
import { useAuth } from 'contexts/AuthProvider';
const getDefaultSettings = ({ settings, showAllRows }: { settings?: string; showAllRows?: boolean }) => {
if (showAllRows) return { pageSize: 1000, pageIndex: 0 };
let limit = 10;
let index = 0;
if (settings) {
const savedSizeSetting = localStorage.getItem(settings);
if (savedSizeSetting) {
try {
limit = parseInt(savedSizeSetting, 10);
} catch (e) {
limit = 10;
}
}
const savedPageSetting = localStorage.getItem(`${settings}.page`);
if (savedPageSetting) {
try {
index = parseInt(savedPageSetting, 10);
} catch (e) {
index = 0;
}
}
}
return {
pageSize: limit,
pageIndex: index,
};
};
const getSavedColumnOrder = (defaultValue: string[], settings?: string) => {
if (settings) {
const savedOrderSetting = localStorage.getItem(`${settings}.order`);
if (savedOrderSetting) {
try {
const savedOrder = JSON.parse(savedOrderSetting);
return savedOrder.length > 0 ? savedOrder : defaultValue;
} catch (e) {
return defaultValue;
}
}
}
return defaultValue;
};
export type DataGridColumn<T> = ColumnDef<T> & SortingColumnDef<T> & { id: string };
export type UseDataGridProps = {
tableSettingsId: string;
defaultOrder: string[];
defaultSortBy?: SortingState;
showAllRows?: boolean;
};
export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder, showAllRows }: UseDataGridProps) => {
const orderSetting = `${tableSettingsId}.order`;
const hiddenColumnSetting = `${tableSettingsId}.hiddenColumns`;
const pageSetting = `${tableSettingsId}.page`;
const { getPref, setPref, setPrefs, deletePref } = useAuth();
const [sortBy, setSortBy] = React.useState<SortingState>(defaultSortBy ?? []);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnOrder, setColumnOrder] = React.useState<string[]>(
getSavedColumnOrder(defaultOrder ?? [], tableSettingsId),
);
const [pageInfo, setPageInfo] = React.useState<PaginationState>(
getDefaultSettings({ settings: tableSettingsId, showAllRows }),
);
const setNewColumnOrder = React.useCallback(
(newOrder: string[]) => {
setColumnOrder(newOrder);
if (tableSettingsId) {
localStorage.setItem(orderSetting, JSON.stringify(newOrder));
setPref({ preference: orderSetting, value: newOrder.join(',') });
}
},
[setPref],
);
const resetPreferences = React.useCallback(async () => {
if (tableSettingsId) {
localStorage.removeItem(orderSetting);
localStorage.removeItem(hiddenColumnSetting);
await deletePref([orderSetting, hiddenColumnSetting]);
}
setColumnOrder(defaultOrder ?? []);
setColumnVisibility({});
}, [deletePref]);
const hideColumn = React.useCallback(
(id: string) => {
const newVisibility = { ...columnVisibility };
newVisibility[id] = false;
let hiddenColumnsArray = Object.entries(newVisibility)
.filter(([, value]) => !value)
.map(([key]) => key);
hiddenColumnsArray = [...new Set(hiddenColumnsArray)]; // Remove duplicates
// New column order without hidden columns
let filteredColumnOrder = columnOrder.filter((columnId) => !hiddenColumnsArray.includes(columnId));
filteredColumnOrder = [...new Set(filteredColumnOrder)]; // Remove duplicates
setPrefs([
{ tag: hiddenColumnSetting, value: hiddenColumnsArray.join(',') },
{ tag: orderSetting, value: filteredColumnOrder.join(',') },
]);
setColumnVisibility({ ...newVisibility });
setColumnOrder(filteredColumnOrder);
localStorage.setItem(orderSetting, JSON.stringify(filteredColumnOrder));
localStorage.setItem(hiddenColumnSetting, hiddenColumnsArray.join(','));
return {
hiddenColumns: hiddenColumnsArray,
columnOrder: filteredColumnOrder,
};
},
[columnOrder, columnVisibility, setPrefs],
);
const unhideColumn = React.useCallback(
(id: string, newOrder: string[]) => {
const newVisibility = { ...columnVisibility };
newVisibility[id] = true;
let hiddenColumnsArray = Object.entries(newVisibility)
.filter(([, value]) => !value)
.map(([key]) => key);
hiddenColumnsArray = [...new Set(hiddenColumnsArray)]; // Remove duplicates
const newColumnOrder = [...new Set(newOrder)]; // Remove duplicates
setPrefs([
{ tag: hiddenColumnSetting, value: hiddenColumnsArray.join(',') },
{ tag: orderSetting, value: newColumnOrder.join(',') },
]);
setColumnVisibility({ ...newVisibility });
setColumnOrder(newColumnOrder);
localStorage.setItem(orderSetting, JSON.stringify(newColumnOrder));
localStorage.setItem(hiddenColumnSetting, hiddenColumnsArray.join(','));
return {
hiddenColumns: hiddenColumnsArray,
columnOrder: newColumnOrder,
};
},
[columnOrder, columnVisibility, setPrefs],
);
React.useEffect(() => {
const savedPrefs = getPref(hiddenColumnSetting);
if (savedPrefs) {
const savedHiddenColumns = savedPrefs.split(',');
setColumnVisibility(savedHiddenColumns.reduce((acc, curr) => ({ ...acc, [curr]: false }), {}));
} else {
setColumnVisibility({});
}
const savedOrderSetting = getPref(orderSetting);
if (savedOrderSetting) {
const savedHiddenColumns = savedOrderSetting.split(',');
setColumnOrder(savedHiddenColumns);
}
}, [tableSettingsId]);
React.useEffect(() => {
if (tableSettingsId) {
localStorage.setItem(pageSetting, String(pageInfo.pageIndex));
if (tableSettingsId) localStorage.setItem(`${tableSettingsId}`, String(pageInfo.pageSize));
}
}, [pageInfo.pageIndex, pageInfo.pageSize]);
return React.useMemo(
() => ({
tableSettingsId,
pageInfo,
sortBy,
setSortBy,
columnOrder,
setColumnOrder: setNewColumnOrder,
hideColumn,
unhideColumn,
columnVisibility,
setColumnVisibility,
onPaginationChange: setPageInfo,
resetPreferences,
}),
[pageInfo, hideColumn, unhideColumn, columnVisibility, sortBy, columnOrder, setNewColumnOrder],
);
};
export type UseDataGridReturn = ReturnType<typeof useDataGrid>;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Icon } from '@chakra-ui/react';
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
interface Props {
isSorted: boolean;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Icon } from '@chakra-ui/react';
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
interface Props {
isSorted: boolean;

View File

@@ -1,79 +0,0 @@
import * as React from 'react';
import { Heading } from '@chakra-ui/react';
import { Select } from 'chakra-react-select';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useAuth } from 'contexts/AuthProvider';
import { useControllerDeviceSearch } from 'contexts/ControllerSocketProvider/hooks/Commands/useDeviceSearch';
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
const DeviceSearchBar = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { token } = useAuth();
const { startWebSocket, isWebSocketOpen } = useControllerStore((state) => ({
startWebSocket: state.startWebSocket,
isWebSocketOpen: state.isWebSocketOpen,
}));
const { inputValue, results, onInputChange } = useControllerDeviceSearch({
minLength: 2,
});
const NoOptionsMessage = React.useCallback(
() => (
<Heading size="sm" textAlign="center">
{isWebSocketOpen ? t('common.no_devices_found') : `${t('controller.devices.connecting')}...`}
</Heading>
),
[t, isWebSocketOpen],
);
const onClick = React.useCallback((v: { value: string }) => {
navigate(`/devices/${v.value}`);
}, []);
const onChange = React.useCallback((v: string) => {
if ((v.length === 0 || v.match('^[a-fA-F0-9-*]+$')) && v.length <= 13) onInputChange(v);
}, []);
const onFocus = () => {
if (!isWebSocketOpen && token && token.length > 0) {
startWebSocket(token, 0);
}
};
return (
<Select
chakraStyles={{
control: (provided) => ({
...provided,
borderRadius: '15px',
color: 'unset',
}),
input: (provided) => ({
...provided,
width: '140px',
}),
dropdownIndicator: (provided) => ({
...provided,
backgroundColor: 'unset',
border: 'unset',
}),
menu: (provided) => ({
...provided,
color: 'black',
}),
}}
components={{ NoOptionsMessage }}
// @ts-ignore
options={results.map((v: string) => ({ label: v, value: v }))}
filterOption={() => true}
inputValue={inputValue}
value={inputValue}
placeholder={t('common.search')}
onInputChange={onChange}
onFocus={onFocus}
// @ts-ignore
onChange={onClick}
/>
);
};
export default DeviceSearchBar;

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import { AddIcon } from '@chakra-ui/icons';
import { IconButton, Input, InputGroup, InputRightElement, Tooltip } from '@chakra-ui/react';
import { Trash } from 'phosphor-react';
import { Trash } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { DataTable } from '../../../DataTables/DataTable';

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

@@ -0,0 +1,196 @@
import * as React from 'react';
import { Tooltip, useColorMode, useColorModeValue } from '@chakra-ui/react';
import {
AsyncSelect,
ChakraStylesConfig,
GroupBase,
LoadingIndicatorProps,
OptionBase,
OptionsOrGroups,
chakraComponents,
} from 'chakra-react-select';
import { useNavigate } from 'react-router-dom';
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
import debounce from 'helpers/debounce';
import { getUsernameRadiusSessions } from 'hooks/Network/Radius';
const chakraStyles: (
colorMode: 'light' | 'dark',
) => ChakraStylesConfig<SearchOption, false, GroupBase<SearchOption>> = (colorMode) => ({
dropdownIndicator: (provided) => ({
...provided,
width: '32px',
}),
placeholder: (provided) => ({
...provided,
lineHeight: '1',
pointerEvents: 'none',
userSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
msUserSelect: 'none',
}),
container: (provided) => ({
...provided,
width: '320px',
backgroundColor: colorMode === 'light' ? 'white' : 'gray.600',
borderRadius: '15px',
}),
input: (provided) => ({
...provided,
gridArea: '1 / 2 / 4 / 4 !important',
}),
});
interface SearchOption extends OptionBase {
label: string;
value: string;
type: 'serial' | 'radius-username' | 'radius-mac';
}
const asyncComponents = {
LoadingIndicator: (props: LoadingIndicatorProps<SearchOption, false, GroupBase<SearchOption>>) => {
const { color, emptyColor } = useColorModeValue(
{
color: 'blue.500',
emptyColor: 'blue.100',
},
{
color: 'blue.300',
emptyColor: 'blue.900',
},
);
return (
<chakraComponents.LoadingIndicator
color={color}
emptyColor={emptyColor}
speed="750ms"
spinnerSize="md"
thickness="3px"
{...props}
/>
);
},
};
const GlobalSearchBar = () => {
const { colorMode } = useColorMode();
const navigate = useNavigate();
const store = useControllerStore((state) => ({
searchSerialNumber: state.searchSerialNumber,
}));
const onNewSearch = React.useCallback(
async (v: string, callback: (options: OptionsOrGroups<SearchOption, GroupBase<SearchOption>>) => void) => {
if (v.length < 3) return callback([]);
if (v.includes('rad:')) {
const trimmed = v.replace('rad:', '').trim();
if (trimmed.length < 3) return callback([]);
const cleaned = trimmed.toLowerCase();
return getUsernameRadiusSessions(cleaned)
.then((res) =>
callback(
res
.map((r) => ({
label: r.serialNumber,
value: r.serialNumber,
type: 'radius-username',
}))
.filter(({ value }, i, a) => a.findIndex((t) => t.value === value) === i) as SearchOption[],
),
)
.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) => {
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',
}));
tryAgain = false;
})
.catch(() => {
result = [];
});
}
callback(result);
}
return callback([]);
},
[],
);
const debouncedNewSearch = React.useCallback(
debounce(
// @ts-ignore
({
v,
callback,
}: {
v: string;
callback: (options: OptionsOrGroups<SearchOption, GroupBase<SearchOption>>) => void;
}) => {
onNewSearch(v as string, callback);
},
300,
),
[],
);
const styles = React.useMemo(() => chakraStyles(colorMode), [colorMode]);
return (
<Tooltip
label={`Search serial numbers and radius clients. For radius clients you can either use the client's username (rad:client@client.com)
or use the client's station ID (rad:11:22:33:44:55:66)`}
shouldWrapChildren
placement="left"
>
<AsyncSelect<SearchOption, false, GroupBase<SearchOption>>
name="global_search"
chakraStyles={styles}
closeMenuOnSelect
placeholder="Search MACs or radius clients"
components={asyncComponents}
loadOptions={(inputValue, callback) => {
debouncedNewSearch({ v: inputValue, callback });
}}
value={null}
onChange={(newValue) => {
if (newValue) {
navigate(`/devices/${newValue.value}`);
}
}}
/>
</Tooltip>
);
};
export default GlobalSearchBar;

View File

@@ -1,18 +1,19 @@
import React from 'react';
import { Tooltip } from '@chakra-ui/react';
import { compactDate, formatDaysAgo } from 'helpers/dateFormatting';
import { compactDate, formatDaysAgo, formatDaysAgoCompact } from 'helpers/dateFormatting';
type Props = { date?: number; hidePrefix?: boolean };
type Props = { date?: number; hidePrefix?: boolean; isCompact?: boolean };
const getDaysAgo = ({ date, hidePrefix }: { date?: number; hidePrefix?: boolean }) => {
const getDaysAgo = ({ date, hidePrefix, isCompact }: { date?: number; hidePrefix?: boolean; isCompact?: boolean }) => {
if (!date || date === 0) return '-';
if (isCompact)
return hidePrefix ? formatDaysAgoCompact(date).split(' ').slice(1).join(' ') : formatDaysAgoCompact(date);
return hidePrefix ? formatDaysAgo(date).split(' ').slice(1).join(' ') : formatDaysAgo(date);
};
const FormattedDate = ({ date, hidePrefix }: Props) => (
const FormattedDate = ({ date, hidePrefix, isCompact }: Props) => (
<Tooltip hasArrow placement="top" label={compactDate(date ?? 0)}>
{getDaysAgo({ date, hidePrefix })}
{getDaysAgo({ date, hidePrefix, isCompact })}
</Tooltip>
);

View File

@@ -9,7 +9,7 @@ import {
PopoverHeader,
PopoverTrigger,
} from '@chakra-ui/react';
import { Question } from 'phosphor-react';
import { Question } from '@phosphor-icons/react';
export type InfoPopoverProps = {
title: string;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { InfoIcon } from '@chakra-ui/icons';
import { Heading, IconButton, LayoutProps, LightMode, SpaceProps, Spacer, Tooltip } from '@chakra-ui/react';
import { MagnifyingGlass } from 'phosphor-react';
import { MagnifyingGlass } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { Card } from 'components/Containers/Card';

View File

@@ -33,7 +33,14 @@ const LanguageSwitcher = () => {
return (
<Menu>
<Tooltip label={t('common.language')}>
<MenuButton background="transparent" as={IconButton} aria-label="Commands" icon={languageIcon} size="sm" />
<MenuButton
background="transparent"
variant="ghost"
as={IconButton}
aria-label="Commands"
icon={languageIcon}
size="sm"
/>
</Tooltip>
<MenuList>
<MenuItem onClick={changeLanguage('de')}>Deutsche</MenuItem>

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