Compare commits

...

130 Commits

Author SHA1 Message Date
pascal-fischer
87ff65f1a7 adding peers approval flag to non user bound devices (#310)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-25 13:01:54 +01:00
Bethuel Mmbaga
748596f710 Add JWT allow groups support in account settings (#309)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-18 12:14:03 +01:00
Misha Bragin
b06cb0ec3d Add a settings page for custom IdP and MFA (#308)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-12 13:42:14 +01:00
Misha Bragin
0c924f7ded Correct peer approval tooltip (#307) 2023-12-12 13:33:11 +01:00
Maycon Santos
f29c915fc2 fix settings update blank page
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-11 18:07:05 +01:00
Maycon Santos
5f8579bfda Fix peer expiration interval init on validation (#306)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-11 17:46:32 +01:00
pascal-fischer
a71389aa29 not sending extra settings on selfhosted deployments (#305)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-11 16:52:29 +01:00
Maycon Santos
c3ba026452 Init domains array for custom nameserver group (#304)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-12-08 11:07:10 +01:00
Maycon Santos
193f8a7bdf Fix missing text update from #302 (#303) 2023-12-07 18:05:44 +01:00
Fabio Fantoni
f9814e1169 add codespell github workflow and fix some typo (#296)
* add codespell github workflow

* fix some typo
2023-12-07 18:05:31 +01:00
Maycon Santos
2f800bf912 Add delete account support (#302)
* Add delete account support

* add peer approval form item

* block menu for non owners
2023-12-07 15:28:07 +01:00
pascal-fischer
0199ea81f3 Add support for peer approval (#299)
* add support for peer approval

* use gold color for tag

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-12-05 13:17:40 +01:00
Maycon Santos
a20894092b Add owner role support (#300)
Update checks for admin to include owner role

Updated role list to include role for users

Updated activity with new event type handler
2023-12-01 16:57:18 +01:00
Maycon Santos
f4d6ce5770 Fix Windows server version lowercase
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-11-24 18:29:43 +01:00
Maycon Santos
dd67ab6dcb Fix Windows server version (#298)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-11-24 17:42:31 +01:00
pascal-fischer
2613948cdf Fix OS display format for iOS (#297)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* fix OS format function to properly show iOS

* bring back windows check
2023-11-13 22:30:50 +08:00
pascal-fischer
dc95d8bfd1 disallow routing through ios devices (#295) 2023-11-11 13:20:39 +01:00
Bethuel Mmbaga
f49c28f550 Add integration activity events target to Activity view as system setting (#293) 2023-11-08 13:51:57 +03:00
Maycon Santos
2fc7b73ea0 Fix/blank page on empty policy groups (#292)
validate null sources and destinations fields
2023-11-01 11:56:12 +01:00
pascal-fischer
a9354d3c87 removed disabled option from setup key filter switch (#291) 2023-10-27 20:06:42 +02:00
Sarooj bukhari
e0ae7d068a add search domains enabled to namespace (#288)
Some checks failed
build and push / build_n_push (push) Has been cancelled
---------

Co-authored-by: braginini <bangvalo@gmail.com>
2023-10-20 09:33:44 +02:00
Sarooj bukhari
ddd812e9a0 Add a refresh button to the table views (#287)
Co-authored-by: braginini <bangvalo@gmail.com>
2023-10-15 19:04:05 +02:00
Sarooj bukhari
2d55d0736f Add referesh button to the peers table (#286)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Co-authored-by: braginini <bangvalo@gmail.com>
2023-10-12 18:38:59 +02:00
Sarooj bukhari
8febc26f1f fix update distribution groups bug (#284)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-10-04 20:07:02 +02:00
Sarooj bukhari
3f854b01a0 Add support to network routes with peer group (#275)
Some checks failed
build and push / build_n_push (push) Has been cancelled
support to routes with peer group
updated the view logic to support the new type
updated peer view as well
for now, we are supporting a single group, but that can be extended
2023-10-04 11:37:23 +02:00
Misha Bragin
303d51eff8 Display service username in the activity (#282) 2023-10-03 16:06:50 +02:00
braginini
21e69e642a Disable user deletion since the API isn't ready
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-29 16:05:26 +02:00
pascal-fischer
835bb37ab9 Update jwt group sync visibility and update description (#281)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-29 13:58:46 +02:00
dependabot[bot]
a944dc8ab0 Bump @adobe/css-tools from 4.0.1 to 4.3.1 (#265)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.0.1 to 4.3.1.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-29 00:39:19 +05:00
Sarooj bukhari
b2c51533fb remove invalid key check (#280) 2023-09-28 21:26:11 +02:00
pascal-fischer
fd24536926 add checks to hide last login for non netbird hosted deployments (#277) 2023-09-27 18:00:33 +02:00
Misha Bragin
8e8484cd45 Fix packages (#276)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-27 12:25:55 +02:00
Sarooj bukhari
6c87f53195 Update the /Activity view to use email addresses from the /api/events response (#273)
Some checks failed
build and push / build_n_push (push) Has been cancelled
uses the events api response information to map user name and email
2023-09-23 10:45:27 +02:00
pascal-fischer
9bbbff7dc0 Fix last login for regular entries in users overview (#264)
* add last login to user overview for regular entries without tag

* add backward compatability to properly show with older version of management

* Merge main and update delete user popup messages

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-09-23 10:40:22 +02:00
Sarooj bukhari
04a20fa31f add new fields on setting pag (#258) 2023-09-22 16:32:40 +02:00
Misha Bragin
3797db93f0 Hide network routes card for non-linux peers (#269) 2023-09-20 10:58:26 +02:00
Sarooj bukhari
2e81765e85 add delete user functionality (#272) 2023-09-19 15:13:31 +02:00
Yoann N
cb9f76c0fc typo in ac creation (#266) 2023-09-18 23:22:52 +02:00
Sarooj bukhari
54accb665c setup ui (#268)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* setup ui

* capital letter

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-09-07 20:26:34 +02:00
Maycon Santos
cfea3bd489 Add ephemeral peers views and subviews (#267)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-09-06 13:36:23 +02:00
Zoltan Papp
a44a1c5424 Feature/ephemeral peers (#263)
* Add ephemeral peer switch for SetupKeyNew component

* Add "sys" handling in activity view
2023-09-04 13:49:11 +02:00
Sarooj bukhari
c2c044421f Peer search bug (#262) 2023-08-25 12:25:58 +02:00
Yulia
e8d57c3445 Fix flaky Access control test (#261)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Fixed flaky test by changing the way to access 
Access Control Page and add some test ids of Add Rules buttons.

Temporary removed tests for Firefox and webkit(safari)
2023-08-22 11:38:11 +02:00
pascal-fischer
0b892c0056 Add User last login and new events
Add last login to users overview list.
Additionally added new events for dashboard login, peer expiration and peer login.
2023-08-18 19:23:56 +02:00
Misha Bragin
14d9b80029 Limit UI view for the "user" role (#259)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-08-15 22:36:21 +02:00
B. Baumgartl
dedbe55308 Add windows/macos/android instructions for self-hosted (#254)
* Add windows/macos instructions for self-hosted

* Add android instructions for self-hosted
2023-08-14 18:54:44 +03:00
Yulia
796a06cf27 Add end-to-end tests using playwright (#257)
Add tests with playwright for:

- add peer modal on first access
- add peer modal on empty peer list
- test install buttons and instructions for Linux, 
Docker, macOS, Windows and Android
- check default ACL

The tests are using a modified version of the getting 
started scripts to run a local environment of 
management services and run the dashboard from the current version

Todo:

- run tests before create docker container
- add more tests
2023-08-12 23:11:32 +02:00
Sarooj bukhari
2443c6332d Apply quick group on all application (#253)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-08-10 15:34:21 +02:00
braginini
3b5193ae4e Fix protocol ALL value 2023-08-05 11:42:06 +02:00
Sarooj bukhari
cf42dd52fc fix expiration validation (#252)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-08-04 23:30:08 +02:00
Sarooj bukhari
bc6842e5b5 Add quick update of group on peer (#250) 2023-08-04 12:03:35 +02:00
Sarooj bukhari
a8ed755dda Sidebar menu for Settings (#249)
Replace tab view in the Settings panel with the left-handed
sidebar menu.
2023-08-02 16:14:13 +02:00
Sarooj bukhari
a87c06ef52 Groups management (#246) 2023-08-01 21:05:42 +02:00
Sarooj bukhari
c0130d265c fix domain validation (#247)
* fix domain validation

* handle on update

* move logic to utils
2023-08-01 10:48:10 +02:00
Misha Bragin
63ced3088a Add public installation page (#248)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-31 20:21:15 +02:00
Misha Bragin
42b7a15466 Drag custom query params to auth layer (e.g., utm_source) (#244)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-31 15:16:51 +02:00
Sarooj bukhari
c88bfa6476 Add filter by groups in peer (#243)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-24 17:45:53 +02:00
Maycon Santos
c4138a8c45 fix one-off key creation
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-21 20:18:59 +02:00
Sarooj bukhari
3f5418bebc Bug/remove group duplication on select view (#241)
* remove duplicate group on select view to change the logic from name to id

* fix group bug and add grouping on DNS

* fix setup key issue
2023-07-21 19:46:22 +02:00
Maycon Santos
f60605e5e3 Allow unset audience (#240)
by setting the AUTH_AUDIENCE=none we will unset the audience value,

not passing that to the requests

This is required for the jumpcloud support
2023-07-21 19:45:37 +02:00
Sarooj bukhari
c1b2ededa7 remove duplicate group on select view to change the logic from name to id (#238)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-20 10:08:26 +02:00
Sarooj bukhari
31eaa4a241 add all filter state on persist on page refresh (#236)
* add all filter state on persist on page refresh

* clear state on logout

* fix issues while refresh
2023-07-20 09:22:01 +02:00
dependabot[bot]
ba365336ff Bump word-wrap from 1.2.3 to 1.2.4 (#237)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-19 22:02:03 +05:00
Sarooj bukhari
06c238b013 Fix peer mobile view (#233) 2023-07-18 08:50:58 +02:00
dependabot[bot]
ed56c240f3 Bump semver from 6.3.0 to 6.3.1 (#230)
Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-15 01:01:59 +05:00
dependabot[bot]
1f3c7d87d7 Bump tough-cookie from 4.1.2 to 4.1.3 (#223)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-15 01:01:37 +05:00
Zoltan Papp
9de2906fb2 Extend activity view with group delete (#232) 2023-07-14 10:11:32 +02:00
Sarooj bukhari
359b443326 Update remaining pages layout (#231) 2023-07-14 10:09:08 +02:00
braginini
ecae39d94b Adjust DNS view
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-13 12:55:04 +02:00
Sarooj bukhari
30b858c1bc Make all modals UI consistent (#229)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-12 17:35:55 +02:00
Sarooj bukhari
e82b269ff5 fix network routes pages (#228) 2023-07-11 09:06:08 +02:00
braginini
23a4b79f01 Fix disabled links 2023-07-10 18:13:08 +02:00
Sarooj bukhari
cc5a9b1033 Fix table loading glitch (#227) 2023-07-10 16:49:52 +02:00
Sarooj bukhari
09ae157be3 Re-work DNS layout (#222) 2023-07-10 09:03:27 +02:00
pascal-fischer
cb89eeb921 Fix default value for expiration for setup keys (#221)
* fix initial value for setup keys so default works

* fix initial value for setup keys so default works
2023-07-07 12:40:51 +02:00
pascal-fischer
79446c0e77 remove formatted expiry logic and fix sent expires_in (#220)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-06 17:16:10 +02:00
Zoltan Papp
1f258e4e2c Update copyright title at the bottom of the page (#219) 2023-07-06 16:38:02 +02:00
braginini
bae95d2e11 Fix peer groups control in the peer update view
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-07-01 20:40:26 +02:00
Sarooj bukhari
3d95a826e7 Improve ACL Ui layout 2023-07-01 20:06:46 +02:00
Sarooj bukhari
d8d13aff01 Add copy on add peer code (#217)
Co-authored-by: Sarooj Bukhari <120650489+saroojbukhari2022@users.noreply.github.com>
2023-06-27 15:01:59 +02:00
Sarooj bukhari
695b571a50 table route layout (#213)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Co-authored-by: Sarooj Bukhari <120650489+saroojbukhari2022@users.noreply.github.com>
Co-authored-by: braginini <misha@netbird.io>
2023-06-27 11:08:49 +03:00
Sarooj bukhari
ddfb6a6179 Add copy button on add peer code box and remove check to shoe default rule
Co-authored-by: Sarooj Bukhari <120650489+saroojbukhari2022@users.noreply.github.com>
2023-06-27 07:06:21 +02:00
pascal-fischer
8c94119c6a extend gitignore to ignore all config files (#212) 2023-06-19 17:16:57 +02:00
Sarooj bukhari
439e803ef2 Fix group change loader (#211)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-06-19 15:51:08 +02:00
braginini
440c51a35c Fix empty tables handling on all tabs 2023-06-18 18:15:34 +02:00
braginini
800e94de85 Make offline peers appear "grey" in the peer's details 2023-06-18 15:32:01 +02:00
braginini
ba7d138156 Fix needs login color palette 2023-06-18 15:29:53 +02:00
braginini
a66fb3bf8f Show offline peers with a grey indicator 2023-06-18 15:05:11 +02:00
Sarooj bukhari
fcc51243e0 Update site fonts (#209) 2023-06-18 15:03:40 +02:00
Sarooj bukhari
312c60dd45 Update site fonts (#208) 2023-06-16 21:00:54 +02:00
Sarooj bukhari
09e6de74ee Update route and add on update peer (#205) 2023-06-15 10:28:55 +02:00
pascal-fischer
addd348456 Fix isAdmin check to not depend on oidc (#207)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* remove admin check depending on oidc user on autogroups field

* fix admin check on peers view
2023-06-14 17:19:45 +02:00
braginini
a8190bfe5b Fix Rule ports placeholder 2023-06-13 16:39:01 +02:00
braginini
9e3d9f245d Add disabled filter option to access controls
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-06-13 14:11:27 +02:00
Misha Bragin
e7a7a75906 Align peers count and font in the popover groups (#206) 2023-06-13 13:17:48 +02:00
braginini
67efd47f22 Minor changes to access control layout 2023-06-13 11:03:30 +02:00
Sarooj bukhari
813cd851ca Update access control Add New layout (#202) 2023-06-12 09:40:04 +02:00
pascal-fischer
f44ccf3ef7 remove keydown handler from PAT popup (#203) 2023-06-09 12:09:07 +02:00
Bethuel
899f56acdc fix: remove any trailing / in auth authority (#201)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-06-07 15:35:20 +02:00
braginini
dc2760d5ff Fix elements alignment for the ACLs view 2023-06-06 15:07:51 +02:00
Sarooj bukhari
5ae4fd31f1 Udpate acceess control table layout (#199)
Co-authored-by: Sarooj Bukhari <120650489+saroojbukhari2022@users.noreply.github.com>
2023-06-06 14:31:18 +02:00
braginini
54d9d7c768 Remove peer view header 2023-06-05 18:44:18 +02:00
braginini
3a73781fca Merge remote-tracking branch 'origin/main' 2023-06-05 18:10:21 +02:00
braginini
f3c5d4df6a Fix color scheme for disabled fields 2023-06-05 18:07:28 +02:00
Bethuel
b4c9135c58 support authentication with client_secret (#198) 2023-06-05 18:02:43 +02:00
Sarooj bukhari
a280d9c67c Update peers layout and add routes component on peers edit (#196) 2023-06-05 15:22:03 +02:00
Misha Bragin
5caf06b086 Decrease length of the hidden setup key prefix (#195)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-06-02 11:06:05 +02:00
braginini
95af1c4e32 lower case of the Create key button 2023-06-02 10:53:47 +02:00
Sarooj bukhari
d247e7f3ed Change edit key on group from popup to page (#194) 2023-06-01 16:51:51 +02:00
Sarooj bukhari
391e534253 Show setupkey once on creation (#191) 2023-06-01 12:08:58 +02:00
Sarooj bukhari
a7f64d4a15 Update setup key edit layout (#190)
https://github.com/netbirdio/dashboard/issues/187
2023-06-01 09:49:10 +02:00
Givi Khojanashvili
53ed514803 ACL to firewall rules (#163)
ACL based on the firewall rules
2023-06-01 10:06:08 +04:00
pascal-fischer
6cadce1598 Add pkg installer to MacOS popup (#188) 2023-05-30 19:05:29 +02:00
pascal-fischer
c03d4b8a4b Fix peers views in safari 2023-05-30 13:42:29 +02:00
dependabot[bot]
bc1053fbb4 Bump json5 from 1.0.1 to 1.0.2 (#124) 2023-05-29 13:58:20 +02:00
dependabot[bot]
d154bfb799 Bump webpack from 5.74.0 to 5.76.1 (#148) 2023-05-29 13:57:48 +02:00
pascal-fischer
67fd2fcb2e Update links to docs (#182)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-05-24 17:31:57 +02:00
Misha Bragin
f4933e45ee Add peer on Android (#185)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-05-19 21:42:13 +02:00
Misha Bragin
331dd2b429 Make it more visible that user can be blocked (#184)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-05-19 12:40:08 +02:00
Misha Bragin
3a8106c1e7 Track block/unblock user activity (#181)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-05-17 09:53:46 +02:00
Misha Bragin
b595e0d6a8 Switch to activate/deactivate a user (#179)
Some checks failed
build and push / build_n_push (push) Has been cancelled
This PR adds a switch to deactivate (or block) a user.
Only admins can block/unblock users.
Users can't block themselves.
2023-05-15 16:57:35 +02:00
pascal-fischer
33621cae5d remove href- from breadcrumbs (#180) 2023-05-15 14:28:14 +02:00
Maycon Santos
360d807008 Enable creating service user for all domains (#178)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Removed unnecessary check from create button.
2023-05-14 11:56:14 +02:00
Misha Bragin
6f8897ffa5 Make breadcrumbs look like links (#176) 2023-05-10 18:48:11 +02:00
pascal-fischer
75d5f804c5 fix dropdown button in lists to only show ellipsis (#175) 2023-05-10 16:45:47 +02:00
pascal-fischer
f7cac02a2d reduce api calls on user edit (#174) 2023-05-10 16:44:43 +02:00
Misha Bragin
ec40730cb2 Fix setup key layout (#172)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-05-08 17:49:27 +02:00
Misha Bragin
7f3648861b Fix popups layout (#171) 2023-05-08 12:30:53 +02:00
Crusadero
b50464db43 Setup keys screen (#167) 2023-05-07 18:39:28 +02:00
pascal-fischer
1eb5ccc131 displaying proper groups and name on route peer update (#169) 2023-05-05 15:50:39 +02:00
116 changed files with 22870 additions and 8686 deletions

View File

@@ -19,7 +19,7 @@ jobs:
node-version: '16'
cache: 'npm'
- name: Install dependecies
- name: Install dependencies
run: npm install
- name: Build

15
.github/workflows/codespell.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Codespell
on: [pull_request]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: codespell
uses: codespell-project/actions-codespell@v2
with:
only_warn: 1
skip: package-lock.json,*.svg

42
.github/workflows/e2e-tests.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: run e2e tests
on:
push:
branches:
- main
pull_request:
jobs:
e2e_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: setup-node
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: install playwright
run: npx playwright install
- name: install playwright deps
run: npx playwright install-deps
- name: create test environment
run: bash ./e2e-tests/create-test-env.sh
- name: run e2e tests
run: npx playwright test --workers 2
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 3

14
.gitignore vendored
View File

@@ -26,6 +26,18 @@ yarn-error.log*
src/auth_config.json
.idea
.eslintcache
src/.local-config.json
src/.local-config*.json
/public/OidcServiceWorker.js
/public/OidcTrustedDomains.js
/e2e-tests/node_modules/
/e2e-tests/playwright-report/
/e2e-tests/test-results/
/test-results/
/playwright-report/
.env
Caddyfile
docker-compose.yml
machinekey/
management.json
turnserver.conf
zitadel.env

View File

@@ -1,15 +1,15 @@
# Wiretrustee Dashboard
Wiretrustee Dashboard is a the Wiretrustee Managemenet server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
Wiretrustee Dashboard is a the Wiretrustee Management server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
## Tags
```latest``` ```vX.X.X``` not available yet.
```main``` builded on every PR being merged to the repository
```main``` built on every PR being merged to the repository
## How to use this image
HTTP run:
```shell
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
```
Using SSL certificate from Let's Encript®:
Using SSL certificate from Let's Encrypt®:
```shell
docker run -d --rm -p 80:80 -p 443:443 \
-e LETSENCRYPT_DOMAIN=app.mydomain.com \

View File

@@ -22,6 +22,10 @@ if [[ -z "${AUTH_AUDIENCE}" ]]; then
fi
fi
if [[ "${AUTH_AUDIENCE}" == "none" ]]; then
unset AUTH_AUDIENCE
fi
if [[ -z "${AUTH_SUPPORTED_SCOPES}" ]]; then
if [[ -z "${AUTH0_DOMAIN}" ]]; then
echo "AUTH_SUPPORTED_SCOPES environment variable must be set"
@@ -43,6 +47,7 @@ fi
export AUTH_AUTHORITY=${AUTH_AUTHORITY:-https://$AUTH0_DOMAIN}
export AUTH_CLIENT_ID=${AUTH_CLIENT_ID:-$AUTH0_CLIENT_ID}
export AUTH_CLIENT_SECRET=${AUTH_CLIENT_SECRET}
export AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH0_AUDIENCE}
export AUTH_REDIRECT_URI=${AUTH_REDIRECT_URI}
export AUTH_SILENT_REDIRECT_URI=${AUTH_SILENT_REDIRECT_URI}
@@ -53,11 +58,12 @@ export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE"
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"

View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker-compose down --volumes
rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json

View File

@@ -0,0 +1,697 @@
#!/bin/bash
set -e
handle_request_command_status() {
PARSED_RESPONSE=$1
FUNCTION_NAME=$2
RESPONSE=$3
if [[ $PARSED_RESPONSE -ne 0 ]]; then
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
exit 1
fi
}
handle_zitadel_request_response() {
PARSED_RESPONSE=$1
FUNCTION_NAME=$2
RESPONSE=$3
if [[ $PARSED_RESPONSE == "null" ]]; then
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
exit 1
fi
sleep 1
}
check_docker_compose() {
if command -v docker-compose &> /dev/null
then
echo "docker-compose"
return
fi
if docker compose --help &> /dev/null
then
echo "docker compose"
return
fi
echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
exit 1
}
check_jq() {
if ! command -v jq &> /dev/null
then
echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr
exit 1
fi
}
wait_crdb() {
set +e
while true; do
if $DOCKER_COMPOSE_COMMAND exec -T crdb curl -sf -o /dev/null 'http://localhost:8080/health?ready=1'; then
break
fi
echo -n " ."
sleep 5
done
echo " done"
set -e
}
init_crdb() {
echo -e "\nInitializing Zitadel's CockroachDB\n\n"
$DOCKER_COMPOSE_COMMAND up -d crdb
echo ""
# shellcheck disable=SC2028
echo -n "Waiting cockroachDB to become ready "
wait_crdb
$DOCKER_COMPOSE_COMMAND exec -T crdb /bin/bash -c "cp /cockroach/certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown -R 1000:1000 /zitadel-certs/"
handle_request_command_status $? "init_crdb failed" ""
}
get_main_ip_address() {
if [[ "$OSTYPE" == "darwin"* ]]; then
interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}')
else
interface=$(ip route | grep default | awk '{print $5}' | head -n 1)
ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
fi
echo "$ip_address"
}
wait_pat() {
PAT_PATH=$1
set +e
while true; do
if [[ -f "$PAT_PATH" ]]; then
break
fi
echo -n " ."
sleep 1
done
echo " done"
set -e
}
wait_api() {
INSTANCE_URL=$1
PAT=$2
set +e
while true; do
curl -s --fail -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT"
if [[ $? -eq 0 ]]; then
break
fi
echo -n " ."
sleep 1
done
echo " done"
set -e
}
create_new_project() {
INSTANCE_URL=$1
PAT=$2
PROJECT_NAME="NETBIRD"
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"name": "'"$PROJECT_NAME"'"}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_new_application() {
INSTANCE_URL=$1
PAT=$2
APPLICATION_NAME=$3
BASE_REDIRECT_URL1=$4
BASE_REDIRECT_URL2=$5
LOGOUT_URL=$6
ZITADEL_DEV_MODE=$7
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"name": "'"$APPLICATION_NAME"'",
"redirectUris": [
"'"$BASE_REDIRECT_URL1"'",
"'"$BASE_REDIRECT_URL2"'"
],
"postLogoutRedirectUris": [
"'"$LOGOUT_URL"'"
],
"RESPONSETypes": [
"OIDC_RESPONSE_TYPE_CODE"
],
"grantTypes": [
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
],
"appType": "OIDC_APP_TYPE_USER_AGENT",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
"version": "OIDC_VERSION_1_0",
"devMode": '"$ZITADEL_DEV_MODE"',
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
"accessTokenRoleAssertion": true,
"skipNativeAppSuccessPage": true
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_service_user() {
INSTANCE_URL=$1
PAT=$2
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userName": "netbird-service-account",
"name": "Netbird Service Account",
"description": "Netbird Service Account for IDP management",
"accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_service_user_secret() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{}'
)
SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId')
handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE"
SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret')
handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE"
}
add_organization_user_manager() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userId": "'"$USER_ID"'",
"roles": [
"ORG_USER_MANAGER"
]
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
create_admin_user() {
INSTANCE_URL=$1
PAT=$2
USERNAME=$3
PASSWORD=$4
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userName": "'"$USERNAME"'",
"profile": {
"firstName": "Zitadel",
"lastName": "Admin"
},
"email": {
"email": "'"$USERNAME"'",
"isEmailVerified": true
},
"password": "'"$PASSWORD"'",
"passwordChangeRequired": false
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
add_instance_admin() {
INSTANCE_URL=$1
PAT=$2
USER_ID=$3
RESPONSE=$(
curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"userId": "'"$USER_ID"'",
"roles": [
"IAM_OWNER"
]
}'
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
delete_auto_service_user() {
INSTANCE_URL=$1
PAT=$2
RESPONSE=$(
curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
USER_ID=$(echo "$RESPONSE" | jq -r '.user.id')
handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE"
RESPONSE=$(
curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE"
RESPONSE=$(
curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
)
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE"
echo "$PARSED_RESPONSE"
}
init_zitadel() {
echo -e "\nInitializing Zitadel with NetBird's applications\n"
INSTANCE_URL="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
TOKEN_PATH=./machinekey/zitadel-admin-sa.token
echo -n "Waiting for Zitadel's PAT to be created "
wait_pat "$TOKEN_PATH"
echo "Reading Zitadel PAT"
PAT=$(cat $TOKEN_PATH)
if [ "$PAT" = "null" ]; then
echo "Failed requesting getting Zitadel PAT"
exit 1
fi
echo -n "Waiting for Zitadel to become ready "
wait_api "$INSTANCE_URL" "$PAT"
# create the zitadel project
echo "Creating new zitadel project"
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT")
ZITADEL_DEV_MODE=false
BASE_REDIRECT_URL=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
if [[ $NETBIRD_HTTP_PROTOCOL == "http" ]]; then
ZITADEL_DEV_MODE=true
fi
# create zitadel spa applications
echo "Creating new Zitadel SPA Dashboard application"
DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "http://localhost:3000/nb-auth" "http://localhost:3000/nb-silent-auth" "http://localhost:3000/" "true")
echo "Creating new Zitadel SPA Cli application"
CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true")
MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")
SERVICE_USER_CLIENT_ID="null"
SERVICE_USER_CLIENT_SECRET="null"
create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"
DATE=$(add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID")
ZITADEL_ADMIN_USERNAME="admin@localhost"
ZITADEL_ADMIN_PASSWORD="testMe123@"
HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD")
DATE="null"
DATE=$(add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID")
DATE="null"
DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT")
if [ "$DATE" = "null" ]; then
echo "Failed deleting auto service user"
echo "Please remove it manually"
fi
export NETBIRD_AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID
export NETBIRD_AUTH_CLIENT_ID_CLI=$CLI_APPLICATION_CLIENT_ID
export NETBIRD_IDP_MGMT_CLIENT_ID=$SERVICE_USER_CLIENT_ID
export NETBIRD_IDP_MGMT_CLIENT_SECRET=$SERVICE_USER_CLIENT_SECRET
export ZITADEL_ADMIN_USERNAME
export ZITADEL_ADMIN_PASSWORD
}
check_nb_domain() {
DOMAIN=$1
if [ "$DOMAIN-x" == "-x" ]; then
echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr
return 1
fi
if [ "$DOMAIN" == "netbird.example.com" ]; then
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
return 1
fi
return 0
}
read_nb_domain() {
READ_NETBIRD_DOMAIN=""
echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr
read -r READ_NETBIRD_DOMAIN < /dev/tty
if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then
read_nb_domain
fi
echo "$READ_NETBIRD_DOMAIN"
}
initEnvironment() {
CADDY_SECURE_DOMAIN=""
ZITADEL_EXTERNALSECURE="false"
ZITADEL_TLS_MODE="disabled"
ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)"
NETBIRD_PORT=80
NETBIRD_HTTP_PROTOCOL="http"
TURN_USER="self"
TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g')
TURN_MIN_PORT=49152
TURN_MAX_PORT=65535
NETBIRD_DOMAIN=$(get_main_ip_address)
if [[ "$OSTYPE" == "darwin"* ]]; then
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
else
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ")
fi
check_jq
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
if [ -f zitadel.env ]; then
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
echo "You can use the following commands:"
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json"
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
exit 1
fi
echo Rendering initial files...
renderDockerCompose > docker-compose.yml
renderCaddyfile > Caddyfile
renderZitadelEnv > zitadel.env
echo "" > turnserver.conf
echo "" > management.json
mkdir -p machinekey
chmod 777 machinekey
init_crdb
echo -e "\nStarting Zidatel IDP for user management\n\n"
$DOCKER_COMPOSE_COMMAND up -d caddy zitadel
init_zitadel
echo -e "\nRendering NetBird files...\n"
renderTurnServerConf > turnserver.conf
renderManagementJson > management.json
renderDashboardEnv > src/.local-config.json
echo -e "\nStarting NetBird services\n"
$DOCKER_COMPOSE_COMMAND up -d
echo -e "\nDone!\n"
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
echo "Login with the following credentials:"
echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
}
renderCaddyfile() {
cat <<EOF
{
debug
servers :80,:443 {
protocols h1 h2c
}
}
:80${CADDY_SECURE_DOMAIN} {
# Signal
reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
# Management
reverse_proxy /api/* management:80
reverse_proxy /management.ManagementService/* h2c://management:80
# Zitadel
reverse_proxy /zitadel.admin.v1.AdminService/* h2c://zitadel:8080
reverse_proxy /admin/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.auth.v1.AuthService/* h2c://zitadel:8080
reverse_proxy /auth/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.management.v1.ManagementService/* h2c://zitadel:8080
reverse_proxy /management/v1/* h2c://zitadel:8080
reverse_proxy /zitadel.system.v1.SystemService/* h2c://zitadel:8080
reverse_proxy /system/v1/* h2c://zitadel:8080
reverse_proxy /assets/v1/* h2c://zitadel:8080
reverse_proxy /ui/* h2c://zitadel:8080
reverse_proxy /oidc/v1/* h2c://zitadel:8080
reverse_proxy /saml/v2/* h2c://zitadel:8080
reverse_proxy /oauth/v2/* h2c://zitadel:8080
reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
reverse_proxy /openapi/* h2c://zitadel:8080
reverse_proxy /debug/* h2c://zitadel:8080
# Dashboard
reverse_proxy /* dashboard:80
}
EOF
}
renderTurnServerConf() {
cat <<EOF
listening-port=3478
tls-listening-port=5349
min-port=$TURN_MIN_PORT
max-port=$TURN_MAX_PORT
fingerprint
lt-cred-mech
user=$TURN_USER:$TURN_PASSWORD
realm=wiretrustee.com
cert=/etc/coturn/certs/cert.pem
pkey=/etc/coturn/private/privkey.pem
log-file=stdout
no-software-attribute
pidfile="/var/tmp/turnserver.pid"
no-cli
EOF
}
renderManagementJson() {
cat <<EOF
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:$NETBIRD_DOMAIN:3478"
}
],
"TURNConfig": {
"Turns": [
{
"Proto": "udp",
"URI": "turn:$NETBIRD_DOMAIN:3478",
"Username": "$TURN_USER",
"Password": "$TURN_PASSWORD"
}
],
"TimeBasedCredentials": false
},
"Signal": {
"Proto": "$NETBIRD_HTTP_PROTOCOL",
"URI": "$NETBIRD_DOMAIN:$NETBIRD_PORT"
},
"HttpConfig": {
"AuthIssuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN",
"AuthAudience": "$NETBIRD_AUTH_CLIENT_ID",
"OIDCConfigEndpoint":"$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/.well-known/openid-configuration"
},
"IdpManagerConfig": {
"ManagerType": "zitadel",
"ClientConfig": {
"Issuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"TokenEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/oauth/v2/token",
"ClientID": "$NETBIRD_IDP_MGMT_CLIENT_ID",
"ClientSecret": "$NETBIRD_IDP_MGMT_CLIENT_SECRET",
"GrantType": "client_credentials"
},
"ExtraConfig": {
"ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/management/v1"
}
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
"Scope": "openid profile email offline_access",
"RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
}
}
}
EOF
}
renderDashboardEnv() {
cat <<EOF
{
"auth0Auth": "false",
"authAuthority": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"authClientId": "$NETBIRD_AUTH_CLIENT_ID",
"authScopesSupported": "openid profile email offline_access",
"authAudience": "$NETBIRD_AUTH_CLIENT_ID",
"apiOrigin": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"grpcApiOrigin": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
"redirectURI": "/nb-auth",
"silentRedirectURI": "/nb-silent-auth"
}
EOF
}
renderZitadelEnv() {
cat <<EOF
ZITADEL_LOG_LEVEL=debug
ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY
ZITADEL_DATABASE_COCKROACH_HOST=crdb
ZITADEL_DATABASE_COCKROACH_USER_USERNAME=zitadel_user
ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT="/crdb-certs/client.zitadel_user.crt"
ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY="/crdb-certs/client.zitadel_user.key"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE=verify-full
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT="/crdb-certs/ca.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT="/crdb-certs/client.root.crt"
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY="/crdb-certs/client.root.key"
ZITADEL_EXTERNALSECURE=$ZITADEL_EXTERNALSECURE
ZITADEL_TLS_ENABLED="false"
ZITADEL_EXTERNALPORT=$NETBIRD_PORT
ZITADEL_EXTERNALDOMAIN=$NETBIRD_DOMAIN
ZITADEL_FIRSTINSTANCE_PATPATH=/machinekey/zitadel-admin-sa.token
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_SCOPES=openid
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE=$ZIDATE_TOKEN_EXPIRATION_DATE
EOF
}
renderDockerCompose() {
cat <<EOF
version: "3.4"
services:
# Caddy reverse proxy
caddy:
image: caddy
restart: unless-stopped
networks: [ netbird ]
ports:
- '443:443'
- '80:80'
- '8080:8080'
volumes:
- netbird_caddy_data:/data
- ./Caddyfile:/etc/caddy/Caddyfile
# Management
management:
image: netbirdio/management:latest
restart: unless-stopped
networks: [netbird]
volumes:
- netbird_management:/var/lib/netbird
- ./management.json:/etc/netbird/management.json
command: [
"--port", "80",
"--log-file", "console",
"--log-level", "info",
"--disable-anonymous-metrics=false",
"--single-account-mode-domain=netbird.selfhosted",
"--dns-domain=netbird.selfhosted",
"--idp-sign-key-refresh-enabled",
]
# Zitadel - identity provider
zitadel:
restart: 'always'
networks: [netbird]
image: 'ghcr.io/zitadel/zitadel:v2.31.3'
command: 'start-from-init --masterkeyFromEnv --tlsMode $ZITADEL_TLS_MODE'
env_file:
- ./zitadel.env
depends_on:
crdb:
condition: 'service_healthy'
volumes:
- ./machinekey:/machinekey
- netbird_zitadel_certs:/crdb-certs:ro
# CockroachDB for zitadel
crdb:
restart: 'always'
networks: [netbird]
image: 'cockroachdb/cockroach:v22.2.2'
command: 'start-single-node --advertise-addr crdb'
volumes:
- netbird_crdb_data:/cockroach/cockroach-data
- netbird_crdb_certs:/cockroach/certs
- netbird_zitadel_certs:/zitadel-certs
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ]
interval: '10s'
timeout: '30s'
retries: 5
start_period: '20s'
volumes:
netbird_management:
netbird_caddy_data:
netbird_crdb_data:
netbird_crdb_certs:
netbird_zitadel_certs:
networks:
netbird:
EOF
}
initEnvironment

View File

@@ -0,0 +1,56 @@
import { Page, test, expect } from "@playwright/test";
export class AccessControlPage {
private readonly accessControlUrl = 'http://localhost:3000/acls'
private readonly defaulAccessControl = this.page.getByRole('cell', { name: 'Default' })
private readonly deleteButton = this.page.getByRole('button', { name: 'Delete' })
private readonly deleteModal = this.page.getByTestId('confirm-delete-modal-title')
private readonly confirmButton = this.page.getByRole('button', { name: 'OK' })
private readonly addRulesButton = this.page.getByTestId('add-rule-empty-state-button')
constructor(private readonly page: Page) {}
async openAccessControlPage() {
await test.step('Open Access Control page', async () => {
await this.page.goto(this.accessControlUrl);
})
}
async assertDefaultAccessCotrolIsCreated() {
await test.step('Assert that default control access is created', async () => {
await expect(this.defaulAccessControl).toBeVisible();
})
}
async pressDeleteButton() {
await test.step('Press delete button', async () => {
await this.deleteButton.click();
})
}
async assertDeleteModalIsVisibile() {
await test.step('Assert access control deletion modal is visible', async () => {
await expect(this.deleteModal).toBeVisible();
})
}
async pressConfirmButton() {
await test.step('Press confirm button on access control deletion modal', async () => {
await this.confirmButton.click();
})
}
async assertDefaultAccessCotrolIsDeleted() {
await test.step('Assert default access control should be deleted', async () => {
await expect(this.defaulAccessControl).not.toBeVisible();
})
}
async assertAddRuleButtonIsVisile() {
await test.step('Assert Add Rules button is visible', async () => {
await expect(this.addRulesButton).toBeVisible();
})
}
}
export default AccessControlPage;

View File

@@ -0,0 +1,34 @@
import { Page, test, expect} from "@playwright/test";
export class LoginPage {
private readonly localUrl = 'http://localhost:3000/'
private readonly usernameField = this.page.getByPlaceholder('username@domain')
private readonly nextButton = this.page.getByRole('button', { name: 'next' })
private readonly passwordField = this.page.getByLabel('Password')
private readonly skipButton = this.page.getByRole('button', { name: 'skip' });
private readonly netBirdLogo = this.page.getByRole('link', { name: 'logo' })
constructor(private readonly page: Page) {}
async doLogin() {
await test.step('Login to local enviroment', async () => {
await this.page.goto(this.localUrl);
await this.usernameField.fill('admin@localhost');
await this.pressNextButton();
await this.passwordField.fill('testMe123@');
await this.pressNextButton();
if (await this.skipButton.isVisible({ timeout: 300 })) {
await this.skipButton.click();
}
await expect(this.netBirdLogo).toBeVisible();
})
}
async pressNextButton() {
await test.step('Press next button', async () => {
await this.nextButton.click();
})
}
}
export default LoginPage;

View File

@@ -0,0 +1,111 @@
import { Page, test, expect } from "@playwright/test";
export class AddPeerModal {
private readonly addPeerModal = this.page.getByTestId('add-peer-modal').locator('div').nth(2)
private readonly linuxTab = this.page.getByTestId('add-peer-modal-linux-tab')
private readonly windowsTab = this.page.getByTestId('add-peer-modal-windows-tab')
private readonly macTab = this.page.getByTestId('add-peer-modal-mac-tab')
private readonly androidTab = this.page.getByTestId('add-peer-modal-android-tab')
private readonly dockerTab = this.page.getByTestId('add-peer-modal-docker-tab')
private readonly linuxTabText = this.page.locator('pre').filter({ hasText: 'curl -fsSL https://pkgs.netbird.io/install.sh | sh' })
private readonly windowsDownloadButton = this.page.getByTestId('download-windows-button')
private readonly intelDownloadButton = this.page.getByTestId('download-intel-button')
private readonly m1M2DownloadButton = this.page.getByTestId('download-m1-m2-button')
private readonly androidDownloadButton = this.page.getByTestId('download-android-button')
private readonly dockerDownloadButton = this.page.getByTestId('download-docker-button')
private readonly closeButton = this.page.getByLabel('Close', { exact: true })
constructor(private readonly page: Page) {}
async assertPeerModalIsVisible() {
await test.step('Assert that add peer modal is visible', async () => {
await expect(this.addPeerModal).toBeVisible();
})
}
async assertPeerModalIsNotVisible() {
await test.step('Assert that add peer modal is not visible', async () => {
await expect(this.addPeerModal).not.toBeVisible();
})
}
async openLinuxTab() {
await test.step('Open Linux tab on add peer modal', async () => {
await this.linuxTab.click();
})
}
async openWindowsTab() {
await test.step('Open Windows tab on add peer modal', async () => {
await this.windowsTab.click();
})
}
async openMacTab() {
await test.step('Open MacOS tab on add peer modal', async () => {
await this.macTab.click();
})
}
async openAndroidTab() {
await test.step('Open Android tab on add peer modal', async () => {
await this.androidTab.click();
})
}
async openDockerTab() {
await test.step('Open Docker tab on add peer modal', async () => {
await this.dockerTab.click();
})
}
async assertLinuxTabHasCorrectText() {
await test.step('Assert Linux tab has correct installation text', async () => {
await expect(this.linuxTabText).toBeVisible();
})
}
async assertWindowsDownloadButtonHasCorrectLink() {
await test.step('Assert Windows download button has a correct link', async () => {
await expect(this.windowsDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/windows/x64');
})
}
async assertIntelDownloadButtonHasCorrectLink() {
await test.step('Assert Intel download button has a correct link', async () => {
await expect(this.intelDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/amd64');
})
}
async assertM1M2DownloadButtonHasCorrectLink() {
await test.step('Assert M1 & M2 download button has a correct link', async () => {
await expect(this.m1M2DownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/arm64');
})
}
async assertAndroidDownloadButtonHasCorrectLink() {
await test.step('Assert Android download button has a correct link', async () => {
await expect(this.androidDownloadButton).toHaveAttribute('href', 'https://play.google.com/store/apps/details?id=io.netbird.client');
})
}
async assertDockerDownloadButtonHasCorrectLink() {
await test.step('Assert Docker download button has a correct link', async () => {
await expect(this.dockerDownloadButton).toHaveAttribute('href', 'https://docs.docker.com/engine/install/');
})
}
async closeAddPeerModal() {
await test.step('Close Add peer modal', async () => {
await this.closeButton.click();
})
}
}
export default AddPeerModal;

View File

@@ -0,0 +1,15 @@
import { Page, test } from "@playwright/test";
export class PeersPage {
private readonly addNewPeerButton = this.page.getByTestId('add-new-peer-button')
constructor(private readonly page: Page) {}
async clickOnAddNewPeerButton() {
await test.step('Click on Add new peer Button to open Add peer modal', async () => {
await this.addNewPeerButton.click();
})
}
}
export default PeersPage;

View File

@@ -0,0 +1,15 @@
import { Page, test} from "@playwright/test";
export class TopMenu {
private readonly accessControlButton = this.page.getByTestId('access-control-page')
constructor(private readonly page: Page) {}
async clickOnAccessControlOnTopMenu() {
await test.step('Click on Access Control page on a top menu', async () => {
await this.accessControlButton.click();
})
}
}
export default TopMenu;

View File

@@ -0,0 +1,22 @@
import { test } from '@playwright/test'
import {LoginPage} from '../pages/login-page'
import {AccessControlPage} from '../pages/access-control-page'
let loginPage: LoginPage
let accessControlPage: AccessControlPage
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.doLogin();
});
test('Confirm that new user has Default access', async ({ page }) => {
accessControlPage = new AccessControlPage(page);
await accessControlPage.openAccessControlPage();
await accessControlPage.assertDefaultAccessCotrolIsCreated();
await accessControlPage.pressDeleteButton();
await accessControlPage.assertDeleteModalIsVisibile();
await accessControlPage.pressConfirmButton();
await accessControlPage.assertDefaultAccessCotrolIsDeleted();
await accessControlPage.assertAddRuleButtonIsVisile();
});

View File

@@ -0,0 +1,49 @@
import { test } from '@playwright/test'
import {AddPeerModal} from '../pages/modals/add-peer-modal'
import {PeersPage} from '../pages/peers-page'
import {LoginPage} from '../pages/login-page'
let addPeerModal: AddPeerModal
let peersPage: PeersPage
let loginPage: LoginPage
test.beforeEach(async ({ page }) => {
addPeerModal = new AddPeerModal(page);
loginPage = new LoginPage(page);
await loginPage.doLogin();
await addPeerModal.assertPeerModalIsVisible();
});
test('Test Linux tab on a first access add peer modal / @bc', async function () {
await addPeerModal.openLinuxTab();
await addPeerModal.assertLinuxTabHasCorrectText();
});
test('Test Windows tab on a first access add peer modal / @bc', async () => {
await addPeerModal.openWindowsTab();
await addPeerModal.assertWindowsDownloadButtonHasCorrectLink();
});
test('Test MacOS tab on a first access add peer modal / @bc', async () => {
await addPeerModal.openMacTab();
await addPeerModal.assertIntelDownloadButtonHasCorrectLink();
await addPeerModal.assertM1M2DownloadButtonHasCorrectLink();
});
test('Test Android tab on a first access add peer modal', async () => {
await addPeerModal.openAndroidTab();
await addPeerModal.assertAndroidDownloadButtonHasCorrectLink();
});
test('Test Docker tab on a first access add peer modal', async () => {
await addPeerModal.openDockerTab();
await addPeerModal.assertDockerDownloadButtonHasCorrectLink();
});
test('Close and open Add peer modal', async ({ page }) => {
peersPage = new PeersPage(page);
await addPeerModal.closeAddPeerModal();
await addPeerModal.assertPeerModalIsNotVisible();
await peersPage.clickOnAddNewPeerButton();
await addPeerModal.assertPeerModalIsVisible();
});

664
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,7 @@
"react-redux": "^8.0.2",
"react-router-dom": "^5.3.3",
"react-scripts": "^5.0.1",
"react-select": "^5.7.3",
"react-syntax-highlighter": "^15.5.0",
"react-table": "^7.7.0",
"redux": "^4.2.0",
@@ -78,6 +79,7 @@
]
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.3"
"@types/react-syntax-highlighter": "^15.5.3",
"@playwright/test": "^1.36.2"
}
}

80
playwright.config.ts Normal file
View File

@@ -0,0 +1,80 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e-tests/tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { channel: 'chrome', },
},
// {
// name: 'firefox',
// use: { browserName: 'firefox', },
// },
//
// {
// name: 'webkit',
// use: { browserName: 'webkit', },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
timeout: 180 * 1000,
},
});

View File

@@ -70,16 +70,7 @@ function App() {
return (
<>
<ConfigProvider
theme={{
token: {
borderRadius: 4,
colorPrimary: "#1890ff",
fontFamily: "Arial"
},
components: {Badge: {fontSizeSM: 20}},
}}
>
<Provider store={store}>
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
{show &&
@@ -124,8 +115,6 @@ function App() {
</Layout>
}
</Provider>
</ConfigProvider>
</>
)
}

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29.435" height="7.636" viewBox="0 0 29.435 7.636">
<path id="direct_bi" d="M3.64,8.158-.178,4.34,3.64.522,4.3,1.17l-2.7,2.7h25.89l-2.7-2.7.656-.648L29.257,4.34,27.213,6.384,25.439,8.158,24.783,7.5l2.7-2.693H1.595L4.3,7.5Z" transform="translate(0.178 -0.522)" fill="#1e429f"/>
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.451731 2.66267C0.315048 2.79935 0.315048 3.02096 0.451731 3.15764L2.67912 5.38503C2.8158 5.52171 3.03741 5.52171 3.17409 5.38503C3.31078 5.24835 3.31078 5.02674 3.17409 4.89006L1.19419 2.91016L3.17409 0.930257C3.31078 0.793574 3.31078 0.571966 3.17409 0.435282C3.03741 0.298599 2.8158 0.298599 2.67912 0.435282L0.451731 2.66267ZM38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#03543F"/>
</svg>

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 837 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29.497" height="7.636" viewBox="0 0 29.497 7.636">
<path id="direct_out" d="M26.589,8l-.656-.656,2.7-2.693H.91V3.713H28.635l-2.7-2.7.656-.648,3.818,3.818Z" transform="translate(-0.91 -0.364)" fill="#03543f"/>
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#1E429F"/>
</svg>

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 506 B

View File

@@ -0,0 +1,3 @@
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.3846 3.68889C38.5213 3.55221 38.5213 3.3306 38.3846 3.19392L36.1572 0.966532C36.0205 0.829849 35.7989 0.829849 35.6622 0.966532C35.5255 1.10322 35.5255 1.32482 35.6622 1.46151L37.6421 3.44141L35.6622 5.42131C35.5255 5.55799 35.5255 5.7796 35.6622 5.91628C35.7989 6.05296 36.0205 6.05296 36.1572 5.91628L38.3846 3.68889ZM0.703125 3.79141H38.1371V3.09141H0.703125V3.79141Z" fill="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

3
src/assets/in_bound.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#2B4DA5"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@@ -0,0 +1,3 @@
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.3807 3.68889C38.5174 3.55221 38.5174 3.3306 38.3807 3.19392L36.1533 0.966532C36.0166 0.829849 35.795 0.829849 35.6583 0.966532C35.5216 1.10322 35.5216 1.32482 35.6583 1.46151L37.6382 3.44141L35.6583 5.42131C35.5216 5.55799 35.5216 5.7796 35.6583 5.91628C35.795 6.05296 36.0166 6.05296 36.1533 5.91628L38.3807 3.68889ZM0.699219 3.79141L38.1332 3.79141V3.09141L0.699219 3.09141V3.79141Z" fill="#2B4DA5"/>
</svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@@ -0,0 +1,3 @@
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.3846 3.68889C38.5213 3.55221 38.5213 3.3306 38.3846 3.19392L36.1572 0.966532C36.0205 0.829849 35.7989 0.829849 35.6622 0.966532C35.5255 1.10322 35.5255 1.32482 35.6622 1.46151L37.6421 3.44141L35.6622 5.42131C35.5255 5.55799 35.5255 5.7796 35.6622 5.91628C35.7989 6.05296 36.0205 6.05296 36.1572 5.91628L38.3846 3.68889ZM0.703125 3.79141H38.1371V3.09141H0.703125V3.79141Z" fill="#03543F"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

View File

@@ -0,0 +1,3 @@
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@@ -0,0 +1,3 @@
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#03543F"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@@ -0,0 +1,943 @@
import React, { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { actions as policyActions } from "../store/policy";
import {
Button,
Col,
Divider,
Form,
Input,
Card,
Row,
Select,
SelectProps,
Switch,
Tag,
Typography,
Breadcrumb,
} from "antd";
import inbound from "../assets/in_bound.svg";
import outBoundGreen from "../assets/out_bound_green.svg";
import outBoundblue from "../assets/out_bound_blue.svg";
import reverseDefault from "../assets/reverse_default.svg";
import forwardDefault from "../assets/forward_default.svg";
import reverseGreen from "../assets/reverse_green.svg";
import { Policy, PolicyToSave } from "../store/policy/types";
import { uniq } from "lodash";
import { Header } from "antd/es/layout/layout";
import { RuleObject } from "antd/lib/form";
import { useGetTokenSilently } from "../utils/token";
import { Container } from "./Container";
import { useGetGroupTagHelpers } from "../utils/groups";
const { Paragraph } = Typography;
const { Option } = Select;
const { Text } = Typography;
interface FormPolicy {
id?: string;
name: string;
description: string;
enabled: boolean;
query: string;
bidirectional: boolean;
protocol: string;
ports: string[];
action: string;
tagSourceGroups: string[];
tagDestinationGroups: string[];
}
const AccessControlEdit = () => {
const { optionRender, blueTagRender, tagGroups, grayTagRender } =
useGetGroupTagHelpers();
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const setupEditPolicyVisible = useSelector(
(state: RootState) => state.policy.setupEditPolicyVisible
);
const groups = useSelector((state: RootState) => state.group.data);
const actions: SelectProps["options"] = [
{ label: "Accept", value: "accept" },
{ label: "Drop", value: "drop" },
];
const protocols: SelectProps["options"] = [
{ label: "ALL", value: "all" },
{ label: "TCP", value: "tcp" },
{ label: "UDP", value: "udp" },
{ label: "ICMP", value: "icmp" },
];
const formPolicyCopy: any = ["ALL"];
const policy = useSelector((state: RootState) => state.policy.policy);
const savedPolicy = useSelector(
(state: RootState) => state.policy.savedPolicy
);
const [editName, setEditName] = useState(false);
const [editDescription, setEditDescription] = useState(false);
const [direction, setDirection] = useState<any>({
biDirectional: false,
reverseDirectional: false,
});
// const [tagGroups, setTagGroups] = useState([] as string[]);
const [formPolicy, setFormPolicy] = useState({} as FormPolicy);
const [form] = Form.useForm();
const inputNameRef = useRef<any>(null);
const inputDescriptionRef = useRef<any>(null);
useEffect(() => {
//Unmounting component clean
return () => {
onCancel();
};
}, []);
useEffect(() => {
if (editName) inputNameRef.current!.focus({ cursor: "end" });
}, [editName]);
useEffect(() => {
if (editDescription) inputDescriptionRef.current!.focus({ cursor: "end" });
}, [editDescription]);
// useEffect(() => {
// setTagGroups(groups?.map((g) => g.name) || []);
// }, [groups]);
useEffect(() => {
if (!policy) return;
const fPolicy = {
id: policy.id,
name: policy.name,
description: policy.description,
enabled: policy.enabled,
query: "",
bidirectional: policy.rules[0].bidirectional,
protocol: policy.rules[0].protocol,
ports: policy.rules[0].ports,
action: policy.rules[0].action,
tagSourceGroups: policy.rules[0].sources
? policy.rules[0].sources?.map((t) => t.id || "")
: [],
tagDestinationGroups: policy.rules[0].destinations
? policy.rules[0].destinations?.map((t) => t.id || "")
: [],
} as FormPolicy;
setFormPolicy(fPolicy);
form.setFieldsValue(fPolicy);
if (fPolicy.bidirectional) {
setDirection({
biDirectional: true,
reverseDirectional: true,
});
} else {
setDirection({
biDirectional: false,
reverseDirectional: false,
});
}
}, [policy, form]);
const createPolicyToSave = (): PolicyToSave => {
const sources =
groups
?.filter((g) => formPolicy.tagSourceGroups.includes(g.id || ""))
.map((g) => g.id || "") || [];
const destinations =
groups
?.filter((g) => formPolicy.tagDestinationGroups.includes(g.id || ""))
.map((g) => g.id || "") || [];
const existingGroupsNames: any[] = groups?.map((g) => g.id);
const sourcesNoId = formPolicy.tagSourceGroups.filter(
(s) => !existingGroupsNames.includes(s)
);
const destinationsNoId = formPolicy.tagDestinationGroups.filter(
(s) => !existingGroupsNames.includes(s)
);
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId]);
return {
id: formPolicy.id,
name: formPolicy.name,
description: formPolicy.description,
enabled: formPolicy.enabled,
sourcesNoId,
destinationsNoId,
groupsToSave,
rules: [
{
id: formPolicy.id,
name: formPolicy.name,
description: formPolicy.description,
enabled: formPolicy.enabled,
sources:
direction.reverseDirectional && !direction.biDirectional
? destinations
: sources,
destinations:
direction.reverseDirectional && !direction.biDirectional
? sources
: destinations,
bidirectional: formPolicy.bidirectional,
protocol: formPolicy.protocol,
ports: formPolicy.ports,
action: "accept",
},
],
} as PolicyToSave;
};
const handleFormSubmit = () => {
form
.validateFields()
.then((_) => {
const policyToSave = createPolicyToSave();
dispatch(
policyActions.savePolicy.request({
getAccessTokenSilently: getTokenSilently,
payload: policyToSave,
})
);
})
.catch((errorInfo) => {
console.log("errorInfo", errorInfo);
});
};
const setVisibleNewRule = (status: boolean) => {
dispatch(policyActions.setSetupEditPolicyVisible(status));
};
const onCancel = () => {
if (savedPolicy.loading) return;
setEditName(false);
dispatch(
policyActions.setPolicy({
name: "",
description: "",
enabled: true,
query: "",
rules: [
{
name: "",
description: "",
enabled: true,
sources: [],
destinations: [],
bidirectional: true,
protocol: "all",
ports: [],
action: "accept",
},
],
} as Policy)
);
setVisibleNewRule(false);
};
const onChange = (data: any) => {
setFormPolicy({ ...formPolicy, ...data });
};
const handleChangeSource = (value: string[]) => {
setFormPolicy({
...formPolicy,
tagSourceGroups: value,
});
};
const handleChangeDestination = (value: string[]) => {
setFormPolicy({
...formPolicy,
tagDestinationGroups: value,
});
};
const handleChangeProtocol = (value: string) => {
if (value === "all" || value === "icmp") {
setDirection({
biDirectional: true,
reverseDirectional: true,
});
}
setFormPolicy({
...formPolicy,
ports: value === "all" || value === "icmp" ? [] : formPolicy.ports,
protocol: value,
});
};
const handleChangePorts = (value: string[]) => {
setFormPolicy({
...formPolicy,
ports: value,
});
};
const handleChangeDisabled = (checked: boolean) => {
setFormPolicy({
...formPolicy,
enabled: checked,
});
};
const handleChangeBidirect = (checked: boolean) => {
setFormPolicy({
...formPolicy,
bidirectional: checked,
});
};
const dropDownRenderGroups = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{ margin: "8px 0" }} />
<Row style={{ padding: "0 8px 4px" }}>
<Col flex="auto">
<span style={{ color: "#9CA3AF" }}>
Add new group by pressing "Enter"
</span>
</Col>
<Col flex="none">
<svg
width="14"
height="12"
viewBox="0 0 14 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"
/>
</svg>
</Col>
</Row>
</>
);
const dropDownRenderPorts = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{ margin: "8px 0" }} />
<Row style={{ padding: "0 8px 4px" }}>
<Col flex="auto">
<Text type={"secondary"}>Add new ports by pressing "Enter"</Text>
</Col>
<Col flex="none">
<svg
width="14"
height="12"
viewBox="0 0 14 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"
/>
</svg>
</Col>
</Row>
</>
);
const toggleEditDescription = (status: boolean) => {
setEditDescription(status);
};
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = [];
if (!value.length) {
return Promise.reject(new Error("Please enter at least one group"));
}
value.forEach(function (v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v);
}
});
if (hasSpaceNamed.length) {
return Promise.reject(
new Error("Group names with just spaces are not allowed")
);
}
return Promise.resolve();
};
const selectPortRangeValidator = (_: RuleObject, value: string[]) => {
if (value) {
var failed = false;
value.forEach(function (v: string) {
let p = Number(v);
if (Number.isNaN(p) || p < 1 || p > 65535 || !Number.isInteger(p)) {
failed = true;
return;
}
});
if (failed) {
return Promise.reject(
new Error("Port value must be in 1..65535 range")
);
}
}
return Promise.resolve();
};
const selectPortProtocolValidator = (_: RuleObject, value: string[]) => {
if (!formPolicy.bidirectional && value.length === 0) {
return Promise.reject(new Error("Directional traffic require ports"));
}
return Promise.resolve();
};
const handleDirection = (directionValue: string) => {
if (
directionValue === "forwardDirectional" &&
!direction.reverseDirectional
) {
setDirection({
biDirectional: false,
reverseDirectional: false,
});
}
if (
directionValue === "forwardDirectional" &&
direction.reverseDirectional
) {
setDirection({
...direction,
biDirectional: !direction.biDirectional,
});
}
if (directionValue === "reverseDirectional" && direction.biDirectional) {
setDirection({
biDirectional: false,
reverseDirectional: !direction.reverseDirectional,
});
}
if (directionValue === "reverseDirectional" && !direction.biDirectional) {
setDirection({
biDirectional: true,
reverseDirectional: true,
});
}
};
const onBreadcrumbUsersClick = () => {
onCancel();
};
const toggleEditName = (status: boolean) => {
setEditName(status);
};
useEffect(() => {
if (Object.keys(formPolicy).length > 0) {
setFormPolicy({
...formPolicy,
bidirectional: direction.biDirectional,
});
}
}, [direction]);
return (
<>
{policy && (
<Container style={{ paddingTop: "40px" }}>
<Breadcrumb
style={{ marginBottom: "25px" }}
items={[
{
title: <a onClick={onBreadcrumbUsersClick}>Access Control</a>,
},
{
title: policy.name,
},
]}
/>
<Card bordered={true} style={{ marginBottom: "7px" }}>
<div style={{ maxWidth: "550px" }}>
<Form
layout="vertical"
requiredMark={false}
form={form}
onValuesChange={onChange}
>
<Row gutter={16}>
<Col span={24}>
<Header
style={{
border: "none",
}}
>
{!editName && (
<Paragraph
style={{
textAlign: "start",
whiteSpace: "pre-line",
fontSize: "22px",
margin: "0px",
marginBottom: "10px",
cursor: "pointer",
fontWeight: "500",
}}
onDoubleClick={() => toggleEditName(true)}
>
{formPolicy.name}
</Paragraph>
)}
<Row align="top">
<Col flex="auto">
{editName && (
<>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
Rule name
</Paragraph>
<Form.Item
name="name"
label=""
rules={[
{
required: true,
message:
"Please add a name for this access rule",
whitespace: true,
},
]}
>
<Input
placeholder={'for example "UserAccessRule"'}
ref={inputNameRef}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)}
autoComplete="off"
/>
</Form.Item>
</>
)}
{!editDescription ? (
<div
style={{
margin: "0 0 39px",
lineHeight: "22px",
cursor: "pointer",
}}
onClick={() => toggleEditDescription(true)}
>
{formPolicy.description &&
formPolicy.description.trim() !== "" ? (
formPolicy.description
) : (
<span style={{ textDecoration: "underline" }}>
Add description
</span>
)}
</div>
) : (
<Form.Item
name="description"
label="Description"
style={{ marginTop: 10, fontWeight: "500" }}
>
<Input
placeholder="Add description..."
ref={inputDescriptionRef}
onPressEnter={() =>
toggleEditDescription(false)
}
onBlur={() => toggleEditDescription(false)}
autoComplete="off"
/>
</Form.Item>
)}
</Col>
</Row>
</Header>
</Col>
<Col span={24} style={{ marginBottom: "15px" }}>
<Form.Item name="enabled" label="">
<div
style={{
display: "flex",
gap: "15px",
}}
>
<Switch
onChange={handleChangeDisabled}
defaultChecked={policy.enabled}
size="small"
/>
<div>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Enabled
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "0",
}}
>
{formPolicy.enabled
? "Disable this rule to apply it later"
: "Enable this rule to apply it immediately"}
</Paragraph>
</div>
</div>
</Form.Item>
</Col>
<Col span={24}>
<Row gutter={15}>
<Col span={10}>
<Form.Item
name="tagSourceGroups"
label="Source groups"
rules={[{ validator: selectValidator }]}
style={{ fontWeight: "500" }}
>
<Select
mode="tags"
style={{ width: "100%", fontWeight: "500" }}
placeholder="Select groups"
tagRender={blueTagRender}
onChange={handleChangeSource}
dropdownRender={dropDownRenderGroups}
optionFilterProp="serchValue"
>
{tagGroups.map((m, index) => (
<Option
key={index}
value={m.id}
serchValue={m.name}
>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col
span={4}
style={{ padding: "0 2.5px", lineHeight: "16px" }}
>
<Button
type={"ghost"}
disabled={
formPolicy.protocol === "all" ||
formPolicy.protocol === "icmp"
}
onClick={() => handleDirection("forwardDirectional")}
style={{
padding: "0",
width: "100%",
marginTop: "30px",
height: "13px",
}}
>
<Tag
style={{
marginInlineEnd: "0",
width: "100%",
textAlign: "center",
height: "13px",
display: "flex",
justifyContent: "center",
}}
color={
!direction.biDirectional &&
!direction.reverseDirectional
? "processing"
: direction.biDirectional
? "green"
: "default"
}
>
{!direction.biDirectional &&
!direction.reverseDirectional ? (
<img
src={outBoundblue}
style={{
width: "100%",
maxWidth: "45px",
}}
alt="out icon"
/>
) : direction.biDirectional ? (
<img
src={outBoundGreen}
alt="out icon"
style={{
width: "100%",
maxWidth: "45px",
}}
/>
) : (
<img
src={forwardDefault}
style={{
width: "100%",
maxWidth: "45px",
}}
alt="out icon"
/>
)}
</Tag>
</Button>
<Button
type="ghost"
disabled={
formPolicy.protocol === "all" ||
formPolicy.protocol === "icmp"
}
onClick={() => handleDirection("reverseDirectional")}
style={{
padding: "0",
width: "100%",
textAlign: "center",
height: "13px",
marginTop: "0",
}}
>
<Tag
style={{
marginInlineEnd: "0",
width: "100%",
textAlign: "center",
height: "13px",
display: "flex",
justifyContent: "center",
}}
color={
direction.reverseDirectional &&
direction.biDirectional
? "green"
: direction.reverseDirectional
? "processing"
: "default"
}
>
{direction.reverseDirectional &&
direction.biDirectional ? (
<img
src={reverseGreen}
style={{
width: "100%",
maxWidth: "45px",
}}
alt="out icon"
/>
) : direction.reverseDirectional ? (
<img
src={inbound}
style={{
width: "100%",
maxWidth: "45px",
}}
alt="out icon"
/>
) : (
<img
src={reverseDefault}
style={{
width: "100%",
maxWidth: "45px",
}}
alt="out icon"
/>
)}
</Tag>
</Button>
</Col>
<Col span={10}>
<Form.Item
name="tagDestinationGroups"
label="Destination groups"
rules={[{ validator: selectValidator }]}
style={{ fontWeight: "500" }}
>
<Select
mode="tags"
style={{ width: "100%", fontWeight: "500" }}
placeholder="Select groups"
tagRender={blueTagRender}
onChange={handleChangeDestination}
dropdownRender={dropDownRenderGroups}
optionFilterProp="serchValue"
>
{tagGroups.map((m, index) => (
<Option
key={index}
value={m.id}
serchValue={m.name}
>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Col>
<Col span={24} style={{ marginBottom: "15px" }}>
<Paragraph
type={"secondary"}
style={{ marginTop: "-15px", marginBottom: "30px" }}
>
To change traffic direction and ports, select TCP or UDP
protocol below
</Paragraph>
</Col>
<Col span={24} style={{ marginBottom: "15px" }}>
<Row>
<Col span={10}>
<Form.Item
name="protocol"
label="Protocol"
style={{ fontWeight: "500" }}
className="tag-box"
>
<Select
style={{ width: "100%", maxWidth: "260px" }}
options={protocols}
onChange={handleChangeProtocol}
className="menlo-font"
defaultValue={"all"}
/>
</Form.Item>
</Col>
</Row>
</Col>
<Col span={24}>
<Row>
<Col span={10}>
{formPolicy.protocol === "all" ||
formPolicy.protocol === "icmp" ? (
<Form.Item
label="Ports"
style={{ fontWeight: "500" }}
>
<Select
mode="tags"
style={{
width: "100%",
maxWidth: "260px",
fontWeight: "500",
}}
placeholder={
<div
color={"rgba(0,0,0,0.25)"}
className="arimo-font"
>
Select ports
</div>
}
className="menlo-font"
value={formPolicyCopy}
disabled={true}
></Select>
</Form.Item>
) : (
<Form.Item
name="ports"
label="Ports"
style={{ fontWeight: "500" }}
rules={[
{
message:
"Directional traffic requires at least one port",
validator: selectPortProtocolValidator,
required: false,
},
{
message: "Port value must be in 1..65535 range",
validator: selectPortRangeValidator,
required: false,
},
]}
>
<Select
mode="tags"
style={{
width: "100%",
maxWidth: "260px",
fontWeight: "500",
}}
placeholder={
<div
color={"rgba(0,0,0,0.25)"}
className="arimo-font"
>
Select ports
</div>
}
tagRender={grayTagRender}
onChange={handleChangePorts}
className="menlo-font"
dropdownRender={dropDownRenderPorts}
>
{formPolicy &&
formPolicy.ports?.map((m) => (
<Option key={m}>
<Tag style={{ marginRight: 3 }}>{m}</Tag>
</Option>
))}
</Select>
</Form.Item>
)}
</Col>
</Row>
</Col>
</Row>
</Form>
</div>
<Container
style={{
display: "flex",
flexDirection: "row",
justifyContent: "start",
padding: 0,
gap: "10px",
marginTop: "12px",
}}
key={0}
>
<Button onClick={onCancel} disabled={savedPolicy.loading}>
Cancel
</Button>
<Button
type="primary"
disabled={savedPolicy.loading}
onClick={handleFormSubmit}
>{`${formPolicy.id ? "Save" : "Create"}`}</Button>
</Container>
</Card>
</Container>
)}
</>
);
};
export default AccessControlEdit;

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ const Banner = () => {
const linkLearnMore = () => {
return (
<a
href="https://netbird.io/docs/how-to-guides/nameservers"
href="https://docs.netbird.io/how-to/manage-dns-in-your-network"
className="font-bold underline"
target="_blank"
rel="noreferrer"

View File

@@ -6,7 +6,7 @@ const { Footer } = Layout
export default () => {
return (
<Footer style={{ textAlign: 'center', bottom: "0"}}>
Copyright © 2022 <a href="https://netbird.io">NetBird Authors</a>
Copyright © 2023 <a href="https://netbird.io">NetBird Authors</a>
</Footer>
);
};
};

View File

@@ -0,0 +1,952 @@
import React, { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { actions as nsGroupActions } from "../store/nameservers";
import {
Button,
Col,
Switch,
Form,
FormListFieldData,
Input,
InputNumber,
message,
Modal,
Row,
Select,
Space,
Typography,
} from "antd";
import {
CloseOutlined,
MinusCircleOutlined,
PlusOutlined,
} from "@ant-design/icons";
import { Header } from "antd/es/layout/layout";
import { RuleObject } from "antd/lib/form";
import cidrRegex from "cidr-regex";
import {
NameServer,
NameServerGroup,
NameServerGroupToSave,
} from "../store/nameservers/types";
import { useGetGroupTagHelpers } from "../utils/groups";
import { useGetTokenSilently } from "../utils/token";
import { domainValidator } from "../utils/common";
const { Paragraph, Text } = Typography;
interface formNSGroup extends NameServerGroup {}
const NameServerGroupAdd = () => {
const {
blueTagRender,
handleChangeTags,
dropDownRender,
optionRender,
tagGroups,
getExistingAndToCreateGroupsLists,
getGroupNamesFromIDs,
selectValidator,
} = useGetGroupTagHelpers();
const dispatch = useDispatch();
const { getTokenSilently } = useGetTokenSilently();
const { Option } = Select;
const nsGroup = useSelector(
(state: RootState) => state.nameserverGroup.nameserverGroup
);
const setupNewNameServerGroupVisible = useSelector(
(state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible
);
const savedNSGroup = useSelector(
(state: RootState) => state.nameserverGroup.savedNameServerGroup
);
const nsGroupData = useSelector(
(state: RootState) => state.nameserverGroup.data
);
const [formNSGroup, setFormNSGroup] = useState({} as formNSGroup);
const [form] = Form.useForm();
const [editName, setEditName] = useState(false);
const [isPrimary, setIsPrimary] = useState(false);
const [editDescription, setEditDescription] = useState(false);
const inputNameRef = useRef<any>(null);
const inputDescriptionRef = useRef<any>(null);
const [selectCustom, setSelectCustom] = useState(false);
const [matchDomains, setMatchDomains] = useState(0);
useEffect(() => {
if (editName)
inputNameRef.current!.focus({
cursor: "end",
});
}, [editName]);
useEffect(() => {
if (editDescription)
inputDescriptionRef.current!.focus({
cursor: "end",
});
}, [editDescription]);
useEffect(() => {
if (!nsGroup) return;
let newFormGroup = {
...nsGroup,
groups: getGroupNamesFromIDs(nsGroup.groups),
} as formNSGroup;
setFormNSGroup(newFormGroup);
form.setFieldsValue(newFormGroup);
if (nsGroup.id) {
setSelectCustom(true);
}
if (nsGroup.primary !== undefined) {
setIsPrimary(nsGroup.primary);
}
}, [nsGroup]);
const onCancel = () => {
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
dispatch(
nsGroupActions.setNameServerGroup({
id: "",
name: "",
description: "",
primary: false,
domains: [],
nameservers: [] as NameServer[],
groups: [],
enabled: false,
search_domains_enabled: false,
} as NameServerGroup)
);
setEditName(false);
setSelectCustom(false);
setIsPrimary(false);
};
const onChange = (changedValues: any) => {
if (changedValues.primary !== undefined) {
setIsPrimary(changedValues.primary);
}
setFormNSGroup({ ...formNSGroup, ...changedValues });
};
let googleChoice = "Google DNS";
let cloudflareChoice = "Cloudflare DNS";
let quad9Choice = "Quad9 DNS";
let customChoice = "Add custom nameserver";
let defaultDNSOptions: NameServerGroup[] = [
{
name: googleChoice,
description: "Google DNS servers",
domains: [],
primary: true,
nameservers: [
{
ip: "8.8.8.8",
ns_type: "udp",
port: 53,
},
{
ip: "8.8.4.4",
ns_type: "udp",
port: 53,
},
],
groups: [],
enabled: true,
search_domains_enabled: false,
},
{
name: cloudflareChoice,
description: "Cloudflare DNS servers",
domains: [],
primary: true,
nameservers: [
{
ip: "1.1.1.1",
ns_type: "udp",
port: 53,
},
{
ip: "1.0.0.1",
ns_type: "udp",
port: 53,
},
],
groups: [],
enabled: true,
search_domains_enabled: false,
},
{
name: quad9Choice,
description: "Quad9 DNS servers",
domains: [],
primary: true,
nameservers: [
{
ip: "9.9.9.9",
ns_type: "udp",
port: 53,
},
{
ip: "149.112.112.112",
ns_type: "udp",
port: 53,
},
],
groups: [],
enabled: true,
search_domains_enabled: false,
},
];
const handleSelectChange = (value: string) => {
let nsGroupLocal = {} as NameServerGroup;
if (value === customChoice) {
nsGroupLocal = nsGroup;
nsGroupLocal.domains = [];
} else {
defaultDNSOptions.forEach((nsg) => {
if (value === nsg.name) {
nsGroupLocal = nsg;
}
});
}
let newFormGroup = {
...nsGroupLocal,
groups: getGroupNamesFromIDs(nsGroupLocal.groups),
} as formNSGroup;
setFormNSGroup(newFormGroup);
form.setFieldsValue(newFormGroup);
setSelectCustom(true);
};
const handleFormSubmit = () => {
form
.validateFields()
.then((values) => {
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup);
dispatch(
nsGroupActions.saveNameServerGroup.request({
getAccessTokenSilently: getTokenSilently,
payload: nsGroupToSave,
})
);
})
.then(() => onCancel())
.catch((errorInfo) => {
let msg = "please check the fields and try again";
if (errorInfo.errorFields) {
msg = errorInfo.errorFields[0].errors[0];
}
message.error({
content: msg,
duration: 1,
});
});
};
const createNSGroupToSave = (
values: NameServerGroup
): NameServerGroupToSave => {
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(
values.groups
);
return {
id: formNSGroup.id || null,
name: values.name ? values.name : formNSGroup.name,
description: values.description
? values.description
: formNSGroup.description,
primary: values.domains.length ? false : true,
domains: values.primary ? [] : values.domains,
nameservers: values.nameservers,
groups: existingGroups,
groupsToCreate: newGroups,
enabled: values.enabled,
search_domains_enabled:
matchDomains > 0 ? formNSGroup.search_domains_enabled : false,
} as NameServerGroupToSave;
};
const toggleEditName = (status: boolean) => {
setEditName(status);
};
const toggleEditDescription = (status: boolean) => {
setEditDescription(status);
};
const nameValidator = (_: RuleObject, value: string) => {
const found = nsGroupData.find(
(u) => u.name == value && u.id !== formNSGroup.id
);
if (found) {
return Promise.reject(
new Error(
"Please enter a unique name for your nameserver configuration"
)
);
}
return Promise.resolve();
};
const ipValidator = (_: RuleObject, value: string) => {
if (!cidrRegex().test(value + "/32")) {
return Promise.reject(
new Error("Please enter a valid IP, e.g. 192.168.1.1 or 8.8.8.8")
);
}
return Promise.resolve();
};
// @ts-ignore
const formListValidator = (_: RuleObject, names) => {
if (names.length >= 3) {
return Promise.reject(
new Error("Exceeded maximum number of Nameservers. (Max is 2)")
);
}
if (names.length < 1) {
return Promise.reject(new Error("You should add at least 1 Nameserver"));
}
return Promise.resolve();
};
// @ts-ignore
const renderNSList = (
fields: FormListFieldData[],
{ add, remove }: any,
{ errors }: any
) => (
<div style={{ width: "100%", maxWidth: "360px" }}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
marginBottom: "10px",
display: "block",
}}
>
Nameservers
</label>
{!!fields.length && (
<Row align="middle">
<Col span={4} style={{ textAlign: "left" }}>
<Typography.Text
style={{ color: "#818183", paddingLeft: "5px" }}
></Typography.Text>
</Col>
<Col span={10} style={{ textAlign: "left" }}>
<Typography.Text style={{ color: "#818183", paddingLeft: "5px" }}>
Nameserver IP
</Typography.Text>
</Col>
<Col span={4} style={{ textAlign: "left" }}>
<Typography.Text style={{ color: "#818183", paddingLeft: "5px" }}>
Port
</Typography.Text>
</Col>
<Col span={4} />
</Row>
)}
{fields.map((field, index) => {
return (
<Row key={index}>
<Col span={4} style={{ textAlign: "left" }}>
<Form.Item
style={{ margin: "3px" }}
name={[field.name, "ns_type"]}
rules={[{ required: true, message: "Missing first protocol" }]}
initialValue={"udp"}
>
<Select
disabled
style={{ width: "100%" }}
className="style-like-text"
>
<Option value="udp">UDP</Option>
</Select>
</Form.Item>
</Col>
<Col span={10} style={{ margin: "1px" }}>
<Form.Item
style={{ margin: "1px" }}
name={[field.name, "ip"]}
rules={[{ validator: ipValidator }]}
>
<Input
placeholder="e.g. X.X.X.X"
style={{ width: "100%" }}
autoComplete="off"
/>
</Form.Item>
</Col>
<Col span={4} style={{ textAlign: "center" }}>
<Form.Item
style={{ margin: "1px" }}
name={[field.name, "port"]}
rules={[{ required: true, message: "Missing port" }]}
initialValue={53}
>
<InputNumber placeholder="Port" style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col
span={2}
style={{
textAlign: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MinusCircleOutlined onClick={() => remove(field.name)} />
</Col>
</Row>
);
})}
<Row>
<Col span={20}>
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
style={{
maxWidth: "270px",
marginTop: "5px",
}}
disabled={fields.length > 1}
icon={<PlusOutlined />}
>
Add nameserver
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</Col>
</Row>
</div>
);
// @ts-ignore
const renderDomains = (
fields: FormListFieldData[],
{ add, remove }: any,
{ errors }: any
) => (
<>
<Row>
<Space>
<Col>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Match domains
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-4px",
fontWeight: "400",
marginBottom: "4px",
}}
>
Add domain if you want to have a specific one
</Paragraph>
</Col>
</Space>
</Row>
{setMatchDomains(fields.length)}
{fields.map((field, index) => {
return (
<Row key={index} style={{ marginBottom: "5px" }}>
<Col span={22}>
<Form.Item
style={{ margin: "0" }}
{...field}
rules={[{ validator: domainValidator }]}
>
<Input
placeholder="e.g. example.com"
style={{ width: "100%" }}
autoComplete="off"
/>
</Form.Item>
</Col>
<Col
span={2}
style={{
textAlign: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(field.name)}
/>
</Col>
</Row>
);
})}
<Row>
<Col span={24} style={{ margin: "1px" }}>
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
style={{ marginTop: "5px" }}
>
Add Domain
</Button>
</Form.Item>
</Col>
</Row>
<Form.ErrorList errors={errors} />
</>
);
const handleChangeDisabled = (checked: boolean) => {
setFormNSGroup({
...formNSGroup,
enabled: checked,
});
};
const handleChangeMarkDomain = (checked: boolean) => {
setFormNSGroup({
...formNSGroup,
search_domains_enabled: checked,
});
};
return (
<>
{nsGroup && (
<Modal
forceRender={true}
footer={false}
onCancel={onCancel}
open={setupNewNameServerGroupVisible}
>
<Row gutter={16}>
<Col span={24}>
<Paragraph
style={{
textAlign: "start",
whiteSpace: "pre-line",
fontSize: "18px",
fontWeight: "500",
}}
>
Add nameserver
</Paragraph>
<Paragraph
type={"secondary"}
style={{
textAlign: "start",
whiteSpace: "pre-line",
marginTop: "-20px",
fontSize: "14px",
paddingBottom: "25px",
marginBottom: "4px",
}}
>
Use this nameserver to resolve domains in your network
</Paragraph>
</Col>
</Row>
{selectCustom ? (
<Form
layout="vertical"
requiredMark={false}
form={form}
onValuesChange={onChange}
>
<Row gutter={16}>
<Col span={24}>
<Header
style={{
border: "none",
}}
>
<Row align="top">
<Col flex="none" style={{ display: "flex" }}>
{!editName && !editDescription && formNSGroup.id && (
<button
type="button"
aria-label="Close"
className="ant-drawer-close"
style={{ paddingTop: 3 }}
onClick={onCancel}
>
<span
role="img"
aria-label="close"
className="anticon anticon-close"
>
<CloseOutlined size={16} />
</span>
</button>
)}
</Col>
<Col flex="auto">
{!editName && formNSGroup.id ? (
<div
className={
"access-control input-text ant-drawer-title"
}
onClick={() => toggleEditName(true)}
>
{formNSGroup.id
? formNSGroup.name
: "New nameserver group"}
</div>
) : (
<div style={{ lineHeight: "15px" }}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Name
</label>
<Form.Item
name="name"
rules={[
{
required: true,
message:
"Please add an identifier for this nameserver group",
whitespace: true,
},
{
validator: nameValidator,
},
]}
style={{
marginBottom: "10px",
marginTop: "10px",
}}
>
<Input
placeholder="e.g. Public DNS"
ref={inputNameRef}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)}
autoComplete="off"
maxLength={40}
/>
</Form.Item>
</div>
)}
{!editDescription ? (
<div
className={
"access-control input-text ant-drawer-subtitle"
}
style={{ marginTop: "0" }}
onClick={() => toggleEditDescription(true)}
>
{formNSGroup.description &&
formNSGroup.description.trim() !== ""
? formNSGroup.description
: "Add description"}
</div>
) : (
<div
style={{ lineHeight: "15px", marginTop: "24px" }}
>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Description
</label>
<Form.Item
name="description"
style={{ marginTop: "8px" }}
>
<Input
placeholder="Add description..."
ref={inputDescriptionRef}
onPressEnter={() =>
toggleEditDescription(false)
}
onBlur={() => toggleEditDescription(false)}
autoComplete="off"
/>
</Form.Item>
</div>
)}
</Col>
</Row>
</Header>
</Col>
<Col span={24}>
<Form.List
name="nameservers"
rules={[{ validator: formListValidator }]}
>
{renderNSList}
</Form.List>
</Col>
<Col span={24}>
<Form.List name="domains">{renderDomains}</Form.List>
</Col>
{matchDomains > 0 && (
<Col span={24}>
<Form.Item name="search_domains_enabled" label="">
<div
style={{
display: "flex",
gap: "15px",
}}
>
<Switch
onChange={handleChangeMarkDomain}
size="small"
/>
<div>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Mark match domains as search domains
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "0",
}}
>
E.g., "peer.example.com" will be accessible with
"peer"
</Paragraph>
</div>
</div>
</Form.Item>
</Col>
)}
<Col span={24}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Distribution groups
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-4px",
fontWeight: "400",
marginBottom: "4px",
}}
>
Advertise this route to peers that belong to the following
groups
</Paragraph>
<Form.Item
name="groups"
rules={[{ validator: selectValidator }]}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the NS group"
tagRender={blueTagRender}
onChange={handleChangeTags}
dropdownRender={dropDownRender}
optionFilterProp="serchValue"
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="enabled" label="">
<div
style={{
display: "flex",
gap: "15px",
}}
>
<Switch
onChange={handleChangeDisabled}
defaultChecked={formNSGroup.enabled}
size="small"
/>
<div>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Enabled
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "0",
}}
>
{formNSGroup.enabled
? "Disable this server if you don't want it to apply immediately"
: " Enable this server if you want it to apply immediately"}
</Paragraph>
</div>
</div>
</Form.Item>
</Col>
<Col
span={24}
style={{ marginTop: "10px", marginBottom: "24px" }}
>
<Text type={"secondary"}>
Learn more about
<a
target="_blank"
rel="noreferrer"
href="https://docs.netbird.io/how-to/manage-dns-in-your-network"
>
{" "}
DNS
</a>
</Text>
</Col>
<Col
style={{
width: "100%",
}}
>
<Space
style={{
display: "flex",
justifyContent: "end",
width: "100%",
}}
>
<Button onClick={onCancel} disabled={savedNSGroup.loading}>
Cancel
</Button>
<Button
type="primary"
onClick={handleFormSubmit}
disabled={savedNSGroup.loading}
>
Create nameserver
</Button>
</Space>
</Col>
</Row>
</Form>
) : (
<>
<Space direction={"vertical"} style={{ width: "100%" }}>
<Row align="middle">
<Col span={24} style={{ textAlign: "left" }}>
<span className="ant-form-item font-500">
Select a predefined one
</span>
</Col>
</Row>
<Row align="middle">
<Col span={24} style={{ textAlign: "center" }}>
<Select
style={{ width: "100%" }}
onChange={handleSelectChange}
options={[
{
value: googleChoice,
label: googleChoice,
},
{
value: cloudflareChoice,
label: cloudflareChoice,
},
{
value: quad9Choice,
label: quad9Choice,
},
{
value: customChoice,
label: customChoice,
},
]}
/>
</Col>
</Row>
<Row align="middle">
<Col span={24} style={{ textAlign: "left" }}>
<Col span={24} style={{ textAlign: "left" }}>
<span className="ant-form-item blue-color">
<Typography.Link
onClick={() => handleSelectChange(customChoice)}
>
or create custom
</Typography.Link>
</span>
</Col>
</Col>
</Row>
</Space>
<Space
style={{
display: "flex",
justifyContent: "end",
marginTop: "25px",
}}
>
<Button onClick={onCancel} type="primary">
Cancel
</Button>
</Space>
</>
)}
</Modal>
)}
</>
);
};
export default NameServerGroupAdd;

File diff suppressed because it is too large Load Diff

View File

@@ -1,185 +1,244 @@
import React, {useEffect, useState} from 'react';
import {Link, useLocation} from 'react-router-dom';
import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import logo from "../assets/logo.png";
import {Avatar, Button, Col, Dropdown, Grid, Menu, Row} from 'antd'
import {ItemType} from "antd/lib/menu/hooks/useItems";
import {AvatarSize} from "antd/es/avatar/SizeContext";
import {UserOutlined} from '@ant-design/icons';
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
import {getConfig} from "../config";
import {User} from "../store/user/types";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as userActions} from "../store/user";
import {useGetTokenSilently} from "../utils/token";
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
import { Avatar, Button, Col, Dropdown, Grid, Menu, Row } from "antd";
import { ItemType } from "antd/lib/menu/hooks/useItems";
import { AvatarSize } from "antd/es/avatar/SizeContext";
import { UserOutlined } from "@ant-design/icons";
import { useOidc, useOidcIdToken, useOidcUser } from "@axa-fr/react-oidc";
import { getConfig } from "../config";
import { User } from "../store/user/types";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { actions as userActions } from "../store/user";
import { useGetTokenSilently } from "../utils/token";
import { actions as personalAccessTokenActions } from "../store/personal-access-token";
const {useBreakpoint} = Grid;
const { useBreakpoint } = Grid;
const Navbar = () => {
let location = useLocation();
const config = getConfig();
const { logout } = useOidc();
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
let location = useLocation();
const config = getConfig();
const { logout } = useOidc();
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const {oidcUser} = useOidcUser();
const {idTokenPayload} = useOidcIdToken()
const user = oidcUser;
const [currentUser, setCurrentUser] = useState({} as User)
const { oidcUser } = useOidcUser();
const { idTokenPayload } = useOidcIdToken();
const user = oidcUser;
const [currentUser, setCurrentUser] = useState({} as User);
const screens = useBreakpoint();
const screens = useBreakpoint();
const [hideMenuUser, setHideMenuUser] = useState(false)
const users = useSelector((state: RootState) => state.user.data)
const [isRefreshingUserState, setIsRefreshingUserState] = useState(false)
const [hideMenuUser, setHideMenuUser] = useState(false);
const users = useSelector((state: RootState) => state.user.data);
const [isRefreshingUserState, setIsRefreshingUserState] = useState(false);
const items = [
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
{label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys'},
{label: (<Link to="/acls">Access Control</Link>), key: '/acls'},
{label: (<Link to="/routes">Network Routes</Link>), key: '/routes'},
{ label: (<Link to="/dns">DNS</Link>), key: '/dns' },
{label: (<Link to="/users">Users</Link>), key: '/users'},
{label: (<Link to="/activity">Activity</Link>), key: '/activity'},
{label: (<Link to="/settings">Settings</Link>), key: '/settings'}
] as ItemType[]
const items = [
{ label: <Link data-testid="peers-page" to="/peers">Peers</Link>, key: "/peers" },
{ label: <Link data-testid="setup-keys-page" to="/setup-keys">Setup Keys</Link>, key: "/setup-keys" },
{ label: <Link data-testid="access-control-page" to="/acls">Access Control</Link>, key: "/acls" },
{ label: <Link data-testid="network-routes-page" to="/routes">Network Routes</Link>, key: "/routes" },
{ label: <Link data-testid="dns-page"to="/dns">DNS</Link>, key: "/dns" },
{ label: <Link data-testid="usersf-page" to="/users">Users</Link>, key: "/users" },
{ label: <Link to="/activity">Activity</Link>, key: "/activity" },
{ label: <Link to="/settings">Settings</Link>, key: "/settings" },
] as ItemType[];
const userEmailKey = 'user-email'
const userLogoutKey = 'user-logout'
const userDividerKey = 'user-divider'
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
const [menuItems, setMenuItems] = useState(items)
const logoutWithRedirect = () =>
logout("/", {client_id: config.clientId});
const userEmailKey = "user-email";
const userLogoutKey = "user-logout";
const userDividerKey = "user-divider";
const adminOnlyTabs = [
"/setup-keys",
"/acls",
"/routes",
"/dns",
"/users",
"/activity",
"/settings",
];
const [menuItems, setMenuItems] = useState(items);
const logoutWithRedirect = () => {
let lRemove = [
"peerFilter",
"setupKeysFilter",
"accessControlFilter",
"routesFilter",
"nameServerFilter",
"userFilter",
"serviceUserFilter",
"activityFilter",
];
lRemove.forEach((element) => {
localStorage.removeItem(element);
});
logout("/", { client_id: config.clientId });
};
const openPersonalUserPage = () => {
dispatch(userActions.setUser({
id: currentUser.id,
email: currentUser.email,
role: currentUser.role,
auto_groups: currentUser.auto_groups ? currentUser.auto_groups : [],
name: currentUser.name,
is_current: currentUser.is_current,
is_service_user: currentUser.is_service_user,
} as User));
dispatch(userActions.setUserTabOpen("Users"));
dispatch(userActions.setEditUserPopupVisible(true));
}
useEffect(() => {
const fs = items.filter(m => showTab(m?.key?.toString(), currentUser) && m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
if (screens.xs === true) {
setHideMenuUser(false)
fs.push({type: 'divider', key: userDividerKey})
fs.push({
label: (
<Link to="#">{user?.name}</Link>
),
icon: createAvatar("small"),
key: userEmailKey
})
fs.push({
label: (<Button type="link" block onClick={logoutWithRedirect}>Logout</Button>),
key: userLogoutKey
})
setMenuItems([...fs])
return
}
setMenuItems([...fs])
setHideMenuUser(true)
}, [screens, currentUser])
useEffect(() => {
if (users.length === 0 && !isRefreshingUserState &&
window.location.pathname !== '/peers' &&
window.location.pathname !== '/users') {
setIsRefreshingUserState(true)
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
return
}
if (users.length === 0 && isRefreshingUserState) {
return
}
let runUser = oidcUser
if (!oidcUser) {
runUser = idTokenPayload
}
setIsRefreshingUserState(false)
if (runUser) {
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
if (found) {
setCurrentUser(found)
}
}
}, [users, oidcUser])
const showTab = (key: string | undefined, user: User | undefined) => {
if (!user) {
return false
}
if (user.role?.toLowerCase() === "admin") {
return true
}
return !adminOnlyTabs.find(t => t === key)
}
const menuUser = (
<Menu
items={[
{
label: (<Link to="/users" onClick={openPersonalUserPage}>{user?.email}</Link>),
key: '0',
},
{
label: (<Link to="/logout" onClick={logoutWithRedirect}>Logout</Link>),
key: '1',
}
]}
/>
const openPersonalUserPage = () => {
dispatch(
userActions.setUser({
id: currentUser.id,
email: currentUser.email,
role: currentUser.role,
auto_groups: currentUser.auto_groups ? currentUser.auto_groups : [],
name: currentUser.name,
is_current: currentUser.is_current,
is_service_user: currentUser.is_service_user,
} as User)
);
dispatch(userActions.setUserTabOpen("Users"));
dispatch(userActions.setEditUserPopupVisible(true));
};
const createAvatar = (size: AvatarSize) => {
return user?.picture ? (
<Avatar size={size} src={user?.picture} icon={<UserOutlined/>}/>
) : (
<Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
)
useEffect(() => {
const fs = items.filter(
(m) =>
showTab(m?.key?.toString(), currentUser) &&
m?.key !== userEmailKey &&
m?.key !== userLogoutKey &&
m?.key !== userDividerKey
);
if (screens.xs === true) {
setHideMenuUser(false);
fs.push({ type: "divider", key: userDividerKey });
fs.push({
label: <Link to="#">{user?.name}</Link>,
icon: createAvatar("small"),
key: userEmailKey,
});
fs.push({
label: (
<Button type="link" block onClick={logoutWithRedirect}>
Logout
</Button>
),
key: userLogoutKey,
});
setMenuItems([...fs]);
return;
}
setMenuItems([...fs]);
setHideMenuUser(true);
}, [screens, currentUser]);
useEffect(() => {
if (
users.length === 0 &&
!isRefreshingUserState &&
window.location.pathname !== "/peers" &&
window.location.pathname !== "/users"
) {
setIsRefreshingUserState(true);
dispatch(
userActions.getUsers.request({
getAccessTokenSilently: getTokenSilently,
payload: null,
})
);
return;
}
if (users.length === 0 && isRefreshingUserState) {
return;
}
let runUser = oidcUser;
if (!oidcUser) {
runUser = idTokenPayload;
}
setIsRefreshingUserState(false);
if (runUser) {
const found = users.find((u) =>
u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false
);
if (found) {
setCurrentUser(found);
}
}
}, [users, oidcUser]);
const showTab = (key: string | undefined, user: User | undefined) => {
if (!user) {
return false;
}
return (
<>
<Row justify="space-evenly" align="middle">
<Col flex="0 1 60px">
<Link id="logo" to="/">
<img
alt="logo"
style={{width: "55px"}}
src={logo}
/>
</Link>
</Col>
<Col flex="1 1 auto">
<div>
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]}
onSelect={(e) => {
dispatch(userActions.setUser(null as unknown as User));
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
}}
defaultSelectedKeys={[location.pathname]} items={menuItems}/>
</div>
</Col>
{hideMenuUser &&
<Col>
<Dropdown overlay={menuUser} placement="bottomRight" trigger={['click']}>
{createAvatar("large")}
</Dropdown>
</Col>
}
</Row>
</>
if (user.role?.toLowerCase() === "admin" || user.role?.toLowerCase() === "owner") {
return true;
}
return !adminOnlyTabs.find((t) => t === key);
};
const menuUser = (
<Menu
items={[
{
label: (
<Link to="/users" onClick={openPersonalUserPage}>
{user?.email}
</Link>
),
key: "0",
},
{
label: (
<Link to="/logout" onClick={logoutWithRedirect}>
Logout
</Link>
),
key: "1",
},
]}
/>
);
const createAvatar = (size: AvatarSize) => {
return user?.picture ? (
<Avatar size={size} src={user?.picture} icon={<UserOutlined />} />
) : (
<Avatar size={size}>
{(user?.name || "").slice(0, 1).toUpperCase()}
</Avatar>
);
};
return (
<>
<Row justify="space-evenly" align="middle">
<Col flex="0 1 60px">
<Link id="logo" to="/">
<img alt="logo" style={{ width: "55px" }} src={logo} />
</Link>
</Col>
<Col flex="1 1 auto">
<div>
<Menu
mode="horizontal"
selectable={true}
selectedKeys={[location.pathname]}
onSelect={(e) => {
dispatch(userActions.setUser(null as unknown as User));
dispatch(
personalAccessTokenActions.resetPersonalAccessTokens(null)
);
}}
defaultSelectedKeys={[location.pathname]}
items={menuItems}
/>
</div>
</Col>
{hideMenuUser && (
<Col>
<Dropdown
overlay={menuUser}
placement="bottomRight"
trigger={["click"]}
>
{createAvatar("large")}
</Dropdown>
</Col>
)}
</Row>
</>
);
};
export default Navbar;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,740 @@
import React, { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { actions as routeActions } from "../store/route";
import {
Button,
Col,
Collapse,
Form,
Input,
InputNumber,
message,
Row,
Select,
SelectProps,
Breadcrumb,
Switch,
Card,
Typography,
Tabs,
} from "antd";
import type { TabsProps } from "antd";
import { Route, RouteToSave } from "../store/route/types";
import { Header } from "antd/es/layout/layout";
import { RuleObject } from "antd/lib/form";
import { isEmpty } from "lodash";
import {
initPeerMaps,
peerToPeerIP,
routePeerSeparator,
} from "../utils/routes";
import { useGetTokenSilently } from "../utils/token";
import { useGetGroupTagHelpers } from "../utils/groups";
import { Container } from "./Container";
const { Paragraph, Text } = Typography;
const { Panel } = Collapse;
interface FormRoute extends Route {}
const RoutePeerUpdate = () => {
const {
blueTagRender,
handleChangeTags,
dropDownRender,
optionRender,
tagGroups,
getExistingAndToCreateGroupsLists,
getGroupNamesFromIDs,
selectValidator,
} = useGetGroupTagHelpers();
const { Option } = Select;
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const peers = useSelector((state: RootState) => state.peer.data);
const route = useSelector((state: RootState) => state.route.route);
const routes = useSelector((state: RootState) => state.route.data);
const savedRoute = useSelector((state: RootState) => state.route.savedRoute);
const [editDescription, setEditDescription] = useState(false);
const options: SelectProps["options"] = [];
const [formRoute, setFormRoute] = useState({} as FormRoute);
const [form] = Form.useForm();
const inputDescriptionRef = useRef<any>(null);
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
const [activeTab, setActiveTab] = useState<string | null>(null);
useEffect(() => {
if (!isEmpty(formRoute))
if (formRoute.peer_groups) {
setActiveTab("groupOfPeers");
} else {
setActiveTab("routingPeer");
}
}, [formRoute]);
useEffect(() => {
if (editDescription)
inputDescriptionRef.current!.focus({
cursor: "end",
});
}, [editDescription]);
useEffect(() => {
if (!route) return;
const fRoute = {
...route,
groups: route.groups,
} as FormRoute;
setFormRoute(fRoute);
form.setFieldsValue(fRoute);
// let options = [];
}, [route]);
peers.forEach((p) => {
let os: string;
os = p.os;
if (
!os.toLowerCase().startsWith("darwin") &&
!os.toLowerCase().startsWith("windows") &&
!os.toLowerCase().startsWith("android") &&
!os.toLowerCase().startsWith("ios") &&
route &&
!routes
.filter((r) => r.network_id === route.network_id)
.find((r) => r.peer === p.id)
) {
options?.push({
label: peerToPeerIP(p.name, p.ip),
value: peerToPeerIP(p.name, p.ip),
disabled: false,
});
}
});
const handleSingleChangeTags = (values: any) => {
const lastValue = values[values.length - 1];
if (values.length > 0) {
form.setFieldsValue({
peer_groups: [lastValue],
});
}
};
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
if (inputRoute.peer_groups) {
inputRoute = {
...inputRoute,
peer_groups: [
inputRoute.peer_groups[inputRoute.peer_groups.length - 1],
],
};
}
let peerIDList = inputRoute.peer.split(routePeerSeparator);
let peerID: string;
if (peerIDList.length === 1) {
peerID = inputRoute.peer;
} else {
if (peerIDList[1]) {
peerID = peerIPToID[peerIDList[1]];
} else {
peerID = peerIPToID[peerNameToIP[inputRoute.peer]];
}
}
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
inputRoute.groups
);
const payload = {
id: inputRoute.id,
network: inputRoute.network,
network_id: inputRoute.network_id,
description: inputRoute.description,
enabled: inputRoute.enabled,
masquerade: inputRoute.masquerade,
metric: inputRoute.metric,
groups: existingGroups,
groupsToCreate: groupsToCreate,
} as RouteToSave;
if (activeTab === "routingPeer") {
let pay = { ...payload, peer: peerID };
return pay;
}
if (activeTab === "groupOfPeers") {
if (inputRoute.peer_groups) {
let [currentPeersGroup, peerGroupsToCreate] =
getExistingAndToCreateGroupsLists(inputRoute.peer_groups);
let pay = {
...payload,
peer_groups: currentPeersGroup,
peerGroupsToCreate: peerGroupsToCreate,
};
return pay;
}
}
return payload;
};
const handleFormSubmit = () => {
form
.validateFields()
.then(() => {
if (formRoute.peer !== "") {
const routeToSave = createRouteToSave(formRoute);
dispatch(
routeActions.saveRoute.request({
getAccessTokenSilently: getTokenSilently,
payload: routeToSave,
})
);
}
})
.catch((errorInfo) => {
console.log("errorInfo", errorInfo);
});
};
const setVisibleNewRoute = (status: boolean) => {
dispatch(routeActions.setSetupEditRoutePeerVisible(status));
};
const onCancel = () => {
if (savedRoute.loading) return;
dispatch(
routeActions.setRoute({
network: "",
network_id: "",
description: "",
peer: "",
metric: 9999,
masquerade: false,
enabled: true,
groups: [],
peer_groups: [],
} as Route)
);
setVisibleNewRoute(false);
};
const onChange = (data: any) => {
setFormRoute({ ...formRoute, ...data });
};
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
const toggleEditDescription = (status: boolean) => {
setEditDescription(status);
};
const peerValidator = (_: RuleObject, value: string) => {
if (value == "") {
return Promise.reject(new Error("Please select routing one peer"));
}
return Promise.resolve();
};
const selectPreValidator = (obj: RuleObject, value: string[]) => {
if (formRoute.peer === "") {
let [, newGroups] = getExistingAndToCreateGroupsLists(value);
if (newGroups.length > 0) {
return Promise.reject(
new Error(
"You can't add new Groups from the group update view, please remove:\"" +
newGroups +
'"'
)
);
}
}
return selectValidator(obj, value);
};
const handleMasqueradeChange = (checked: boolean) => {
setFormRoute({
...formRoute,
masquerade: checked,
});
};
const handleEnableChange = (checked: boolean) => {
setFormRoute({
...formRoute,
enabled: checked,
});
};
const onBreadcrumbUsersClick = () => {
onCancel();
};
const styleNotification = { marginTop: 85 };
const saveKey = "saving";
useEffect(() => {
if (savedRoute.loading) {
message.loading({
content: "Saving...",
key: saveKey,
duration: 0,
style: styleNotification,
});
} else if (savedRoute.success) {
message.success({
content: "Route has been successfully updated.",
key: saveKey,
duration: 2,
style: styleNotification,
});
dispatch(routeActions.setSetupNewRouteVisible(false));
dispatch(routeActions.setSetupEditRouteVisible(false));
dispatch(routeActions.setSetupEditRoutePeerVisible(false));
dispatch(routeActions.setSavedRoute({ ...savedRoute, success: false }));
dispatch(routeActions.resetSavedRoute(null));
} else if (savedRoute.error) {
let errorMsg = "Failed to update network route";
switch (savedRoute.error.statusCode) {
case 403:
errorMsg =
"Failed to update network route. You might not have enough permissions.";
break;
default:
errorMsg = savedRoute.error.data.message
? savedRoute.error.data.message
: errorMsg;
break;
}
message.error({
content: errorMsg,
key: saveKey,
duration: 5,
style: styleNotification,
});
dispatch(routeActions.setSavedRoute({ ...savedRoute, error: null }));
dispatch(routeActions.resetSavedRoute(null));
}
}, [savedRoute]);
const onTabChange = (key: string) => {
console.log(key);
// setActiveTab(key);
};
const items: TabsProps["items"] = [
{
key: "routingPeer",
label: "Routing Peer",
disabled: true,
children: (
<>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Assign a peer as a routing peer for the Network CIDR
</Paragraph>
<Form.Item
name="peer"
rules={[
{
required: true,
message: "Please select routing one peer",
},
]}
style={{ maxWidth: "400px" }}
>
<Select
showSearch
style={{ width: "100%" }}
placeholder="Select Peer"
dropdownRender={peerDropDownRender}
options={options}
allowClear={true}
/>
</Form.Item>
</>
),
},
{
key: "groupOfPeers",
label: "Peer group",
disabled: true,
children: (
<>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Assign peer group with Linux machines to be used as routing peers
</Paragraph>
<Form.Item
name="peer_groups"
rules={[
{
required: true,
message: "Please select peer group",
},
]}
>
<Select
mode="tags"
style={{ maxWidth: "400px" }}
tagRender={blueTagRender}
onChange={handleSingleChangeTags}
dropdownRender={dropDownRender}
optionFilterProp="serchValue"
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</>
),
},
];
return (
<>
<Container style={{ paddingTop: "40px", paddingBottom: "50px" }}>
<Breadcrumb
style={{ marginBottom: "25px" }}
items={[
{
title: <a onClick={onBreadcrumbUsersClick}>Network Routes</a>,
},
{
title: formRoute.network_id,
},
]}
/>
{route && (
<Card>
<Form
layout="vertical"
form={form}
requiredMark={false}
onValuesChange={onChange}
style={{ width: "100%", maxWidth: "600px" }}
className="route-form edit-form-wrapper"
>
<Row gutter={16}>
<Col span={24}>
<Header
style={{
border: "none",
}}
>
<Row align="top">
<Col span={24} style={{ lineHeight: "20px" }}>
<div
style={{
color: "rgba(0, 0, 0, 0.88)",
fontWeight: "500",
fontSize: "22px",
}}
>
{formRoute.network_id}
<Paragraph
type={"secondary"}
style={{
textAlign: "left",
whiteSpace: "pre-line",
fontWeight: "400",
marginBottom: "0",
}}
>
<div style={{ margin: "5px 0" }}>
{" "}
{formRoute.network}
</div>
<div></div>
</Paragraph>
</div>
{!editDescription ? (
<div
onClick={() => toggleEditDescription(true)}
style={{
margin: "0 0 30px",
lineHeight: "22px",
cursor: "pointer",
}}
>
{formRoute.description &&
formRoute.description.trim() !== "" ? (
formRoute.description
) : (
<span style={{ textDecoration: "underline" }}>
Add description
</span>
)}
</div>
) : (
<Form.Item
name="description"
label="Description"
style={{ marginTop: 24, fontWeight: 500 }}
>
<Input
placeholder="Add description..."
ref={inputDescriptionRef}
onPressEnter={() => toggleEditDescription(false)}
onBlur={() => toggleEditDescription(false)}
autoComplete="off"
style={{ maxWidth: "400px" }}
/>
</Form.Item>
)}
</Col>
</Row>
<Row align="top">
<Col flex="auto"></Col>
</Row>
</Header>
</Col>
<Col span={24}>
<Form.Item name="enabled" label="">
<div
style={{
display: "flex",
gap: "15px",
}}
>
<Switch
size={"small"}
checked={formRoute.enabled}
onChange={handleEnableChange}
/>
<div>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Enabled
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "0",
}}
>
You can enable or disable the route
</Paragraph>
</div>
</div>
</Form.Item>
</Col>
<Col span={24}>
{activeTab && (
<Tabs
defaultActiveKey={activeTab}
items={items}
onChange={onTabChange}
/>
)}
</Col>
<Col span={24}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Distribution groups
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Advertise this route to peers that belong to the following
groups
</Paragraph>
<Form.Item
name="groups"
label=""
rules={[{ validator: selectPreValidator }]}
style={{ maxWidth: "400px" }}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the network route"
tagRender={blueTagRender}
onChange={handleChangeTags}
dropdownRender={dropDownRender}
optionFilterProp="serchValue"
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Collapse
onChange={onChange}
bordered={false}
ghost={true}
style={{ padding: "0" }}
className="remove-bg"
>
<Panel
key="0"
header={
<Paragraph
style={{
textAlign: "left",
whiteSpace: "pre-line",
fontSize: "14px",
fontWeight: "400",
margin: "0",
textDecoration: "underline",
}}
>
More settings
</Paragraph>
}
className="system-info-panel"
>
<Row gutter={16} style={{ padding: "15px 0 0" }}>
<Col span={12}>
<Form.Item name="masquerade" label="">
<div
style={{
display: "flex",
gap: "15px",
}}
>
<Switch
size={"small"}
checked={formRoute.masquerade}
onChange={handleMasqueradeChange}
/>
<div>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Masquerade
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "0",
}}
>
Allow access to your private networks without
configuring routes on your local routers or
other devices.
</Paragraph>
</div>
</div>
</Form.Item>
</Col>
<Col span={12}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Metric
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Lower metrics indicating higher priority routes
</Paragraph>
<Row>
<Col span={12}>
<Form.Item name="metric" label="">
<InputNumber
min={1}
max={9999}
autoComplete="off"
className="w-100"
/>
</Form.Item>
</Col>
</Row>
</Col>
</Row>
</Panel>
</Collapse>
</Col>
</Row>
<Col
span={24}
style={{
display: "flex",
justifyContent: "start",
gap: "10px",
marginTop: "20px",
}}
>
<Button onClick={onCancel} disabled={savedRoute.loading}>
Cancel
</Button>
<Button
type="primary"
disabled={savedRoute.loading}
onClick={handleFormSubmit}
>
Save
</Button>
</Col>
</Form>
</Card>
)}
</Container>
</>
);
};
export default RoutePeerUpdate;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,514 @@
import React, { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { actions as setupKeyActions } from "../store/setup-key";
import {
Button,
Col,
Divider,
Form,
Input,
Row,
Select,
Breadcrumb,
Tag,
Typography,
Card,
Tooltip,
} from "antd";
import { RootState } from "typesafe-actions";
import {
FormSetupKey,
SetupKey,
SetupKeyToSave,
} from "../store/setup-key/types";
import { formatDate } from "../utils/common";
import { RuleObject } from "antd/lib/form";
import { Group } from "../store/group/types";
import { useGetTokenSilently } from "../utils/token";
import moment from "moment";
import { Container } from "./Container";
import Paragraph from "antd/es/typography/Paragraph";
import { LockOutlined } from "@ant-design/icons";
import { actions as personalAccessTokenActions } from "../store/personal-access-token";
import { useGetGroupTagHelpers } from "../utils/groups";
const { Option } = Select;
const { Text } = Typography;
const customExpiresFormat = (value: Date): string | null => {
return formatDate(value);
};
const SetupKeyNew = (props: any) => {
const { isGroupUpdateView, setShowGroupModal } = props;
const {
optionRender,
blueTagRender,
tagGroups,
getExistingAndToCreateGroupsLists,
setGroupTagFilterAll,
} = useGetGroupTagHelpers();
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey);
const savedSetupKey = useSelector(
(state: RootState) => state.setupKey.savedSetupKey
);
const groups = useSelector((state: RootState) => state.group.data);
const [form] = Form.useForm();
const [editName, setEditName] = useState(false);
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey);
const inputNameRef = useRef<any>(null);
useEffect(() => {
setGroupTagFilterAll(true);
}, []);
useEffect(() => {
//Unmounting component clean
return () => {
setVisibleNewSetupKey(false);
};
}, []);
useEffect(() => {
if (!editName) return;
inputNameRef.current!.focus({ cursor: "end" });
}, [editName]);
useEffect(() => {
if (!setupKey) return;
const allGroups = new Map<string, Group>();
let formKeyGroups: string[] = [];
groups.forEach((g) => allGroups.set(g.id!, g));
if (setupKey.auto_groups) {
formKeyGroups = setupKey.auto_groups
.filter((g) => allGroups.get(g))
.map((g) => allGroups.get(g)!.name);
}
const fSetupKey = {
...setupKey,
autoGroupNames: setupKey.auto_groups || [],
exp: moment(setupKey.expires),
last: moment(setupKey.last_used),
} as FormSetupKey;
form.setFieldsValue(fSetupKey);
setFormSetupKey(fSetupKey);
}, [setupKey]);
const createSetupKeyToSave = (): SetupKeyToSave => {
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
formSetupKey.autoGroupNames
);
const expiresIn = formSetupKey.expires_in * 24 * 3600; // the api expects seconds while the form returns days
return {
id: formSetupKey.id,
name: formSetupKey.name,
type: formSetupKey.type,
auto_groups: existingGroups,
revoked: formSetupKey.revoked,
groupsToCreate: groupsToCreate,
expires_in: expiresIn,
usage_limit: formSetupKey.usage_limit,
ephemeral: formSetupKey.ephemeral,
} as SetupKeyToSave;
};
const handleFormSubmit = async () => {
try {
await form.validateFields();
} catch (e) {
const errorFields = (e as any).errorFields;
}
const setupKeyToSave = createSetupKeyToSave();
dispatch(
setupKeyActions.saveSetupKey.request({
getAccessTokenSilently: getTokenSilently,
payload: setupKeyToSave,
})
);
};
const setVisibleNewSetupKey = (status: boolean) => {
form.resetFields();
dispatch(setupKeyActions.setSetupEditKeyVisible(status));
};
const onCancel = () => {
if (savedSetupKey.loading) return;
dispatch(
setupKeyActions.setSetupKey({
name: "",
type: "one-off",
key: "",
last_used: "",
expires: "",
state: "valid",
auto_groups: [] as string[],
usage_limit: 0,
used_times: 0,
expires_in: 0,
} as SetupKey)
);
setFormSetupKey({} as FormSetupKey);
setVisibleNewSetupKey(false);
if (setShowGroupModal) {
setShowGroupModal(false);
}
};
const onChange = (data: any) => {
setFormSetupKey({ ...formSetupKey, ...data });
};
const toggleEditName = (status: boolean) => {
setEditName(status);
};
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = [];
value.forEach(function (v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v);
}
});
if (hasSpaceNamed.length) {
return Promise.reject(
new Error("Group names with just spaces are not allowed")
);
}
return Promise.resolve();
};
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{ margin: "8px 0" }} />
<Row style={{ padding: "0 8px 4px" }}>
<Col flex="auto">
<span style={{ color: "#9CA3AF" }}>
Add new group by pressing "Enter"
</span>
</Col>
<Col flex="none">
<svg
width="14"
height="12"
viewBox="0 0 14 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"
/>
</svg>
</Col>
</Row>
</>
);
const changesDetected = (): boolean => {
return (
formSetupKey.name == null ||
formSetupKey.name !== setupKey.name ||
groupsChanged() ||
formSetupKey.usage_limit !== setupKey.usage_limit
);
};
const groupsChanged = (): boolean => {
if (
setupKey &&
setupKey.auto_groups &&
formSetupKey.autoGroupNames.length !== setupKey.auto_groups.length
) {
return true;
}
const formGroupIds =
groups
?.filter((g) => formSetupKey.autoGroupNames.includes(g.name))
.map((g) => g.id || "") || [];
return (
setupKey.auto_groups?.filter((g) => !formGroupIds.includes(g)).length > 0
);
};
const getFormKey = (key: string) => {
if (key) return key.substring(0, 4).concat("****");
};
const onBreadcrumbUsersClick = () => {
if (savedSetupKey.loading) return;
// dispatch(userActions.setUser(null as unknown as User));
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null));
setVisibleNewSetupKey(false);
};
return (
<>
{!isGroupUpdateView && (
<Breadcrumb
style={{ marginBottom: "25px" }}
items={[
{
title: <a onClick={onBreadcrumbUsersClick}>Setup Keys</a>,
},
{
title: setupKey.name,
},
]}
/>
)}
<Card
bordered={true}
className={isGroupUpdateView ? " noborderPadding" : ""}
style={{ marginBottom: "7px", border: "none" }}
>
<div style={{ maxWidth: "800px" }}>
{!isGroupUpdateView && (
<h3
style={{
fontSize: "22px",
fontWeight: "500",
marginBottom: "30px",
}}
>
{setupKey.name}
</h3>
)}
<Form
layout="vertical"
requiredMark={false}
form={form}
onValuesChange={onChange}
initialValues={{
usage_limit: 1,
}}
>
{!isGroupUpdateView && (
<Row style={{ marginTop: "10px" }}>
<Col
sm={24}
md={8}
lg={8}
style={{
paddingRight: "70px",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
fontWeight: "500",
margin: 0,
}}
>
Key
<Tag
color={`${
formSetupKey.state === "valid" ? "green" : "red"
}`}
style={{
marginLeft: "10px",
borderRadius: "2px",
fontWeight: "500",
}}
>
{formSetupKey.state}
</Tag>
</Paragraph>
<Input
style={{ marginTop: "8px" }}
disabled
value={getFormKey(formSetupKey.key)}
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
/>
</Col>
<Col
sm={24}
md={8}
lg={6}
style={{
paddingRight: "70px",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
></Paragraph>
Type{" "}
{formSetupKey.ephemeral ? (
<Tooltip title="Peers that are offline for over 10 minutes will be removed automatically">
<Tag>
<Text type="secondary" style={{ fontSize: 10 }}>
ephemeral
</Text>
</Tag>
</Tooltip>
) : (
" "
)}
</Paragraph>
<Col>
<Input
disabled
value={
formSetupKey.type === "one-off" ? "One-off" : "Reusable"
}
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
style={{ marginTop: "8px" }}
/>
</Col>
</Col>
<Col sm={24} md={8} lg={3}>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
></Paragraph>
{/* {formSetupKey.type === "one-off" ? "One-off" : "Reusable"}, */}
Available uses
</Paragraph>
<Col>
<Input
disabled
value={
formSetupKey.type === "reusable" &&
formSetupKey.usage_limit === 0
? "unlimited"
: formSetupKey.usage_limit - formSetupKey.used_times
}
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
style={{ marginTop: "8px" }}
/>
</Col>
</Col>
</Row>
)}
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
{!isGroupUpdateView && (
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Paragraph style={{ margin: 0, fontWeight: "500" }}>
Expires
</Paragraph>
<Row>
<Input
style={{ marginTop: "8px" }}
disabled
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
value={
customExpiresFormat(new Date(formSetupKey.expires))!
}
/>
</Row>
</Col>
)}
</Row>
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
<Col
xs={24}
sm={24}
md={!isGroupUpdateView ? 11 : 24}
lg={!isGroupUpdateView ? 11 : 24}
xl={!isGroupUpdateView ? 11 : 24}
xxl={!isGroupUpdateView ? 11 : 24}
span={!isGroupUpdateView ? 11 : 24}
style={{
paddingRight: `${!isGroupUpdateView ? "70px" : "0"}`,
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
Auto-assigned groups
</Paragraph>
<Col span={24}>
<Form.Item
style={{ marginTop: "8px", marginBottom: 0 }}
name="autoGroupNames"
rules={[{ validator: selectValidator }]}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the key"
tagRender={blueTagRender}
dropdownRender={dropDownRender}
optionFilterProp="searchValue"
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Col>
</Row>
</Form>
</div>
<Container
style={{
display: "flex",
flexDirection: "row",
justifyContent: `${!isGroupUpdateView ? "start" : "end"}`,
padding: 0,
gap: "10px",
marginTop: "24px",
}}
key={0}
>
<Button onClick={onCancel}>Cancel</Button>
<Button
type="primary"
disabled={savedSetupKey.loading || !changesDetected()}
onClick={handleFormSubmit}
>
{`${formSetupKey.id ? "Save" : "Create"} key`}
</Button>
</Container>
</Card>
</>
);
};
export default SetupKeyNew;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
import React, { useEffect, useState } from "react";
import { Modal } from "antd";
import SetupKeyNew from "./SetupKeyEdit";
export const UpdateKeyGroupModal = (props:any) => {
return (
<>
<Modal
closable={false}
open={true}
footer={[]}
onCancel={() => props.setShowGroupModal(false)}
width={450}
>
<SetupKeyNew
isGroupUpdateView={true}
setShowGroupModal={props.setShowGroupModal}
/>
</Modal>
</>
);
};

View File

@@ -0,0 +1,21 @@
import React, { useEffect, useState } from "react";
import { Modal } from "antd";
import NameServerGroupUpdate from "./NameServerGroupUpdate";
export const UpdateNameServerGroupModal = (props:any) => {
return (
<>
<Modal
closable={false}
open={true}
footer={[]}
onCancel={() => props.setShowGroupModal(false)}
width={450}
>
<NameServerGroupUpdate
isGroupUpdateView={true}
setShowGroupModal={props.setShowGroupModal}
/>
</Modal>
</>
);
};

View File

@@ -0,0 +1,21 @@
import React, { useEffect, useState } from "react";
import { Modal } from "antd";
import PeerUpdate from "./PeerUpdate";
export const UpdatePeerGroupModal = (props:any) => {
return (
<>
<Modal
closable={false}
open={true}
footer={[]}
onCancel={() => props.setShowGroupModal(false)}
width={450}
>
<PeerUpdate
isGroupUpdateView={true}
setShowGroupModal={props.setShowGroupModal}
/>
</Modal>
</>
);
};

View File

@@ -0,0 +1,24 @@
import React, { useEffect, useState } from "react";
import { Modal } from "antd";
import UserEdit from "./UserEdit";
export const UpdateUsersGroupModal = (props: any) => {
return (
<>
<Modal
closable={false}
open={true}
footer={[]}
onCancel={() => props.setShowGroupModal(false)}
width={450}
>
{props.showGroupModal && (
<UserEdit
isGroupUpdateView={true}
setShowGroupModal={props.setShowGroupModal}
/>
)}
</Modal>
</>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -98,22 +98,6 @@ const AddPATPopup = () => {
}
}, [savedPersonalAccessToken])
useEffect(() => {
const keyDownHandler = (event: any) => {
console.log('User pressed: ', event.key);
if (event.key === 'Enter') {
event.preventDefault();
handleFormSubmit();
}
};
document.addEventListener('keydown', keyDownHandler);
return () => {
document.removeEventListener('keydown', keyDownHandler);
};
}, []);
return (
<>
@@ -130,27 +114,25 @@ const AddPATPopup = () => {
}
width={460}
>
<Container style={{textAlign: "start"}}>
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
{showPlainToken ? "Token created successfully!" : "Create a Personal Access Token"}
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "18px",fontWeight:"500"}}>
{showPlainToken ? "Token created successfully!" : "Create token"}
</Paragraph>
{!showPlainToken && <Paragraph type={"secondary"}
style={{
textAlign: "start",
fontSize: "14px",
whiteSpace: "pre-line",
marginTop: "-23px",
paddingBottom: "25px",
}}>
{"This token can be used to authenticate against" + "\n" + "NetBird's Public API."}
{"Use this token to access NetBird's public API"}
</Paragraph>}
{showPlainToken && <Paragraph type={"secondary"} style={{
textAlign: "start",
fontSize: "14px",
whiteSpace: "pre-line",
marginTop: "40px",
}}>{"This token will not be shown again, so be sure to copy it and" + "\n" + "store in a secure location."}</Paragraph>}
marginTop: "25px",
}}>{"This token won't be shown again, so be sure to copy and store it in a secure location"}</Paragraph>}
{!showPlainToken && <Form layout="vertical" hideRequiredMark form={form}
initialValues={{
expires_in: ExpiresInDefault,
@@ -160,14 +142,14 @@ const AddPATPopup = () => {
<Col span={24}>
<Row align="top">
<Col flex="auto">
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "-8px"}}>Token name</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Create a name to identify the token easily</Paragraph>
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set an easily identifiable name for your token</Paragraph>
<Form.Item
name="name"
style={{marginTop: "-10px"}}
rules={[{
required: true,
message: 'Please add a name for this personal access token',
message: 'Please add a name for this token',
whitespace: true
}]}
>
@@ -179,9 +161,9 @@ const AddPATPopup = () => {
</Col>
</Row>
</Col>
<Col span={24} style={{textAlign: "left"}}>
<Paragraph style={{fontSize: "16px", fontWeight: "500"}}>Expires in</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Number of days this token is valid for</Paragraph>
<Col span={24} style={{textAlign: "left", marginTop: "10px"}}>
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Expires in</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Number of days this token will be valid for</Paragraph>
<Form.Item
name="expires_in"
style={{marginTop: "-10px"}}
@@ -195,10 +177,19 @@ const AddPATPopup = () => {
</Form.Item>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Should be between 1 and 365 days</Paragraph>
</Col>
{/*<Col span={24}>*/}
{/* <Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}*/}
{/* href="https://netbird.io/docs/overview/personal-access-tokens">Learn more about personal access tokens</Button>*/}
{/*</Col>*/}
<Col span={24} style={{marginTop: "15px"}}>
<Text type={"secondary"}>
Learn more about
<a
target="_blank"
rel="noreferrer"
href="https://docs.netbird.io/how-to/access-netbird-public-api"
>
{" "}
access tokens
</a>
</Text>
</Col>
</Row>
</Form>}
{showPlainToken &&

View File

@@ -1,292 +1,362 @@
import {
Button,
Col,
Divider,
Form,
Input,
Modal,
Row,
Select,
Space,
Tag,
Typography
Button,
Col,
Divider,
Form,
Input,
Modal,
Row,
Select,
Space,
Tag,
Typography,
} from "antd";
import {Container} from "../Container";
import {CloseOutlined} from "@ant-design/icons";
import React, {useEffect, useRef, useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {useGetTokenSilently} from "../../utils/token";
import {actions as userActions} from "../../store/user";
import {actions as groupActions} from "../../store/group";
import {User, UserToSave} from "../../store/user/types";
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import { Container } from "../Container";
import { CloseOutlined } from "@ant-design/icons";
import React, { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { useGetTokenSilently } from "../../utils/token";
import { actions as userActions } from "../../store/user";
import { actions as groupActions } from "../../store/group";
import { User, UserToSave } from "../../store/user/types";
import { Header } from "antd/es/layout/layout";
import { RuleObject } from "antd/lib/form";
import { CustomTagProps } from "rc-select/lib/BaseSelect";
const {Title, Text, Paragraph} = Typography;
const {Option} = Select;
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
const AddServiceUserPopup = () => {
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const groups = useSelector((state: RootState) => state.group.data)
const users = useSelector((state: RootState) => state.user.data)
const groups = useSelector((state: RootState) => state.group.data);
const users = useSelector((state: RootState) => state.user.data);
const user = useSelector((state: RootState) => state.user.user)
const failed = useSelector((state: RootState) => state.user.failed);
const loading = useSelector((state: RootState) => state.user.loading);
const addServiceUserModalOpen = useSelector((state: RootState) => state.user.addServiceUserPopupVisible)
const savedUser = useSelector((state: RootState) => state.user.savedUser)
const user = useSelector((state: RootState) => state.user.user);
const failed = useSelector((state: RootState) => state.user.failed);
const loading = useSelector((state: RootState) => state.user.loading);
const addServiceUserModalOpen = useSelector(
(state: RootState) => state.user.addServiceUserPopupVisible
);
const savedUser = useSelector((state: RootState) => state.user.savedUser);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const [form] = Form.useForm();
const inputNameRef = useRef<any>(null);
const [form] = Form.useForm()
const inputNameRef = useRef<any>(null)
const [tagGroups, setTagGroups] = useState([] as string[]);
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
const [tagGroups, setTagGroups] = useState([] as string[])
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
const createUserToSave = (values: any): UserToSave => {
const autoGroups = groups?.filter(g => values.autoGroupsNames && values.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
// find groups that do not yet exist (newly added by the user)
const allGroupsNames: string[] = groups?.map(g => g.name);
const groupsToCreate = values.autoGroupsNames?.filter((s: string) => !allGroupsNames.includes(s)) || []
return {
id: values.id,
role: values.role,
name: values.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
is_service_user: true
} as UserToSave
}
const onCancel = () => {
if (savedUser.loading) return
dispatch(userActions.setUser(null as unknown as User));
form.resetFields();
dispatch(userActions.setAddServiceUserPopupVisible(false));
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
let userToSave = createUserToSave(values)
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}))
form.resetFields();
dispatch(userActions.getServiceUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
dispatch(userActions.setAddServiceUserPopupVisible(false));
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = []
if (!value) {
return Promise.resolve()
}
value.forEach(function (v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v)
}
})
if (hasSpaceNamed.length) {
return Promise.reject(new Error("Group names with just spaces are not allowed"))
}
return Promise.resolve()
}
const tagRender = (props: CustomTagProps) => {
const {label, value, closable, onClose} = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
return (
<Tag
color="blue"
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{marginRight: 3}}
>
<strong>{value}</strong>
</Tag>
);
}
const handleChangeTags = (value: string[]) => {
let validatedValues: string[] = []
value.forEach(function (v) {
if (v.trim().length) {
validatedValues.push(v)
}
})
setSelectedTagGroups(validatedValues)
};
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{margin: '8px 0'}}/>
<Row style={{padding: '0 8px 4px'}}>
<Col flex="auto">
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
</Col>
<Col flex="none">
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"/>
</svg>
</Col>
</Row>
</>
)
const optionRender = (label: string) => {
let peersCount = ''
const g = groups.find(_g => _g.name === label)
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
return (
<>
<Tag
color="blue"
style={{marginRight: 3}}
>
<strong>{label}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
</>
const createUserToSave = (values: any): UserToSave => {
const autoGroups =
groups
?.filter(
(g) =>
values.autoGroupsNames && values.autoGroupsNames.includes(g.name)
)
.map((g) => g.id || "") || [];
// find groups that do not yet exist (newly added by the user)
const allGroupsNames: string[] = groups?.map((g) => g.name);
const groupsToCreate =
values.autoGroupsNames?.filter(
(s: string) => !allGroupsNames.includes(s)
) || [];
return {
id: values.id,
role: values.role,
name: values.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
is_service_user: true,
} as UserToSave;
};
const onCancel = () => {
if (savedUser.loading) return;
dispatch(userActions.setUser(null as unknown as User));
form.resetFields();
dispatch(userActions.setAddServiceUserPopupVisible(false));
};
const handleFormSubmit = () => {
form
.validateFields()
.then((values) => {
let userToSave = createUserToSave(values);
dispatch(
userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave,
})
);
form.resetFields();
dispatch(
userActions.getServiceUsers.request({
getAccessTokenSilently: getTokenSilently,
payload: null,
})
);
dispatch(userActions.setAddServiceUserPopupVisible(false));
})
.catch((errorInfo) => {
console.log("errorInfo", errorInfo);
});
};
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = [];
if (!value) {
return Promise.resolve();
}
useEffect(() => {
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
}, [groups])
value.forEach(function (v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v);
}
});
useEffect(() => {
dispatch(groupActions.getGroups.request({
getAccessTokenSilently: getTokenSilently,
payload: null
}))
}, [])
if (hasSpaceNamed.length) {
return Promise.reject(
new Error("Group names with just spaces are not allowed")
);
}
return Promise.resolve();
};
const tagRender = (props: CustomTagProps) => {
const { label, value, closable, onClose } = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
return (
<>
<Modal
open={addServiceUserModalOpen}
onCancel={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
<Button type="primary"
onClick={handleFormSubmit}>Create user</Button>
</Space>
}
width={460}
>
<Container style={{textAlign: "start"}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
{"Add Service User"}
</Paragraph>
<Paragraph type={"secondary"}
style={{
textAlign: "start",
whiteSpace: "pre-line",
marginTop: "-23px",
paddingBottom: "25px",
}}>
{"Service Users are non-login users that are not associated with any specific person."}
</Paragraph>
<Form layout="vertical" hideRequiredMark form={form}
initialValues={{
["role"]: "user"
}}
>
<Row gutter={16}>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a name to easily identify the user</Paragraph>
<Form.Item
name="name"
rules={[{
required: true,
message: 'Please add a new name for this user',
whitespace: true
}]}
style={{marginTop: "-8px"}}
>
<Input
placeholder={'for example "Ansible User"'}
ref={inputNameRef}
autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Role</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a role for the user to assign dashboard access permissions</Paragraph>
<Form.Item
name="role"
rules={[{
required: true,
message: 'Please select a role for this user',
whitespace: true
}]}
style={{marginTop: "-8px"}}
>
<Select style={{width: "120px"}}>
<Option value="admin">admin</Option>
<Option value="user">user</Option>
</Select>
</Form.Item>
</Col>
{/*<Col span={24}>*/}
{/* <Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
{/* <Form.Item*/}
{/* name="autoGroupsNames"*/}
{/* label="Auto-assigned groups"*/}
{/* tooltip="Every peer enrolled with this user will be automatically added to these groups"*/}
{/* rules={[{validator: selectValidator}]}*/}
{/* >*/}
{/* <Select mode="tags"*/}
{/* style={{width: '100%'}}*/}
{/* placeholder="Associate groups with the user"*/}
{/* tagRender={tagRender}*/}
{/* onChange={handleChangeTags}*/}
{/* dropdownRender={dropDownRender}*/}
{/* >*/}
{/* {*/}
{/* tagGroups.map(m =>*/}
{/* <Option key={m}>{optionRender(m)}</Option>*/}
{/* )*/}
{/* }*/}
{/* </Select>*/}
{/* </Form.Item>*/}
{/*</Col>*/}
</Row>
</Form>
</Container>
</Modal>
{confirmModalContextHolder}
</>
)
<Tag
color="blue"
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{ marginRight: 3 }}
>
<strong>{value}</strong>
</Tag>
);
};
}
const handleChangeTags = (value: string[]) => {
let validatedValues: string[] = [];
value.forEach(function (v) {
if (v.trim().length) {
validatedValues.push(v);
}
});
setSelectedTagGroups(validatedValues);
};
export default AddServiceUserPopup
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{ margin: "8px 0" }} />
<Row style={{ padding: "0 8px 4px" }}>
<Col flex="auto">
<span style={{ color: "#9CA3AF" }}>
Add new group by pressing "Enter"
</span>
</Col>
<Col flex="none">
<svg
width="14"
height="12"
viewBox="0 0 14 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"
/>
</svg>
</Col>
</Row>
</>
);
const optionRender = (label: string) => {
let peersCount = "";
const g = groups.find((_g) => _g.name === label);
if (g)
peersCount = ` - ${g.peers_count || 0} ${
!g.peers_count || parseInt(g.peers_count) !== 1 ? "peers" : "peer"
} `;
return (
<>
<Tag color="blue" style={{ marginRight: 3 }}>
<strong>{label}</strong>
</Tag>
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
</>
);
};
useEffect(() => {
setTagGroups(
groups?.filter((g) => g.name != "All").map((g) => g.name) || []
);
}, [groups]);
useEffect(() => {
dispatch(
groupActions.getGroups.request({
getAccessTokenSilently: getTokenSilently,
payload: null,
})
);
}, []);
return (
<>
<Modal
open={addServiceUserModalOpen}
onCancel={onCancel}
footer={
<Space style={{ display: "flex", justifyContent: "end" }}>
<Button disabled={loading} onClick={onCancel}>
Cancel
</Button>
<Button type="primary" onClick={handleFormSubmit}>
Create user
</Button>
</Space>
}
width={460}
>
<Container
style={{
textAlign: "start",
marginLeft: "-15px",
marginRight: "-15px",
}}
>
<Paragraph
style={{
textAlign: "start",
whiteSpace: "pre-line",
fontSize: "18px",
fontWeight: "500",
}}
>
{"Add service user"}
</Paragraph>
<Paragraph
type={"secondary"}
style={{
textAlign: "start",
whiteSpace: "pre-line",
marginTop: "-23px",
paddingBottom: "25px",
}}
>
{
"Service users are non-login users that are not associated with any specific person."
}
</Paragraph>
<Form
layout="vertical"
hideRequiredMark
form={form}
initialValues={{
["role"]: "user",
}}
>
<Row gutter={16}>
<Col span={24}>
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
Name
</Paragraph>
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
Set a name to easily identify the user
</Paragraph>
<Form.Item
name="name"
rules={[
{
required: true,
message: "Please add a new name for this user",
whitespace: true,
},
]}
style={{ marginTop: "-8px" }}
>
<Input
placeholder={'for example "Ansible user"'}
ref={inputNameRef}
autoComplete="off"
/>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
Role
</Paragraph>
<Paragraph
type={"secondary"}
style={{ fontSize: "14px", marginTop: "-15px" }}
>
Set a role for the user to assign access permissions
</Paragraph>
<Form.Item
name="role"
rules={[
{
required: true,
message: "Please select a role for this user",
whitespace: true,
},
]}
style={{ marginTop: "-8px" }}
>
<Select style={{ width: "120px" }}>
<Option value="admin">admin</Option>
<Option value="user">user</Option>
</Select>
</Form.Item>
</Col>
{/*<Col span={24}>*/}
{/* <Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
{/* <Form.Item*/}
{/* name="autoGroupsNames"*/}
{/* label="Auto-assigned groups"*/}
{/* tooltip="Every peer enrolled with this user will be automatically added to these groups"*/}
{/* rules={[{validator: selectValidator}]}*/}
{/* >*/}
{/* <Select mode="tags"*/}
{/* style={{width: '100%'}}*/}
{/* placeholder="Associate groups with the user"*/}
{/* tagRender={tagRender}*/}
{/* onChange={handleChangeTags}*/}
{/* dropdownRender={dropDownRender}*/}
{/* >*/}
{/* {*/}
{/* tagGroups.map(m =>*/}
{/* <Option key={m}>{optionRender(m)}</Option>*/}
{/* )*/}
{/* }*/}
{/* </Select>*/}
{/* </Form.Item>*/}
{/*</Col>*/}
</Row>
</Form>
</Container>
</Modal>
{confirmModalContextHolder}
</>
);
};
export default AddServiceUserPopup;

View File

@@ -1,317 +1,367 @@
import {
Button,
Col,
Divider,
Form,
Input,
Modal,
Row,
Select,
Space,
Tag,
Typography
Button,
Col,
Divider,
Form,
Input,
Modal,
Row,
Select,
Space,
Typography,
} from "antd";
import {Container} from "../Container";
import {CloseOutlined} from "@ant-design/icons";
import React, {useEffect, useRef, useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {useGetTokenSilently} from "../../utils/token";
import {actions as userActions} from "../../store/user";
import {actions as groupActions} from "../../store/group";
import {User, UserToSave} from "../../store/user/types";
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import { Container } from "../Container";
import React, { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { useGetTokenSilently } from "../../utils/token";
import { actions as userActions } from "../../store/user";
import { actions as groupActions } from "../../store/group";
import { User, UserToSave } from "../../store/user/types";
import { RuleObject } from "antd/lib/form";
import { CustomTagProps } from "rc-select/lib/BaseSelect";
import { QuestionCircleFilled } from "@ant-design/icons";
import { useGetGroupTagHelpers } from "../../utils/groups";
const {Title, Text, Paragraph} = Typography;
const {Option} = Select;
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
const InviteUserPopup = () => {
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const { optionRender, blueTagRender, tagGroups, handleChangeTags } =
useGetGroupTagHelpers();
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const groups = useSelector((state: RootState) => state.group.data)
const users = useSelector((state: RootState) => state.user.data)
const groups = useSelector((state: RootState) => state.group.data);
const users = useSelector((state: RootState) => state.user.data);
const user = useSelector((state: RootState) => state.user.user)
const failed = useSelector((state: RootState) => state.user.failed);
const loading = useSelector((state: RootState) => state.user.loading);
const inviteUserModalOpen = useSelector((state: RootState) => state.user.inviteUserPopupVisible)
const savedUser = useSelector((state: RootState) => state.user.savedUser)
const user = useSelector((state: RootState) => state.user.user);
const failed = useSelector((state: RootState) => state.user.failed);
const loading = useSelector((state: RootState) => state.user.loading);
const inviteUserModalOpen = useSelector(
(state: RootState) => state.user.inviteUserPopupVisible
);
const savedUser = useSelector((state: RootState) => state.user.savedUser);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const [form] = Form.useForm();
const inputNameRef = useRef<any>(null);
const [form] = Form.useForm()
const inputNameRef = useRef<any>(null)
const [tagGroups, setTagGroups] = useState([] as string[])
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
const createUserToSave = (values: any): UserToSave => {
const autoGroups = groups?.filter(g => values.autoGroupsNames && values.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
// find groups that do not yet exist (newly added by the user)
const allGroupsNames: string[] = groups?.map(g => g.name);
const groupsToCreate = values.autoGroupsNames?.filter((s: string) => !allGroupsNames.includes(s)) || []
return {
id: values.id,
role: values.role,
email: values.email,
name: values.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
is_service_user: false
} as UserToSave
}
const onCancel = () => {
if (savedUser.loading) return
dispatch(userActions.setUser(null as unknown as User));
form.resetFields();
dispatch(userActions.setInviteUserPopupVisible(false));
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
let userToSave = createUserToSave(values)
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}))
form.resetFields();
dispatch(userActions.getRegularUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
dispatch(userActions.setInviteUserPopupVisible(false));
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = []
if (!value) {
return Promise.resolve()
}
value.forEach(function (v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v)
}
})
if (hasSpaceNamed.length) {
return Promise.reject(new Error("Group names with just spaces are not allowed"))
}
return Promise.resolve()
}
const tagRender = (props: CustomTagProps) => {
const {label, value, closable, onClose} = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
return (
<Tag
color="blue"
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{marginRight: 3}}
>
<strong>{value}</strong>
</Tag>
);
}
const handleChangeTags = (value: string[]) => {
let validatedValues: string[] = []
value.forEach(function (v) {
if (v.trim().length) {
validatedValues.push(v)
}
})
setSelectedTagGroups(validatedValues)
};
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{margin: '8px 0'}}/>
<Row style={{padding: '0 8px 4px'}}>
<Col flex="auto">
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
</Col>
<Col flex="none">
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"/>
</svg>
</Col>
</Row>
</>
)
const optionRender = (label: string) => {
let peersCount = ''
const g = groups.find(_g => _g.name === label)
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
return (
<>
<Tag
color="blue"
style={{marginRight: 3}}
>
<strong>{label}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
</>
const createUserToSave = (values: any): UserToSave => {
const autoGroups =
groups
?.filter(
(g) =>
values.autoGroupsNames && values.autoGroupsNames.includes(g.id)
)
.map((g) => g.id || "") || [];
// find groups that do not yet exist (newly added by the user)
const allGroupsNames: string[] = groups?.map((g) => g.id || "");
const groupsToCreate =
values.autoGroupsNames?.filter(
(s: string) => !allGroupsNames.includes(s)
) || [];
return {
id: values.id,
role: values.role,
email: values.email,
name: values.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
is_service_user: false,
} as UserToSave;
};
const onCancel = () => {
if (savedUser.loading) return;
dispatch(userActions.setUser(null as unknown as User));
form.resetFields();
dispatch(userActions.setInviteUserPopupVisible(false));
};
const handleFormSubmit = () => {
form
.validateFields()
.then((values) => {
let userToSave = createUserToSave(values);
dispatch(
userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave,
})
);
form.resetFields();
dispatch(
userActions.getRegularUsers.request({
getAccessTokenSilently: getTokenSilently,
payload: null,
})
);
dispatch(userActions.setInviteUserPopupVisible(false));
})
.catch((errorInfo) => {
console.log("errorInfo", errorInfo);
});
};
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = [];
if (!value) {
return Promise.resolve();
}
useEffect(() => {
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
}, [groups])
value.forEach(function (v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v);
}
});
useEffect(() => {
dispatch(groupActions.getGroups.request({
getAccessTokenSilently: getTokenSilently,
payload: null
}))
}, [])
if (hasSpaceNamed.length) {
return Promise.reject(
new Error("Group names with just spaces are not allowed")
);
}
return (
<>
<Modal
open={inviteUserModalOpen}
onCancel={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
<Button type="primary"
onClick={handleFormSubmit}>Invite</Button>
</Space>
}
width={460}
>
<Container style={{textAlign: "start"}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
{"Invite User"}
</Paragraph>
<Paragraph type={"secondary"}
style={{
textAlign: "start",
whiteSpace: "pre-line",
fontSize: "14px",
marginTop: "-23px",
paddingBottom: "25px",
}}>
{"Invite a user to your network and set their permissions."}
</Paragraph>
<Form layout="vertical" hideRequiredMark form={form}
initialValues={{
["role"]: "user"
}}
>
<Row gutter={16}>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a name to easily identify the user</Paragraph>
<Form.Item
name="name"
rules={[{
required: true,
message: 'Please add a name for this user',
whitespace: true
}]}
style={{marginTop: "-8px"}}
>
<Input
placeholder={'for example "Max Schmidt"'}
ref={inputNameRef}
autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Email</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Provide the email address of the user</Paragraph>
<Form.Item
name="email"
rules={[{
required: true,
message: 'Please add a valid email address for this user',
whitespace: false,
pattern: new RegExp(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i)
}]}
style={{marginTop: "-8px"}}
>
<Input
placeholder={'for example "max.schmidt@gmail.com"'}
ref={inputNameRef}
autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Role</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a role for the user to assign dashboard access permissions</Paragraph>
<Form.Item
name="role"
rules={[{
required: true,
message: 'Please select a role for this user',
whitespace: true
}]}
style={{marginTop: "-8px"}}
>
<Select style={{width: "120px"}}>
<Option value="admin">admin</Option>
<Option value="user">user</Option>
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Auto-assigned groups</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>
<Form.Item
name="autoGroupsNames"
tooltip="Every peer enrolled with this user will be automatically added to these groups"
rules={[{validator: selectValidator}]}
style={{marginTop: "-8px"}}
>
<Select mode="tags"
style={{width: '100%'}}
placeholder="Associate groups with the user"
tagRender={tagRender}
onChange={handleChangeTags}
dropdownRender={dropDownRender}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
{/*<Col span={24}>*/}
{/* <Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}*/}
{/* href="https://netbird.io/docs/overview/personal-access-tokens">Learn more about user</Button>*/}
{/*</Col>*/}
</Row>
</Form>
</Container>
</Modal>
{confirmModalContextHolder}
</>
)
return Promise.resolve();
};
}
// const handleChangeTags = (value: string[]) => {
// let validatedValues: string[] = [];
// value.forEach(function (v) {
// if (v.trim().length) {
// validatedValues.push(v);
// }
// });
// setSelectedTagGroups(validatedValues);
// };
export default InviteUserPopup
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{ margin: "8px 0" }} />
<Row style={{ padding: "0 8px 4px" }}>
<Col flex="auto">
<span style={{ color: "#9CA3AF" }}>
Add new group by pressing "Enter"
</span>
</Col>
<Col flex="none">
<svg
width="14"
height="12"
viewBox="0 0 14 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"
/>
</svg>
</Col>
</Row>
</>
);
// useEffect(() => {
// setTagGroups(
// groups?.filter((g) => g.name != "All").map((g) => g.name) || []
// );
// }, [groups]);
useEffect(() => {
dispatch(
groupActions.getGroups.request({
getAccessTokenSilently: getTokenSilently,
payload: null,
})
);
}, []);
return (
<>
<Modal
open={inviteUserModalOpen}
onCancel={onCancel}
footer={
<Space style={{ display: "flex", justifyContent: "end" }}>
<Button disabled={loading} onClick={onCancel}>
Cancel
</Button>
<Button type="primary" onClick={handleFormSubmit}>
Invite
</Button>
</Space>
}
width={460}
>
<Container
style={{
textAlign: "start",
marginLeft: "-15px",
marginRight: "-15px",
}}
>
<Paragraph
style={{
textAlign: "start",
whiteSpace: "pre-line",
fontSize: "18px",
fontWeight: "500",
}}
>
{"Invite user"}
</Paragraph>
<Paragraph
type={"secondary"}
style={{
textAlign: "start",
whiteSpace: "pre-line",
fontSize: "14px",
marginTop: "-23px",
paddingBottom: "25px",
}}
>
{"Invite a user to your network and set their permissions."}
</Paragraph>
<Form
layout="vertical"
hideRequiredMark
form={form}
initialValues={{
["role"]: "user",
}}
>
<Row gutter={16}>
<Col span={24}>
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
Name
</Paragraph>
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
Set a name to easily identify the user
</Paragraph>
<Form.Item
name="name"
rules={[
{
required: true,
message: "Please add a name for this user",
whitespace: true,
},
]}
style={{ marginTop: "-8px" }}
>
<Input
placeholder={'for example "Max Schmidt"'}
ref={inputNameRef}
autoComplete="off"
/>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
Email
</Paragraph>
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
Provide the email address of the user
</Paragraph>
<Form.Item
name="email"
rules={[
{
required: true,
message: "Please add a valid email address for this user",
whitespace: false,
pattern: new RegExp(
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
),
},
]}
style={{ marginTop: "-8px" }}
>
<Input
placeholder={'for example "max.schmidt@gmail.com"'}
ref={inputNameRef}
autoComplete="off"
/>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
Role
</Paragraph>
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
Set a role for the user to assign access permissions
</Paragraph>
<Form.Item
name="role"
rules={[
{
required: true,
message: "Please select a role for this user",
whitespace: true,
},
]}
style={{ marginTop: "-8px" }}
>
<Select style={{ width: "120px" }}>
<Option value="admin">admin</Option>
<Option value="user">user</Option>
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
Auto-assigned groups
</Paragraph>
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
Add groups, that will be assigned to peers added by this user
</Paragraph>
<Form.Item
name="autoGroupsNames"
tooltip="Every peer enrolled with this user will be automatically added to these groups"
rules={[{ validator: selectValidator }]}
style={{ marginTop: "-8px" }}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the user"
tagRender={blueTagRender}
onChange={handleChangeTags}
optionFilterProp="serchValue"
dropdownRender={dropDownRender}
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Text type={"secondary"}>
Learn more about
<a
target="_blank"
rel="noreferrer"
href="https://docs.netbird.io/how-to/add-users-to-your-network"
>
{" "}
user management
</a>
</Text>
</Col>
</Row>
</Form>
</Container>
</Modal>
{confirmModalContextHolder}
</>
);
};
export default InviteUserPopup;

View File

@@ -10,6 +10,7 @@ import WindowsTab from "./WindowsTab";
import MacTab from "./MacTab";
import Link from "antd/lib/typography/Link";
import DockerTab from "./DockerTab";
import AndroidTab from "./AndroidTab";
type Props = {
greeting?: string;
@@ -38,27 +39,27 @@ export const AddPeerPopup: React.FC<Props> = ({
const items: TabsProps['items'] = [
{
key: "1",
label: <span><Icon component={LinuxSVG}/>Linux</span>,
label: <span data-testid="add-peer-modal-linux-tab"><Icon component={LinuxSVG}/>Linux</span>,
children: <UbuntuTab/>,
},
{
key: "2",
label: <span><WindowsFilled/>Windows</span>,
label: <span data-testid="add-peer-modal-windows-tab"><WindowsFilled/>Windows</span>,
children: <WindowsTab/>,
},
{
key: "3",
label: <span><AppleFilled/>macOS</span>,
label: <span data-testid="add-peer-modal-mac-tab"><AppleFilled/>MacOS</span>,
children: <MacTab/>,
},
/*{
{
key: "4",
label: <span><AndroidFilled/>Android</span>,
children: <></>,
},*/
label: <span data-testid="add-peer-modal-android-tab"><AndroidFilled/>Android</span>,
children: <AndroidTab/>,
},
{
key: "5",
label: <span><Icon component={DockerSVG}/>Docker</span>,
label: <span data-testid="add-peer-modal-docker-tab"><Icon component={DockerSVG}/>Docker</span>,
children: <DockerTab/>,
}
];
@@ -78,7 +79,7 @@ export const AddPeerPopup: React.FC<Props> = ({
textAlign: "center",
whiteSpace: "pre-line",
}}>
To get started install NetBird and log in using your {"\n"} email account.
To get started, install NetBird and log in using your {"\n"} email account.
</Paragraph>
<Tabs centered={!isMobile}
@@ -91,7 +92,7 @@ export const AddPeerPopup: React.FC<Props> = ({
After that you should be connected. Add more devices to your network or manage your existing devices in the
admin panel.
If you have further questions check out our {<Link target="_blank"
href={"https://netbird.io/docs/getting-started/installation"}>installation
href={"https://docs.netbird.io/how-to/getting-started#installation"}>installation
guide</Link>}
</Paragraph>
</>

View File

@@ -0,0 +1,62 @@
import React, {useState} from 'react';
import {Button, Image, Typography} from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import googleplay from '../../../../assets/google-play-badge.png';
import {getConfig} from "../../../../config";
const { grpcApiOrigin } = getConfig();
const {Text} = Typography;
export const AndroidTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and install the application from Google Play Store:',
commands: (
<a data-testid="download-android-button" href="https://play.google.com/store/apps/details?id=io.netbird.client" target="_blank">
<Image width="12em" preview={false} style={{marginTop: "5px"}} src={googleplay}/>
</a>
),
copied: false
} as StepCommand,
... grpcApiOrigin ? [{
key: 2,
title: 'Click on "Change Server" and enter the following "Server"',
commands: grpcApiOrigin,
commandsForCopy: grpcApiOrigin,
copied: false,
showCopyButton: false
}] : [],
{
key: 2 + (grpcApiOrigin ? 1 : 0),
title: 'Click on the "Connect" button in the middle of the screen',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3 + (grpcApiOrigin ? 1 : 0),
title: 'Sign up using your email address',
commands: '',
copied: false,
showCopyButton: false
}
])
return (
<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on Android
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
)
}
export default AndroidTab

View File

@@ -11,32 +11,46 @@ const {Title, Paragraph, Text} = Typography;
export const DockerTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Install Docker',
commands: (
<Button style={{marginTop: "5px"}} type="primary" href="https://docs.docker.com/engine/install/" target="_blank">Official Docker website</Button>
),
copied: false,
showCopyButton: false
} as StepCommand,
{
key: 2,
title: 'Run NetBird container',
commands: formatDockerCommand(),
copied: false,
showCopyButton: false
} as StepCommand,
{
key: 3,
title: "Read docs",
commands: (
<Link href="https://netbird.io/docs/getting-started/installation#running-netbird-in-docker" target="_blank">Running NetBird in Docker</Link>
),
copied: false,
showCopyButton: false
} as StepCommand
])
{
key: 1,
title: "Install Docker",
commands: (
<Button
data-testid="download-docker-button"
style={{ marginTop: "5px" }}
type="primary"
href="https://docs.docker.com/engine/install/"
target="_blank"
>
Official Docker website
</Button>
),
copied: false,
showCopyButton: false,
} as StepCommand,
{
key: 2,
title: "Run NetBird container",
commands: formatDockerCommand(),
commandsForCopy: formatDockerCommand(),
copied: false,
showCopyButton: false,
} as StepCommand,
{
key: 3,
title: "Read docs",
commands: (
<Link
href="https://docs.netbird.io/how-to/getting-started#running-net-bird-in-docker"
target="_blank"
>
Running NetBird in Docker
</Link>
),
copied: false,
showCopyButton: false,
} as StepCommand,
]);
return (
<div style={{marginTop: 10}}>

View File

@@ -1,28 +1,28 @@
import { useState } from 'react';
import { useState } from "react";
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import { StepCommand } from "./types";
export const OtherTab = () => {
const [steps, _] = useState([
{
key: 1,
title: "For other installation options check our documentation.",
commands: (
<Button
type="primary"
href={`https://docs.netbird.io/how-to/getting-started#binary-install`}
target="_blank"
>
Documentation
</Button>
),
copied: false,
} as StepCommand,
]);
const [steps, _] = useState([
{
key: 1,
title: 'For other installation options check our documentation.',
commands: (
<Button type="primary" href={`https://netbird.io/docs/getting-started/installation#binary-install`} target="_blank">
Documentation
</Button>
),
copied: false,
} as StepCommand,
])
return <TabSteps stepsItems={steps} />;
};
return (
<TabSteps stepsItems={steps} />
)
}
export default OtherTab
export default OtherTab;

View File

@@ -1,113 +1,223 @@
import React, {useState} from 'react';
import React, { useState } from "react";
import {Button, Typography} from "antd";
import { Button, Divider, Row, Tooltip, Typography } from "antd";
import TabSteps from "./TabSteps";
import {StepCommand} from "./types"
import {formatNetBirdUP} from "./common"
import {Collapse} from "antd";
import { StepCommand } from "./types";
import { formatNetBirdUP } from "./common";
import { Collapse } from "antd";
import SyntaxHighlighter from "react-syntax-highlighter";
import { QuestionCircleOutlined } from "@ant-design/icons";
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
import { copyToClipboard } from "../../../../utils/common";
import {getConfig} from "../../../../config";
const { grpcApiOrigin } = getConfig();
const { Panel } = Collapse;
const {Text} = Typography;
const { Text } = Typography;
export const LinuxTab = () => {
const [copied, setCopied] = useState(false);
const [quickSteps, setQuickSteps] = useState([
{
key: 1,
title: (
<Row>
<Text>Download and run MacOS installer: </Text>
<Tooltip
title={
<text>
If you don't know what chip your Mac has, you can find out by
clicking on the Apple logo in the top left corner of your screen
and selecting 'About This Mac'. For more information click{" "}
<a
href="https://support.apple.com/en-us/HT211814"
target="_blank"
>
here
</a>
</text>
}
className={"ant-form-item-tooltip"}
>
<QuestionCircleOutlined
style={{
color: "rgba(0, 0, 0, 0.45)",
cursor: "help",
marginLeft: "3px",
}}
/>
</Tooltip>
</Row>
),
commands: (
<Row style={{ paddingTop: "5px" }}>
<Button
data-testid="download-intel-button"
style={{ marginRight: "10px" }}
type="primary"
href="https://pkgs.netbird.io/macos/amd64"
>
Download for Intel
</Button>
<Button
data-testid="download-m1-m2-button"
style={{ marginRight: "10px" }}
type="default"
href="https://pkgs.netbird.io/macos/arm64"
>
Download for M1 & M2
</Button>
</Row>
),
copied: false,
} as StepCommand,
... grpcApiOrigin ? [{
key: 2,
title: 'Click on "Settings" from the NetBird icon in your system tray and enter the following "Management URL"',
commands: grpcApiOrigin,
commandsForCopy: grpcApiOrigin,
copied: false,
showCopyButton: false,
}] : [],
{
key: 2 + (grpcApiOrigin ? 1 : 0),
title: 'Click on "Connect" from the NetBird icon in your system tray',
commands: "",
copied: false,
showCopyButton: false,
},
{
key: 3 + (grpcApiOrigin) ? 1 : 0,
title: "Sign up using your email address",
commands: "",
copied: false,
showCopyButton: false,
},
]);
const [quickSteps, setQuickSteps] = useState([
{
key: 1,
title: 'Download and run installer:',
commands: (
<Button style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64"
target="_blank">Download NetBird</Button>
),
copied: false
} as StepCommand,
{
key: 2,
title: 'Click on "Connect" from the NetBird icon in your system tray',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3,
title: 'Sign up using your email address',
commands: '',
copied: false,
showCopyButton: false
}
])
const [steps, setSteps] = useState([
{
key: 1,
title: "Download and install Homebrew",
commands: (
<Button
style={{ marginTop: "5px" }}
type="primary"
href="https://brew.sh/"
target="_blank"
>
Download Brew
</Button>
),
copied: false,
} as StepCommand,
{
key: 2,
title: "Install NetBird:",
commands: [
`# for CLI only`,
`brew install netbirdio/tap/netbird`,
`# for GUI package`,
`brew install --cask netbirdio/tap/netbird-ui`,
].join("\n"),
commandsForCopy: [
`brew install netbirdio/tap/netbird`,
`brew install --cask netbirdio/tap/netbird-ui`,
].join("\n"),
copied: false,
showCopyButton: true,
} as StepCommand,
{
key: 3,
title: "Start NetBird daemon:",
commands: [
`sudo netbird service install`,
`sudo netbird service start`,
].join("\n"),
commandsForCopy: [
`sudo netbird service install`,
`sudo netbird service start`,
].join("\n"),
copied: false,
showCopyButton: true,
} as StepCommand,
{
key: 4,
title: "Run NetBird and log in the browser:",
commands: formatNetBirdUP(),
commandsForCopy: formatNetBirdUP(),
copied: false,
showCopyButton: true,
} as StepCommand,
]);
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and install Homebrew',
commands: (
<Button style={{marginTop: "5px"}} type="primary" href="https://brew.sh/" target="_blank">Download
Brew</Button>
),
copied: false
} as StepCommand,
{
key: 2,
title: 'Install NetBird:',
commands: [
`# for CLI only`,
`brew install netbirdio/tap/netbird`,
`# for GUI package`,
`brew install --cask netbirdio/tap/netbird-ui`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 3,
title: 'Start NetBird daemon:',
commands: [
`sudo netbird service install`,
`sudo netbird service start`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Run NetBird and log in the browser:',
commands: formatNetBirdUP(),
copied: false,
showCopyButton: true
} as StepCommand
])
return (
<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install with one command
</Text>
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
<SyntaxHighlighter language="bash">
curl -fsSL https://pkgs.netbird.io/install.sh | sh
</SyntaxHighlighter>
const onCopyClick = () => {
const stringToCopy = "curl -fsSL https://pkgs.netbird.io/install.sh | sh";
copyToClipboard(stringToCopy);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
};
return (
<div style={{ marginTop: 10 }}>
<Text style={{ fontWeight: "bold" }}>Install on MacOS</Text>
<div style={{ marginTop: 5, marginBottom: 5 }}>
<TabSteps stepsItems={quickSteps} />
</div>
<div style={{ marginTop: 0 }} />
{/*<Divider style={{marginTop: "5px"}} />*/}
<Collapse bordered={false} style={{ backgroundColor: "unset" }}>
<Panel
className="CustomPopupCollapse"
header={<Text strong={true}>Or install via command line</Text>}
key="1"
>
<div style={{ marginLeft: "25px" }}>
<Text style={{ fontWeight: "bold" }}>Install with one command</Text>
<div
style={{
fontSize: ".85em",
marginTop: 5,
marginBottom: 25,
position: "relative",
}}
>
{!copied ? (
<Button
type="text"
size="middle"
className="btn-copy-code peer"
icon={<CopyOutlined />}
style={{ color: "rgb(107, 114, 128)", top: "0", zIndex: "3" }}
onClick={onCopyClick}
/>
) : (
<Button
type="text"
size="middle"
className="btn-copy-code peer"
icon={<CheckOutlined />}
style={{ color: "green", top: "0", zIndex: "3" }}
/>
)}
<SyntaxHighlighter language="bash">
curl -fsSL https://pkgs.netbird.io/install.sh | sh
</SyntaxHighlighter>
</div>
<Text style={{fontWeight: "bold"}}>
Or install manually with HomeBrew
<Text style={{ fontWeight: "bold" }}>
Or install manually with HomeBrew
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
<div style={{ marginTop: 5 }}>
<TabSteps stepsItems={steps} />
</div>
</div>
/*<div style={{marginTop: 5}}>
<TabSteps stepsItems={quickSteps}/>
</div>*/
/*<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on macOS with Homebrew
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>*/
)
}
</div>
</Panel>
</Collapse>
</div>
);
};
export default LinuxTab
export default LinuxTab;

View File

@@ -1,63 +1,89 @@
import "highlight.js/styles/mono-blue.css";
import "highlight.js/lib/languages/bash";
import { StepCommand } from './types'
import SyntaxHighlighter from 'react-syntax-highlighter';
import {
Typography,
Space,
Steps, Button, Popover, StepsProps
} from "antd";
import {copyToClipboard} from "../../../../utils/common";
import {CheckOutlined, CopyOutlined} from "@ant-design/icons";
import React, {useEffect, useState} from "react";
import { StepCommand } from "./types";
import SyntaxHighlighter from "react-syntax-highlighter";
import { Typography, Space, Steps, Button, Popover, StepsProps } from "antd";
import { copyToClipboard } from "../../../../utils/common";
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
import React, { useEffect, useState } from "react";
const { Step } = Steps;
const {Text} = Typography;
const { Text } = Typography;
type Props = {
stepsItems: Array<StepCommand>
stepsItems: Array<StepCommand>;
};
const TabSteps:React.FC<Props> = ({stepsItems}) => {
const TabSteps: React.FC<Props> = ({ stepsItems }) => {
const [steps, setSteps] = useState(stepsItems);
const [steps, setSteps] = useState(stepsItems)
useEffect(() => setSteps(stepsItems), [stepsItems]);
useEffect(() => setSteps(stepsItems), [stepsItems])
const onCopyClick = (
key: string | number,
commands: React.ReactNode | string,
copied: boolean
) => {
if (!(typeof commands === "string")) return;
copyToClipboard(commands);
const step = steps.find((s) => s.key === key);
if (step) step.copied = copied;
setSteps([...steps]);
const onCopyClick = (key: string | number, commands:React.ReactNode | string, copied: boolean) => {
if (!(typeof commands === 'string')) return
copyToClipboard(commands)
const step = steps.find(s => s.key === key)
if (step) step.copied = copied
setSteps([...steps])
if (copied) {
setTimeout(() => {
onCopyClick(key, commands, false)
}, 2000)
}
if (copied) {
setTimeout(() => {
onCopyClick(key, commands, false);
}, 2000);
}
return (
<Steps direction="vertical" size={"small"}>
{steps.map(c =>
<Step
status={"process"}
key={c.key}
title={<Text>{c.title}</Text>}
description={
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex", fontSize: ".85em"}}>
{ (c.commands && (typeof c.commands === 'string')) ? (
<SyntaxHighlighter language="bash">
{c.commands}
</SyntaxHighlighter>
) : (
c.commands
)}
</Space>
}
/>
)}
</Steps>
)
}
};
return (
<Steps direction="vertical" size={"small"}>
{steps.map((c) => (
<Step
status={"process"}
key={c.key}
title={<Text>{c.title}</Text>}
description={
<Space
className="nb-code"
direction="vertical"
size="small"
style={{ display: "flex", fontSize: ".85em" }}
>
{c.commands && typeof c.commands === "string" ? (
<>
{!c.copied ? (
<Button
type="text"
size="middle"
className="btn-copy-code peer"
icon={<CopyOutlined />}
style={{ color: "rgb(107, 114, 128)"}}
onClick={() => {
onCopyClick(c.key, c.commandsForCopy, true);
}}
/>
) : (
<Button
type="text"
size="middle"
className="btn-copy-code peer"
icon={<CheckOutlined />}
style={{ color: "green"}}
/>
)}
<SyntaxHighlighter language="bash">
{c.commands}
</SyntaxHighlighter>
</>
) : (
c.commands
)}
</Space>
}
/>
))}
</Steps>
);
};
export default TabSteps;
export default TabSteps;

View File

@@ -1,68 +1,112 @@
import React, {useState} from 'react';
import {StepCommand} from "./types"
import {formatNetBirdUP} from "./common"
import React, { useState } from "react";
import { StepCommand } from "./types";
import { formatNetBirdUP } from "./common";
import SyntaxHighlighter from "react-syntax-highlighter";
import TabSteps from "./TabSteps";
import {Typography} from "antd";
import { Typography, Button } from "antd";
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
import { copyToClipboard } from "../../../../utils/common";
const {Title, Paragraph, Text} = Typography;
const { Title, Paragraph, Text } = Typography;
export const UbuntuTab = () => {
const [copied, setCopied] = useState(false);
const [steps, setSteps] = useState([
{
key: 1,
title: 'Add repository',
commands: [
`sudo apt install ca-certificates curl gnupg -y`,
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/wiretrustee.gpg`,
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
].join('\n'),
copied: false,
showCopyButton: false
} as StepCommand,
{
key: 2,
title: 'Install NetBird',
commands: [
`sudo apt-get update`,
`# for CLI only`,
`sudo apt-get install netbird`,
`# for GUI package`,
`sudo apt-get install netbird-ui`
].join('\n'),
copied: false,
showCopyButton: false
} as StepCommand,
{
key: 3,
title: 'Run NetBird and log in the browser',
commands: formatNetBirdUP(),
copied: false,
showCopyButton: false
} as StepCommand
])
const [steps, setSteps] = useState([
{
key: 1,
title: "Add repository",
commands: [
`sudo apt-get update`,
`sudo apt install ca-certificates curl gnupg -y`,
`curl -sSL https://pkgs.netbird.io/debian/public.key | sudo gpg --dearmor --output /usr/share/keyrings/netbird-archive-keyring.gpg`,
`echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' | sudo tee /etc/apt/sources.list.d/netbird.list`,
].join("\n"),
commandsForCopy: [
`sudo apt-get update`,
`sudo apt-get install ca-certificates curl gnupg -y`,
`curl -sSL https://pkgs.netbird.io/debian/public.key | sudo gpg --dearmor --output /usr/share/keyrings/netbird-archive-keyring.gpg`,
`echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' | sudo tee /etc/apt/sources.list.d/netbird.list`,
].join("\n"),
copied: false,
showCopyButton: false,
} as StepCommand,
{
key: 2,
title: "Install NetBird",
commands: [
`sudo apt-get update`,
`# for CLI only`,
`sudo apt-get install netbird`,
`# for GUI package`,
`sudo apt-get install netbird-ui`,
].join("\n"),
commandsForCopy: [
`sudo apt-get update`,
`sudo apt-get install netbird`,
`sudo apt-get install netbird-ui`,
].join("\n"),
copied: false,
showCopyButton: false,
} as StepCommand,
{
key: 3,
title: "Run NetBird and log in the browser",
commands: formatNetBirdUP(),
commandsForCopy: formatNetBirdUP(),
copied: false,
showCopyButton: false,
} as StepCommand,
]);
return (
<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install with one command
</Text>
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
<SyntaxHighlighter language="bash">
curl -fsSL https://pkgs.netbird.io/install.sh | sh
</SyntaxHighlighter>
</div>
<Text style={{fontWeight: "bold"}}>
Or install manually on Ubuntu
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
const onCopyClick = () => {
const stringToCopy = "curl -fsSL https://pkgs.netbird.io/install.sh | sh";
copyToClipboard(stringToCopy);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
};
</div>
return (
<div style={{ marginTop: 10 }}>
<Text style={{ fontWeight: "bold" }}>Install with one command</Text>
<div
style={{
fontSize: ".85em",
marginTop: 5,
marginBottom: 25,
position: "relative",
}}
>
{!copied ? (
<Button
type="text"
size="middle"
className="btn-copy-code peer"
icon={<CopyOutlined />}
style={{ color: "rgb(107, 114, 128)", top: "0", zIndex: "3" }}
onClick={onCopyClick}
/>
) : (
<Button
type="text"
size="middle"
className="btn-copy-code peer"
icon={<CheckOutlined />}
style={{ color: "green", top: "0", zIndex: "3" }}
/>
)}
<SyntaxHighlighter language="bash">
curl -fsSL https://pkgs.netbird.io/install.sh | sh
</SyntaxHighlighter>
</div>
<Text style={{ fontWeight: "bold" }}>Or install manually on Ubuntu</Text>
<div style={{ marginTop: 5 }}>
<TabSteps stepsItems={steps} />
</div>
</div>
);
};
)
}
export default UbuntuTab
export default UbuntuTab;

View File

@@ -3,6 +3,9 @@ import React, {useState} from 'react';
import {Button, Typography} from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import {getConfig} from "../../../../config";
const { grpcApiOrigin } = getConfig();
const {Text} = Typography;
export const WindowsTab = () => {
@@ -12,19 +15,27 @@ export const WindowsTab = () => {
key: 1,
title: 'Download and run Windows installer:',
commands: (
<Button style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64" target="_blank">Download NetBird</Button>
<Button data-testid="download-windows-button" style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64" target="_blank">Download NetBird</Button>
),
copied: false
} as StepCommand,
{
... grpcApiOrigin ? [{
key: 2,
title: 'Click on "Settings" from the NetBird icon in your system tray and enter the following "Management URL"',
commands: grpcApiOrigin,
commandsForCopy: grpcApiOrigin,
copied: false,
showCopyButton: false
}] : [],
{
key: 2 + (grpcApiOrigin ? 1 : 0),
title: 'Click on "Connect" from the NetBird icon in your system tray',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3,
key: 3 + (grpcApiOrigin ? 1 : 0),
title: 'Sign up using your email address',
commands: '',
copied: false,

View File

@@ -1,9 +1,10 @@
import * as React from "react";
export interface StepCommand {
key: number | string,
title: React.ReactNode | string | null,
commands: React.ReactNode | string | null,
copied?: boolean,
showCopyButton?: boolean
key: number | string;
title: React.ReactNode | string | null;
commands: React.ReactNode | string | null;
commandsForCopy?: React.ReactNode | string | null;
copied?: boolean;
showCopyButton?: boolean;
}

View File

@@ -2,6 +2,7 @@
"auth0Auth": "$USE_AUTH0",
"authAuthority": "$AUTH_AUTHORITY",
"authClientId": "$AUTH_CLIENT_ID",
"authClientSecret": "$AUTH_CLIENT_SECRET",
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
"authAudience": "$AUTH_AUDIENCE",
@@ -10,5 +11,6 @@
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"redirectURI": "$AUTH_REDIRECT_URI",
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
"tokenSource": "$NETBIRD_TOKEN_SOURCE"
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS"
}

View File

@@ -28,8 +28,9 @@ export function getConfig() {
return {
auth0Auth: configJson.auth0Auth == "true", //due to substitution we can't use boolean in the config
authority: configJson.authAuthority,
authority: configJson.authAuthority.replace(/\/+$/, ''),
clientId: configJson.authClientId,
clientSecret: configJson.authClientSecret,
scopesSupported: configJson.authScopesSupported,
apiOrigin: configJson.apiOrigin,
grpcApiOrigin: configJson.grpcApiOrigin,
@@ -38,5 +39,7 @@ export function getConfig() {
redirectURI: redirectURI,
silentRedirectURI: silentRedirectURI,
tokenSource: tokenSource,
// drags all the query params to the auth layer specified in the URL when accessing dashboard.
dragQueryParams: configJson.dragQueryParams == "true"
};
}

View File

@@ -1,16 +1,35 @@
@import url('https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;500&display=swap');
@import 'antd/dist/reset.css';
pre,
code,
kbd,
samp,
pre *,
code *,
kbd *,
samp * {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace !important;
}
html,
body,
* {
font-family: 'Arimo', sans-serif !important;
}
body {
font-size: 16px;
background-color: #f0f2f5;
}
.ant-layout-header {
background: #fff;
padding: 0;
height: auto;
min-height: 64px;
border-bottom: 1px solid #e0e0e0;
}
@@ -23,13 +42,14 @@ body {
border: none;
}
.ant-menu-horizontal > .ant-menu-item a {
.ant-menu-horizontal>.ant-menu-item a {
color: rgba(107, 114, 128, 1);
font-weight: 500;
font-weight: 400;
}
.ant-menu-horizontal > .ant-menu-item-selected a {
.ant-menu-horizontal>.ant-menu-item-selected a {
color: rgba(17, 24, 39, 1);
font-weight: 500;
}
@@ -47,23 +67,25 @@ body {
flex-wrap: wrap;
align-items: flex-start;
}
.space-align-block {
flex: none;
margin: 8px 4px;
padding: 4px;
border: none;
}
.space-align-block .mock-block {
display: inline-block;
padding: 32px 8px 16px;
background: rgba(150, 150, 150, 0.2);
}
.bg-indigo-600{
.bg-indigo-600 {
background-color: rgb(79, 70, 229);
}
.card-table-no-placeholder .ant-table-placeholder{
.card-table-no-placeholder .ant-table-placeholder {
display: none;
}
@@ -75,9 +97,10 @@ body {
height: 200px;
}
.card-table .ant-table-ping-left:not(.ant-table-has-fix-left) .ant-table-container::before {
.card-table .ant-table-ping-left:not(.ant-table-has-fix-left) .ant-table-container::before {
box-shadow: none;
}
.card-table .ant-table-ping-right:not(.ant-table-has-fix-right) .ant-table-container::after {
box-shadow: none;
}
@@ -119,15 +142,17 @@ body {
margin: 24px 0;
}
.ant-steps-item-icon{
.ant-steps-item-icon {
background: #EBEBEB !important;
border-color: #EBEBEB !important;
}
.ant-steps-icon-dot{
.ant-steps-icon-dot {
background: #EBEBEB !important;
border-color: #EBEBEB !important;
}
.ant-steps-icon {
.ant-steps-icon {
background: #EBEBEB !important;
border: none;
color: black !important;
@@ -145,10 +170,425 @@ td.non-highlighted-table-column {
background-color: #FFFFFF !important;
}
.ant-table-tbody > tr.ant-table-row:hover > td {
.ant-table-tbody>tr.ant-table-row:hover>td {
background: #FAFAFA !important;
}
.ant-table-thead .ant-table-cell {
font-weight: normal !important;
}
.CustomPopupCollapse .ant-collapse-content-box,
.CustomPopupCollapse .ant-collapse-header {
padding-left: 0 !important;
padding-top: 0 !important;
}
.system-info-panel .ant-collapse-header {
padding: 0 !important;
}
.system-info-panel .ant-collapse-content-box {
padding-left: 0 !important;
padding-right: 0 !important;
}
.peers-form .ant-layout-header {
line-height: 25px;
height: auto;
min-height: auto;
padding-bottom: 15px;
cursor: pointer;
}
.cursor-pointer {
cursor: pointer;
}
.ant-modal-content {
border-radius: 0px !important;
}
.ant-form-item-explain-error {
font-weight: 500;
font-size: 12px;
line-height: 14px;
margin-top: 2px;
}
.menlo-font,
.menlo-font * {
font-family: 'Menlo', monospace !important;
}
.arimo-font,
.arimo-font * {
font-family: 'Arimo', sans-serif !important;
}
.tag-box .ant-select-selector {
padding: 0 5px !important;
}
.tag-box .ant-select-selection-item {
width: 100%;
line-height: 20px;
justify-content: center;
list-style: none;
display: flex;
white-space: nowrap;
background: rgba(0, 0, 0, 0.02);
border: 1px solid #d9d9d9;
border-radius: 4px;
opacity: 1;
transition: all 0.2s;
text-align: start;
max-width: 40px;
height: 25px;
padding: 0 4px !important;
align-items: center;
margin-top: 3px;
text-align: center;
}
.w-100 {
width: 100%;
}
.font-500 {
font-weight: 500;
}
.page-heading {
font-weight: 500 !important;
font-size: 22px !important;
margin: 0 !important;
}
.ant-tag {
font-weight: 400 !important;
}
.react-select__indicator-separator {
display: none !important;
}
.react-select__control,
.react-select__value-container,
.react-select__input-container {
min-height: 32px !important;
padding: 0 5px !important;
max-height: 32px !important;
margin: 0 !important;
}
.react-select__value-container {
align-items: center !important;
}
.react-select__indicator {
padding: 0 5px !important;
}
.ant-badge-status-dot {
width: 8px !important;
height: 8px !important;
}
.routes-accordian {
margin-top: 20px;
}
.routes-accordian .ant-collapse-header {
background: #fff;
align-items: center !important;
}
.ant-collapse-content-box {
background: #FAFAFA;
padding: 5px 16px !important;
}
.accordian-header p {
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.85);
margin: 0;
width: 25%;
}
.headerInner {
display: flex;
height: 45px;
}
.headerInner p {
margin: 0;
width: 33.33%;
display: flex;
align-items: center;
}
.accordian-header {
display: flex;
padding: 10px 40px;
}
.accordian-inner-header {
display: flex;
padding: 10px 40px;
margin-bottom: 10px;
}
.accordian-inner-header p {
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.85);
margin: 0;
width: 20%;
}
.accordian-inner-listing {
padding: 3px 40px;
display: flex;
align-items: center;
}
.accordian-inner-listing p {
font-size: 14px;
align-items: center;
color: rgba(0, 0, 0, 0.85);
width: 10%;
margin: 0;
}
.headerInner .text-right {
display: block;
text-align: right;
padding-right: 40px;
}
.accordian-inner-listing p:nth-child(5) {
text-align: right;
}
.text-center {
text-align: center;
}
.accordian-inner-listing p:nth-child(1) {
width: 25%;
padding-right: 0;
}
.accordian-inner-header p:nth-child(1) {
width: calc(20% + 50px);
}
.accordian-inner-listing p:nth-child(2) {
width: 25%;
}
.accordian-inner-header p:nth-child(2) {
width: 25%;
}
.accordian-inner-listing p:nth-child(3) {
width: 20%;
}
.accordian-inner-listing p:nth-child(4) {
width: 20%;
}
.ant-list-item {
padding: 5px 0 !important;
margin-inline: 0 !important;
}
ul.ant-list-items {
margin-top: 5px !important;
}
.container-spinner {
margin: 20px 0;
margin-bottom: 20px;
padding: 30px 50px;
text-align: center;
background: rgb(0 0 0 / 2%);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
height: 300px;
}
.remove-bg .ant-collapse-content-box {
background-color: transparent;
}
.route-form .ant-form-item {
margin-bottom: 24px;
}
.nb-code {
position: relative;
}
.btn-copy-code.peer {
position: absolute;
top: 10px;
right: 2px;
}
.blue-color {
color: #1890FF;
}
.style-like-text .ant-select-selector {
background: transparent !important;
border: none !important;
padding: 0 !important;
color: rgba(0, 0, 0, 1) !important;
}
.style-like-text .ant-select-selection-search::after {
display: none !important;
}
.style-like-text .ant-select-selection-item {
padding-inline-end: 0 !important;
}
.style-like-text .ant-select-arrow {
display: none !important;
}
.ant-spin-nested-loading .ant-spin-spinning {
min-height: 300px;
}
.route-form.edit-form-wrapper .ant-form-item {
margin-bottom: 39px;
}
.delete-button {
cursor: auto !important;
}
.groupsSelect {
max-width: 200px;
width: 100%;
}
@media (max-width: 1220px) {
.groupsSelect {
margin-left: 0;
margin-top: 15px;
}
}
@media (max-width: 991px) {
.setting-nav {
flex-flow: column !important;
}
.setting-nav>div {
max-width: 100% !important;
}
}
.noborderPadding .ant-card-body {
padding: 0 !important;
}
.noborderPadding {
border: none !important;
box-shadow: none !important;
}
.d-none {
display: none !important;
}
.nohover {
background: transparent !important;
cursor: text;
}
.emp-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.emp-wrapper p {
margin: 0;
}
.g-r-wrapper {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.gp-main-wrapper .g-r-wrapper {
margin: 0;
}
.gp-main-wrapper {
display: flex;
gap: 5px;
align-items: center;
}
.availtooltip a {
padding: 5px 0 0;
display: inline-block;
font-size: 14px;
}
.availtooltip {
padding: 5px;
}
.availtooltip p {
color: #000;
font-size: 13.5px;
margin: 0;
}
.avail-inner {
display: flex;
align-items: flex-start;
gap: 10px;
}
.avail-inner span {
color: #1890FF;
font-size: 24px;
}
.avail-icon {
font-size: 24px;
}
.peer-avail-tooltip .ant-tooltip-inner {
color: #000;
font-size: 14px;
line-height: 24px;
padding: 25px;
}
.peer-avail-tooltip {
min-width: 400px;
}
.info-icon {
font-size: 22px;
color: #FD9349;
}

View File

@@ -10,6 +10,8 @@ import {BrowserRouter} from "react-router-dom";
import Loading from "./components/Loading";
import LoginError from "./components/LoginError";
import {AuthorityConfiguration} from "@axa-fr/react-oidc/dist/vanilla/oidc";
import InstallPage from "./views/Install";
import {ConfigProvider} from "antd";
const config = getConfig();
@@ -17,12 +19,29 @@ const config = getConfig();
// is required for doing logout. Therefore, we need to hardcode the config for auth
const auth0AuthorityConfig: AuthorityConfiguration = {
authorization_endpoint: new URL("authorize", config.authority).href,
token_endpoint: new URL("oauth/token", config.authority).href,
token_endpoint: new URL("oauth/token", config.authority).href,
revocation_endpoint: new URL("oauth/revoke", config.authority).href,
end_session_endpoint: new URL("v2/logout", config.authority).href,
userinfo_endpoint: new URL("userinfo", config.authority).href,
} as AuthorityConfiguration
const buildExtras = (config: any) => {
type Extras = { [key: string]: string }
let extras: Extras = {};
if (config.dragQueryParams) {
const searchParams = new URLSearchParams(window.location.search);
searchParams.forEach((value, key) => {
extras[key] = value
});
}
if (config.audience) {
extras.audience = config.audience
}
return extras
}
const providerConfig = {
authority: config.authority,
client_id: config.clientId,
@@ -34,7 +53,8 @@ const providerConfig = {
// service_worker_relative_url:'/OidcServiceWorker.js',
service_worker_only: false,
authority_configuration: config.auth0Auth ? auth0AuthorityConfig : undefined,
...(config.audience ? {extras: {audience: config.audience}} : null)
extras: buildExtras(config),
...(config.clientSecret ? {token_request_extras: {client_secret: config.clientSecret}} : null)
};
const root = ReactDOM.createRoot(
@@ -43,8 +63,14 @@ const root = ReactDOM.createRoot(
const loadingComponent = () => <Loading padding="3em" width={50} height={50}/>
root.render(
<OidcProvider
const showApp = () => {
if (window.location.pathname === "/install") {
// We bypass authentication for pages that do not require auth.
// E.g., when we just want to show installation steps for public.
return <InstallPage/>
}
return <OidcProvider
configuration={providerConfig}
callbackSuccessComponent={loadingComponent}
authenticatingErrorComponent={LoginError}
@@ -59,6 +85,21 @@ root.render(
<App/>
</BrowserRouter>
</OidcProvider>
}
root.render(
<ConfigProvider
theme={{
token: {
borderRadius: 4,
colorPrimary: "#1890ff",
fontFamily: "Arial"
},
components: {Badge: {fontSizeSM: 20}},
}}
>
{showApp()}
</ConfigProvider>
);
// If you want to start measuring performance in your app, pass a function

View File

@@ -1,13 +1,13 @@
import { Method } from 'axios';
import { Method } from "axios";
export interface RequestPayload<T> {
getAccessTokenSilently: any | null;
queryParams?: any | null;
payload:T;
payload: T;
}
export interface RequestHeader {
'content-type': string;
"content-type": string;
[key: string]: string;
}
@@ -28,16 +28,16 @@ export interface RequestConfig {
export interface ApiRequestParams extends RequestConfig {
method: Method;
url: string;
params?: any,
params?: any;
data: unknown;
urlBase: string;
}
export interface ApiError {
code:string;
message:string;
data?:any;
statusCode:number;
code: string;
message: string;
data?: any;
statusCode: number;
}
export interface DeleteResponse<T> {
@@ -45,7 +45,7 @@ export interface DeleteResponse<T> {
success: boolean;
failure: boolean;
error: ApiError | null;
data : T;
data: T;
}
export interface CreateResponse<T> {
@@ -53,7 +53,7 @@ export interface CreateResponse<T> {
success: boolean;
failure: boolean;
error: ApiError | null;
data : T;
data: T;
}
export interface ChangeResponse<T> {
@@ -61,5 +61,5 @@ export interface ChangeResponse<T> {
success: boolean;
failure: boolean;
error: ApiError | null;
data : T;
}
data: T;
}

View File

@@ -1,6 +1,6 @@
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
import {Account} from './types';
import {ApiError, ChangeResponse, RequestPayload} from '../../services/api-client/types';
import {ApiError, ChangeResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getAccounts: createAsyncAction(
@@ -16,6 +16,13 @@ const actions = {
)<RequestPayload<Account>, ChangeResponse<Account | null>, ChangeResponse<Account | null>>(),
setUpdateAccount: createAction('SET_UPDATED_ACCOUNT')<ChangeResponse<Account | null>>(),
resetUpdateAccount: createAction('RESET_UPDATED_ACCOUNT')<null>(),
deleteAccount: createAsyncAction(
'DELETE_ACCOUNT_REQUEST',
'DELETE_ACCOUNT_SUCCESS',
'DELETE_ACCOUNT_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
resetDeletedAccount: createAction('RESET_DELETED_ACCOUNT')<null>(),
setDeleteAccount: createAction('SET_DELETE_ACCOUNT')<DeleteResponse<string | null>>(),
};

View File

@@ -2,13 +2,14 @@ import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Account } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, ChangeResponse} from "../../services/api-client/types";
import {ApiError, ChangeResponse, DeleteResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: Account[] | null;
loading: boolean;
failed: ApiError | null;
savedAccount: ChangeResponse<Account | null>;
deletedAccount: DeleteResponse<string | null>;
}>;
const initialState: StateType = {
@@ -22,6 +23,13 @@ const initialState: StateType = {
error: null,
data : null
},
deletedAccount: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
};
const data = createReducer<Account[], ActionTypes>(initialState.data as Account[])
@@ -45,10 +53,18 @@ const updatedAccount = createReducer<ChangeResponse<Account | null>, ActionTypes
.handleAction(actions.setUpdateAccount, (store, action) => action.payload)
.handleAction(actions.resetUpdateAccount, () => initialState.savedAccount)
const deleteAccount = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedAccount)
.handleAction(actions.deleteAccount.request, () => initialState.deletedAccount)
.handleAction(actions.deleteAccount.success, (store, action) => action.payload)
.handleAction(actions.deleteAccount.failure, (store, action) => action.payload)
.handleAction(actions.setDeleteAccount, (store, action) => action.payload)
.handleAction(actions.resetDeletedAccount, () => initialState.deletedAccount)
export default combineReducers({
data,
loading,
failed,
updatedAccount
updatedAccount,
deleteAccount
});

View File

@@ -1,8 +1,9 @@
import {all, call, put, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, ChangeResponse} from '../../services/api-client/types';
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, ChangeResponse, DeleteResponse} from '../../services/api-client/types';
import {Account} from './types'
import service from './service';
import actions from './actions';
import {deletePeer} from "../peer/sagas";
export function* getAccounts(action: ReturnType<typeof actions.getAccounts.request>): Generator {
try {
@@ -55,10 +56,55 @@ export function* updateAccount(action: ReturnType<typeof actions.updateAccount.r
}
}
export function* deleteAccount(
action: ReturnType<typeof actions.deleteAccount.request>
): Generator {
try {
yield call(actions.setDeleteAccount, {
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>);
const effect = yield call(service.deleteAccount, action.payload);
const response = effect as ApiResponse<any>;
yield put(
actions.deleteAccount.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
const accounts = (yield select((state) => state.peer.data)) as Account[];
yield put(
actions.getAccounts.success(
accounts.filter((p: Account) => p.id !== action.payload.payload)
)
);
} catch (err) {
yield put(
actions.deleteAccount.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getAccounts.request, getAccounts),
takeLatest(actions.updateAccount.request, updateAccount),
takeLatest(actions.deleteAccount.request, deleteAccount),
]);
}

View File

@@ -15,5 +15,11 @@ export default {
`/api/accounts/${id}`,
payload
);
}
},
async deleteAccount(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/accounts/` + payload.payload,
payload
);
},
};

View File

@@ -1,11 +1,26 @@
import {ExpiresInValue} from "../../views/ExpiresInInput";
export interface Account {
id: string;
settings: { peer_login_expiration_enabled: boolean, peer_login_expiration: number}
id: string;
settings: {
peer_login_expiration_enabled: boolean;
peer_login_expiration: number;
jwt_groups_enabled: boolean;
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
jwt_allow_groups: string[];
extra: {
peer_approval_enabled: boolean;
}
};
}
export interface FormAccount extends Account {
peer_login_expiration_enabled: boolean
peer_login_expiration_formatted : ExpiresInValue
peer_login_expiration_enabled: boolean;
jwt_groups_enabled: boolean;
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
jwt_allow_groups: string[];
peer_login_expiration_formatted: ExpiresInValue;
peer_approval_enabled: boolean;
}

View File

@@ -1,9 +1,11 @@
export interface Event {
id: string;
timestamp: string
activity: string
activity_code: string
initiator_id: string
target_id: string
meta: { [key: string]: string }
id: string;
timestamp: string;
activity: string;
activity_code: string;
initiator_id: string;
initiator_email: string;
initiator_name: string;
target_id: string;
meta: { [key: string]: string };
}

View File

@@ -74,13 +74,13 @@ export function* setDeleteGroup(action: ReturnType<typeof actions.setDeleteGrou
export function* deleteGroup(action: ReturnType<typeof actions.deleteGroup.request>): Generator {
try {
yield call(actions.setDeleteGroup,{
yield put(actions.setDeleteGroup({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
} as DeleteResponse<string | null>))
const effect = yield call(service.deleteGroup, action.payload);
const response = effect as ApiResponse<any>;
@@ -93,8 +93,8 @@ export function* deleteGroup(action: ReturnType<typeof actions.deleteGroup.reque
data: response.body
} as DeleteResponse<string | null>));
const rules = (yield select(state => state.rule.data)) as Group[]
yield put(actions.getGroups.success(rules.filter((p:Group) => p.id !== action.payload.payload)))
const groups = (yield select(state => state.group.data)) as Group[]
yield put(actions.getGroups.success(groups.filter((p:Group) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteGroup.failure({
loading: false,

View File

@@ -5,6 +5,7 @@ import { composeWithDevTools } from 'redux-devtools-extension';
import { sagas as peerSagas } from './peer';
import { sagas as setupKeySagas } from './setup-key';
import { sagas as userSagas } from './user';
import { sagas as policySagas } from './policy';
import { sagas as ruleSagas } from './rule';
import { sagas as groupSagas } from './group';
import { sagas as routeSagas } from './route';
@@ -28,6 +29,7 @@ sagaMiddleware.run(peerSagas);
sagaMiddleware.run(setupKeySagas);
sagaMiddleware.run(userSagas);
sagaMiddleware.run(ruleSagas);
sagaMiddleware.run(policySagas);
sagaMiddleware.run(groupSagas);
sagaMiddleware.run(routeSagas);
sagaMiddleware.run(nameserverGroupSagas);
@@ -36,4 +38,4 @@ sagaMiddleware.run(dnsSettingsSagas);
sagaMiddleware.run(accountSagas);
sagaMiddleware.run(personalAccessTokenSagas);
export { apiClient, rootReducer, store };
export { apiClient, rootReducer, store };

View File

@@ -28,6 +28,7 @@ const actions = {
setNameServerGroup: createAction('SET_NameServerGroup')<NameServerGroup>(),
setSetupNewNameServerGroupVisible: createAction('SET_SETUP_NEW_NameServerGroup_VISIBLE')<boolean>(),
setSetupEditNameServerGroupVisible: createAction('SET_SETUP_EDIT_NameServerGroup_VISIBLE')<boolean>(),
setSetupNewNameServerGroupHA: createAction('SET_SETUP_NEW_NameServerGroup_HA')<boolean>()
};

View File

@@ -13,6 +13,7 @@ type StateType = Readonly<{
deleteNameServerGroup: DeleteResponse<string | null>;
savedNameServerGroup: CreateResponse<NameServerGroup | null>;
setupNewNameServerGroupVisible: boolean;
setupEditNameServerGroupVisible: boolean;
setupNewNameServerGroupHA: boolean
}>;
@@ -27,17 +28,18 @@ const initialState: StateType = {
success: false,
failure: false,
error: null,
data : null
data: null,
},
savedNameServerGroup: <CreateResponse<NameServerGroup | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
data: null,
},
setupNewNameServerGroupVisible: false,
setupNewNameServerGroupHA: false
setupEditNameServerGroupVisible: false,
setupNewNameServerGroupHA: false,
};
const data = createReducer<NameServerGroup[], ActionTypes>(initialState.data as NameServerGroup[])
@@ -79,6 +81,9 @@ const savedNameServerGroup = createReducer<CreateResponse<NameServerGroup | null
const setupNewNameServerGroupVisible = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupVisible)
.handleAction(actions.setSetupNewNameServerGroupVisible, (store, action) => action.payload)
const setupEditNameServerGroupVisible = createReducer<boolean, ActionTypes>(initialState.setupEditNameServerGroupVisible)
.handleAction(actions.setSetupEditNameServerGroupVisible, (store, action) => action.payload)
const setupNewNameServerGroupHA = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupHA)
.handleAction(actions.setSetupNewNameServerGroupHA, (store, action) => action.payload)
@@ -91,5 +96,6 @@ export default combineReducers({
deletedNameServerGroup,
savedNameServerGroup,
setupNewNameServerGroupVisible,
setupNewNameServerGroupHA
setupEditNameServerGroupVisible,
setupNewNameServerGroupHA,
});

View File

@@ -69,8 +69,9 @@ export function* saveNameServerGroup(action: ReturnType<typeof actions.saveNameS
nameservers: nameserverGroupToSave.nameservers,
groups: newGroups,
enabled: nameserverGroupToSave.enabled,
} as NameServerGroup
}
search_domains_enabled: nameserverGroupToSave.search_domains_enabled,
} as NameServerGroup,
};
let effect
if (!nameserverGroupToSave.id) {
@@ -118,13 +119,13 @@ export function* setDeleteNameServerGroup(action: ReturnType<typeof actions.set
export function* deleteNameServerGroup(action: ReturnType<typeof actions.deleteNameServerGroup.request>): Generator {
try {
yield call(actions.setDeletedNameServerGroup,{
yield put(actions.setDeletedNameServerGroup({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
}))
const effect = yield call(service.deletedNameServerGroup, action.payload);
const response = effect as ApiResponse<any>;

View File

@@ -1,21 +1,21 @@
export interface NameServerGroup {
id?: string
name: string
description: string
primary: boolean
domains: string[]
nameservers: NameServer[]
groups: string[]
enabled: boolean
id?: string;
name: string;
description: string;
primary: boolean;
domains: string[];
nameservers: NameServer[];
groups: string[];
enabled: boolean;
search_domains_enabled: boolean;
}
export interface NameServer {
ip: string
ns_type: string
port: number
ip: string;
ns_type: string;
port: number;
}
export interface NameServerGroupToSave extends NameServerGroup
{
groupsToCreate: string[]
}
export interface NameServerGroupToSave extends NameServerGroup {
groupsToCreate?: string[];
}

View File

@@ -79,11 +79,14 @@ const deletedPeer = createReducer<DeleteResponse<string | null>, ActionTypes>(in
const updateGroupsVisible = createReducer<boolean, ActionTypes>(initialState.setUpdateGroupsVisible)
.handleAction(actions.setUpdateGroupsVisible, (store, action) => action.payload)
const savedGroups = createReducer<ChangeResponse<Group[] | null>, ActionTypes>(initialState.savedGroups)
.handleAction(actions.saveGroups.request, () => initialState.savedGroups)
.handleAction(actions.saveGroups.success, (store, action) => action.payload)
.handleAction(actions.saveGroups.failure, (store, action) => action.payload)
.handleAction(actions.resetSavedGroups, () => initialState.savedGroups)
const savedGroups = createReducer<ChangeResponse<Group[] | null>, ActionTypes>(
initialState.savedGroups
)
.handleAction(actions.saveGroups.request, () => initialState.savedGroups)
.handleAction(actions.saveGroups.success, (store, action) => action.payload)
.handleAction(actions.saveGroups.failure, (store, action) => action.payload)
.handleAction(actions.setSavedGroups, (store, action) => action.payload)
.handleAction(actions.resetSavedGroups, () => initialState.savedGroups);
const updatedPeer = createReducer<CreateResponse<Peer | null>, ActionTypes>(initialState.updatedPeer)
.handleAction(actions.updatePeer.request, () => initialState.updatedPeer)

View File

@@ -1,198 +1,268 @@
import {all, call, spawn, put, select, takeLatest} from 'redux-saga/effects';
import { all, call, spawn, put, select, takeLatest } from "redux-saga/effects";
import {
ApiError,
ApiResponse,
ChangeResponse,
CreateResponse,
DeleteResponse
} from '../../services/api-client/types';
import { Peer } from './types'
import service from './service';
import actions from './actions';
import {Group, GroupPeer} from "../group/types";
DeleteResponse,
} from "../../services/api-client/types";
import { Peer } from "./types";
import service from "./service";
import actions from "./actions";
import { Group, GroupPeer } from "../group/types";
import serviceGroup from "../group/service";
import {actions as groupActions} from "../group";
export function* getPeers(action: ReturnType<typeof actions.getPeers.request>): Generator {
import { actions as groupActions } from "../group";
import userService from "../user/service";
export function* getPeers(
action: ReturnType<typeof actions.getPeers.request>
): Generator {
try {
yield put(
actions.setDeletePeer({
loading: false,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>)
);
yield put(actions.setDeletePeer({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const users: any = yield call(userService.getUsers, action.payload);
let currentUser = users.body.find((user: any) => user.is_current);
const effect = yield call(service.getPeers, action.payload);
const response = effect as ApiResponse<Peer[]>;
yield put(actions.getPeers.success(response.body));
let peersBody: any;
if (currentUser.role === "user") {
peersBody = response.body.filter((pe) => {
return pe.user_id === currentUser.id;
});
} else {
peersBody = response.body;
}
yield put(actions.getPeers.success(peersBody));
} catch (err) {
yield put(actions.getPeers.failure(err as ApiError));
}
}
export function* setDeletePeer(action: ReturnType<typeof actions.setDeletePeer>): Generator {
yield put(actions.setDeletePeer(action.payload))
export function* setDeletePeer(
action: ReturnType<typeof actions.setDeletePeer>
): Generator {
yield put(actions.setDeletePeer(action.payload));
}
export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.request>): Generator {
export function* deletePeer(
action: ReturnType<typeof actions.deletedPeer.request>
): Generator {
try {
yield call(actions.setDeletePeer,{
yield call(actions.setDeletePeer, {
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
data: null,
} as DeleteResponse<string | null>);
const effect = yield call(service.deletedPeer, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deletedPeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
yield put(
actions.deletedPeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
const peers = (yield select((state) => state.peer.data)) as Peer[];
yield put(
actions.getPeers.success(
peers.filter((p: Peer) => p.id !== action.payload.payload)
)
);
} catch (err) {
yield put(actions.deletedPeer.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
yield put(
actions.deletedPeer.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export function* saveGroups(action: ReturnType<typeof actions.saveGroups.request>): Generator {
export function* saveGroups(
action: ReturnType<typeof actions.saveGroups.request>
): Generator {
try {
yield put(actions.setSavedGroups({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))
yield put(
actions.setSavedGroups({
loading: true,
success: false,
failure: false,
error: null,
data: null,
})
);
const currentGroups = [...(yield select(state => state.group.data)) as Group[]]
const currentGroups = [
...((yield select((state) => state.group.data)) as Group[]),
];
const peerGroupsToSave = action.payload.payload
const peerGroupsToSave = action.payload.payload;
let groupsToSave = [] as Group[]
let groupsNoId = [] as Group[]
let groupsToSave = [] as Group[];
let groupsNoId = [] as Group[];
groupsToSave = groupsToSave.concat(
currentGroups
.filter(g => peerGroupsToSave.groupsToRemove.includes(g.id || ''))
.map(g => ({
id: g.id,
name: g.name,
peers: (g.peers as GroupPeer[]).filter(p => p.id !== peerGroupsToSave.ID).map(p => p.id) as string[]
}))
)
currentGroups
.filter((g) => peerGroupsToSave.groupsToRemove.includes(g.id || ""))
.map((g) => ({
id: g.id,
name: g.name,
peers: (g.peers as GroupPeer[])
.filter((p) => p.id !== peerGroupsToSave.ID)
.map((p) => p.id) as string[],
}))
);
groupsToSave = groupsToSave.concat(
currentGroups
.filter(g => peerGroupsToSave.groupsToAdd.includes(g.id || ''))
.map(g => ({
id: g.id,
name: g.name,
Peers: g.peers ? [...(g.peers as GroupPeer[]).map((p:GroupPeer) => p.id), peerGroupsToSave.ID] : [peerGroupsToSave.ID]
}))
)
currentGroups
.filter((g) => peerGroupsToSave.groupsToAdd.includes(g.id || ""))
.map((g) => ({
id: g.id,
name: g.name,
Peers: g.peers
? [
...(g.peers as GroupPeer[]).map((p: GroupPeer) => p.id),
peerGroupsToSave.ID,
]
: [peerGroupsToSave.ID],
}))
);
groupsNoId = peerGroupsToSave.groupsNoId.map(g => ({
groupsNoId = peerGroupsToSave.groupsNoId.map((g) => ({
name: g,
peers: [peerGroupsToSave.ID]
}))
peers: [peerGroupsToSave.ID],
}));
if (!groupsNoId.length && !groupsToSave.length) {
return
return;
}
const responsesGroup = yield all(groupsToSave.map(g => call(serviceGroup.editGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g
})
))
const responsesGroup = yield all(
groupsToSave.map((g) =>
call(serviceGroup.editGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g,
})
)
);
const responsesGroupNoId = yield all(groupsNoId.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g
})
))
yield put(actions.saveGroups.success({
loading: false,
success: true,
failure: false,
error: null,
data: [...(responsesGroup as ApiResponse<Group>[]).map(r => r.body), ...(responsesGroupNoId as ApiResponse<Group>[]).map(r => r.body)]
} as CreateResponse<Group[] | null>))
const responsesGroupNoId = yield all(
groupsNoId.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g,
})
)
);
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(actions.getPeers.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(
actions.saveGroups.success({
loading: false,
success: true,
failure: false,
error: null,
data: [
...(responsesGroup as ApiResponse<Group>[]).map((r) => r.body),
...(responsesGroupNoId as ApiResponse<Group>[]).map((r) => r.body),
],
} as CreateResponse<Group[] | null>)
);
yield put(
groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
yield put(
actions.getPeers.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
} catch (err) {
console.log(err)
yield put(actions.saveGroups.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Group[] | null>));
console.log(err);
yield put(
actions.saveGroups.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as ChangeResponse<Group[] | null>)
);
}
}
export function* updatePeer(action: ReturnType<typeof actions.updatePeer.request>): Generator {
export function* updatePeer(
action: ReturnType<typeof actions.updatePeer.request>
): Generator {
try {
yield put(actions.setUpdatedPeer({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))
yield put(
actions.setUpdatedPeer({
loading: true,
success: false,
failure: false,
error: null,
data: null,
})
);
const peer = action.payload.payload
const peerId = peer.id
const peer = action.payload.payload;
const peerId = peer.id;
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: peer
}
payload: peer,
};
const effect = yield call(service.updatePeer, payloadToSave)
const effect = yield call(service.updatePeer, payloadToSave);
const response = effect as ApiResponse<Peer>;
yield put(actions.updatePeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as ChangeResponse<Peer | null>));
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== peerId).concat(response.body)))
yield put(
actions.updatePeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as ChangeResponse<Peer | null>)
);
const peers = (yield select((state) => state.peer.data)) as Peer[];
yield put(
actions.getPeers.success(
peers.filter((p: Peer) => p.id !== peerId).concat(response.body)
)
);
} catch (err) {
console.log(err)
yield put(actions.updatePeer.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Peer | null>));
console.log(err);
yield put(
actions.updatePeer.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as ChangeResponse<Peer | null>)
);
}
}
@@ -201,7 +271,6 @@ export default function* sagas(): Generator {
takeLatest(actions.getPeers.request, getPeers),
takeLatest(actions.deletedPeer.request, deletePeer),
takeLatest(actions.saveGroups.request, saveGroups),
takeLatest(actions.updatePeer.request, updatePeer)
takeLatest(actions.updatePeer.request, updatePeer),
]);
}

View File

@@ -16,7 +16,8 @@ export interface Peer {
dns_label: string,
last_login: string,
login_expired: boolean,
login_expiration_enabled: boolean
login_expiration_enabled: boolean,
approval_required: boolean
}
export interface FormPeer extends Peer {

View File

@@ -0,0 +1,35 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import { Policy, PolicyToSave } from './types';
import { ApiError, CreateResponse, DeleteResponse, RequestPayload } from '../../services/api-client/types';
const actions = {
getPolicies: createAsyncAction(
'GET_POLICIES_REQUEST',
'GET_POLICIES_SUCCESS',
'GET_POLICIES_FAILURE',
)<RequestPayload<null>, Policy[], ApiError>(),
savePolicy: createAsyncAction(
'SAVE_POLICY_REQUEST',
'SAVE_POLICY_SUCCESS',
'SAVE_POLICY_FAILURE',
)<RequestPayload<PolicyToSave>, CreateResponse<Policy | null>, CreateResponse<Policy | null>>(),
setSavedPolicy: createAction('SET_CREATE_POLICY')<CreateResponse<Policy | null>>(),
resetSavedPolicy: createAction('RESET_CREATE_POLICY')<null>(),
deletePolicy: createAsyncAction(
'DELETE_POLICY_REQUEST',
'DELETE_POLICY_SUCCESS',
'DELETE_POLICY_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedPolicy: createAction('SET_DELETED_POLICY')<DeleteResponse<string | null>>(),
resetDeletedPolicy: createAction('RESET_DELETED_POLICY')<null>(),
removePolicy: createAction('REMOVE_POLICY')<string>(),
setPolicy: createAction('SET_POLICY')<Policy>(),
setSetupNewPolicyVisible: createAction('SET_SETUP_NEW_POLICY_VISIBLE')<boolean>(),
setSetupEditPolicyVisible: createAction('SET_SETUP_EDIT_POLICY_VISIBLE')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

View File

@@ -0,0 +1,95 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Policy } from './types';
import actions, { ActionTypes } from './actions';
import { ApiError, DeleteResponse, CreateResponse } from "../../services/api-client/types";
type StateType = Readonly<{
data: Policy[] | null;
policy: Policy | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletePolicy: DeleteResponse<string | null>;
savedPolicy: CreateResponse<Policy | null>;
setupNewPolicyVisible: boolean;
setupEditPolicyVisible: boolean;
}>;
const initialState: StateType = {
data: [],
policy: null,
loading: false,
failed: null,
saving: false,
deletePolicy: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data: null,
},
savedPolicy: <CreateResponse<Policy | null>>{
loading: false,
success: false,
failure: false,
error: null,
data: null,
},
setupNewPolicyVisible: false,
setupEditPolicyVisible: false,
};
const data = createReducer<Policy[], ActionTypes>(initialState.data as Policy[])
.handleAction(actions.getPolicies.success, (_, action) => action.payload)
.handleAction(actions.getPolicies.failure, () => []);
const policy = createReducer<Policy, ActionTypes>(initialState.policy as Policy)
.handleAction(actions.setPolicy, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getPolicies.request, () => true)
.handleAction(actions.getPolicies.success, () => false)
.handleAction(actions.getPolicies.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getPolicies.request, () => null)
.handleAction(actions.getPolicies.success, () => null)
.handleAction(actions.getPolicies.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getPolicies.request, () => true)
.handleAction(actions.getPolicies.success, () => false)
.handleAction(actions.getPolicies.failure, () => false);
const deletedPolicy = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletePolicy)
.handleAction(actions.deletePolicy.request, () => initialState.deletePolicy)
.handleAction(actions.deletePolicy.success, (store, action) => action.payload)
.handleAction(actions.deletePolicy.failure, (store, action) => action.payload)
.handleAction(actions.setDeletedPolicy, (store, action) => action.payload)
.handleAction(actions.resetDeletedPolicy, () => initialState.deletePolicy)
const savedPolicy = createReducer<CreateResponse<Policy | null>, ActionTypes>(initialState.savedPolicy)
.handleAction(actions.savePolicy.request, () => initialState.savedPolicy)
.handleAction(actions.savePolicy.success, (store, action) => action.payload)
.handleAction(actions.savePolicy.failure, (store, action) => action.payload)
.handleAction(actions.setSavedPolicy, (store, action) => action.payload)
.handleAction(actions.resetSavedPolicy, () => initialState.savedPolicy)
const setupNewPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewPolicyVisible)
.handleAction(actions.setSetupNewPolicyVisible, (store, action) => action.payload)
const setupEditPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupEditPolicyVisible)
.handleAction(actions.setSetupEditPolicyVisible, (store, action) => action.payload)
export default combineReducers({
data,
policy,
loading,
failed,
saving,
deletedPolicy,
savedPolicy,
setupNewPolicyVisible,
setupEditPolicyVisible,
});

225
src/store/policy/sagas.ts Normal file
View File

@@ -0,0 +1,225 @@
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import {
ApiError,
ApiResponse,
CreateResponse,
DeleteResponse,
} from "../../services/api-client/types";
import { Policy, PolicyRule } from "./types";
import service from "./service";
import serviceGroup from "../group/service";
import actions from "./actions";
import { actions as groupActions } from "../group";
import { Group } from "../group/types";
export function* getPolicies(
action: ReturnType<typeof actions.getPolicies.request>
): Generator {
try {
yield put(
actions.setDeletedPolicy({
loading: false,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>)
);
const effect = yield call(service.getPolicies, action.payload);
const response = effect as ApiResponse<Policy[]>;
yield put(actions.getPolicies.success(response.body));
} catch (err) {
yield put(actions.getPolicies.failure(err as ApiError));
}
}
export function* setCreatedPolicy(
action: ReturnType<typeof actions.setSavedPolicy>
): Generator {
yield put(actions.setSavedPolicy(action.payload));
}
function getNewGroupIds(dataString: string[], responses: Group[]): string[] {
return responses
.filter((r) => dataString.includes(r.name))
.map((r) => r.id || "");
}
export function* savePolicy(
action: ReturnType<typeof actions.savePolicy.request>
): Generator {
try {
yield put(
actions.setSavedPolicy({
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as CreateResponse<Policy | null>)
);
const policyToSave = action.payload.payload;
const groupsToSave = policyToSave.groupsToSave
? policyToSave.groupsToSave
: [];
const responsesGroup = yield all(
groupsToSave.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { name: g },
})
)
);
const resGroups = (responsesGroup as ApiResponse<Policy>[])
.filter((r) => r.statusCode === 200)
.map((r) => r.body as Group);
const currentGroups = [
...((yield select((state) => state.group.data)) as Policy[]),
];
const newGroups = [...currentGroups, ...resGroups];
yield put(groupActions.getGroups.success(newGroups));
const newSources = getNewGroupIds(
policyToSave.sourcesNoId ? policyToSave.sourcesNoId : [],
resGroups
);
const newDestinations = getNewGroupIds(
policyToSave.destinationsNoId ? policyToSave.destinationsNoId : [],
resGroups
);
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
name: policyToSave.name,
description: policyToSave.description,
enabled: policyToSave.enabled,
query: policyToSave.query,
} as Policy,
};
if (policyToSave.rules.length > 0) {
payloadToSave.payload.rules = [];
}
policyToSave.rules.forEach((r) => {
payloadToSave.payload.rules.push({
name: r.name,
description: r.description,
enabled: r.enabled,
sources: [...(r.sources as string[]), ...newSources],
destinations: [...(r.destinations as string[]), ...newDestinations],
bidirectional: r.bidirectional,
protocol: r.protocol,
ports: r.ports,
action: r.action,
} as PolicyRule);
});
let effect;
if (!policyToSave.id) {
effect = yield call(service.createPolicy, payloadToSave);
} else {
payloadToSave.payload.id = policyToSave.id;
effect = yield call(service.editPolicy, payloadToSave);
}
const response = effect as ApiResponse<Policy>;
yield put(
actions.savePolicy.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as CreateResponse<Policy | null>)
);
yield put(
groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
yield put(
actions.getPolicies.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
} catch (err) {
yield put(
actions.savePolicy.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as CreateResponse<Policy | null>)
);
}
}
export function* setDeletePolicy(
action: ReturnType<typeof actions.setDeletedPolicy>
): Generator {
yield put(actions.setDeletedPolicy(action.payload));
}
export function* deletePolicy(
action: ReturnType<typeof actions.deletePolicy.request>
): Generator {
try {
yield put(
actions.setDeletedPolicy({
loading: true,
success: false,
failure: false,
error: null,
data: null,
})
);
const effect = yield call(service.deletedPolicy, action.payload);
const response = effect as ApiResponse<any>;
yield put(
actions.deletePolicy.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
const policies = (yield select((state) => state.policy.data)) as Policy[];
yield put(
actions.getPolicies.success(
policies.filter((p: Policy) => p.id !== action.payload.payload)
)
);
} catch (err) {
yield put(
actions.deletePolicy.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getPolicies.request, getPolicies),
takeLatest(actions.savePolicy.request, savePolicy),
takeLatest(actions.deletePolicy.request, deletePolicy),
]);
}

View File

@@ -0,0 +1,32 @@
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { Policy } from './types';
export default {
async getPolicies(payload: RequestPayload<null>): Promise<ApiResponse<Policy[]>> {
return apiClient.get<Policy[]>(
`/api/policies`,
payload
);
},
async deletedPolicy(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/policies/` + payload.payload,
payload
);
},
async createPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
return apiClient.post<Policy>(
`/api/policies`,
payload
);
},
async editPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Policy>(
`/api/policies/${id}`,
payload
);
},
};

29
src/store/policy/types.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Group } from "../group/types";
export interface PolicyRule {
id?: string
name: string
description: string
enabled: boolean
sources: Group[] | string[] | null
destinations: Group[] | string[] | null
bidirectional: boolean
action: string
protocol: string
ports: string[]
}
export interface Policy {
id?: string
name: string
description: string
enabled: boolean
query: string
rules: PolicyRule[]
};
export interface PolicyToSave extends Policy {
sourcesNoId?: string[],
destinationsNoId?: string[],
groupsToSave?: string[]
};

View File

@@ -5,6 +5,7 @@ import { reducer as setupKey } from './setup-key';
import { reducer as user } from './user';
import { reducer as group } from './group';
import { reducer as rule } from './rule';
import { reducer as policy } from './policy';
import { reducer as route } from './route';
import { reducer as nameserverGroup } from './nameservers';
import { reducer as event } from './event';
@@ -13,15 +14,16 @@ import { reducer as account } from './account';
import { reducer as personalAccessToken } from './personal-access-token';
export default combineReducers({
peer,
setupKey,
user,
group,
rule,
route,
nameserverGroup,
event,
dnsSettings,
account,
personalAccessToken
peer,
setupKey,
user,
group,
rule,
policy,
route,
nameserverGroup,
event,
dnsSettings,
account,
personalAccessToken
});

View File

@@ -21,13 +21,15 @@ const actions = {
'DELETE_ROUTE_REQUEST',
'DELETE_ROUTE_SUCCESS',
'DELETE_ROUTE_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
)<RequestPayload<any>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedRoute: createAction('SET_DELETED_ROUTE')<DeleteResponse<string | null>>(),
resetDeletedRoute: createAction('RESET_DELETED_ROUTE')<null>(),
removeRoute: createAction('REMOVE_ROUTE')<string>(),
setRoute: createAction('SET_ROUTE')<Route>(),
setSetupNewRouteVisible: createAction('SET_SETUP_NEW_ROUTE_VISIBLE')<boolean>(),
setSetupEditRouteVisible: createAction('SET_SETUP_EDIT_ROUTE_VISIBLE')<boolean>(),
setSetupEditRoutePeerVisible: createAction('SET_SETUP_EDIT_ROUTE_PEER_VISIBLE')<boolean>(),
setSetupNewRouteHA: createAction('SET_SETUP_NEW_ROUTE_HA')<boolean>()
};

View File

@@ -13,7 +13,9 @@ type StateType = Readonly<{
deleteRoute: DeleteResponse<string | null>;
savedRoute: CreateResponse<Route | null>;
setupNewRouteVisible: boolean;
setupNewRouteHA: boolean
setupNewRouteHA: boolean;
setupEditRouteVisible: boolean;
setEditRoutePeerVisible: boolean;
}>;
const initialState: StateType = {
@@ -27,17 +29,19 @@ const initialState: StateType = {
success: false,
failure: false,
error: null,
data : null
data: null,
},
savedRoute: <CreateResponse<Route | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
data: null,
},
setupNewRouteVisible: false,
setupNewRouteHA: false
setupNewRouteHA: false,
setupEditRouteVisible: false,
setEditRoutePeerVisible: false,
};
const data = createReducer<Route[], ActionTypes>(initialState.data as Route[])
@@ -79,6 +83,21 @@ const savedRoute = createReducer<CreateResponse<Route | null>, ActionTypes>(init
const setupNewRouteVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRouteVisible)
.handleAction(actions.setSetupNewRouteVisible, (store, action) => action.payload)
const setupEditRouteVisible = createReducer<boolean, ActionTypes>(
initialState.setupEditRouteVisible
).handleAction(
actions.setSetupEditRouteVisible,
(store, action) => action.payload
);
const setEditRoutePeerVisible = createReducer<boolean, ActionTypes>(
initialState.setEditRoutePeerVisible
).handleAction(
actions.setSetupEditRoutePeerVisible,
(store, action) => action.payload
);
const setupNewRouteHA = createReducer<boolean, ActionTypes>(initialState.setupNewRouteHA)
.handleAction(actions.setSetupNewRouteHA, (store, action) => action.payload)
@@ -91,5 +110,7 @@ export default combineReducers({
deletedRoute,
savedRoute,
setupNewRouteVisible,
setupNewRouteHA
setupNewRouteHA,
setupEditRouteVisible,
setEditRoutePeerVisible,
});

View File

@@ -1,22 +1,30 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {Route} from './types'
import service from './service';
import actions from './actions';
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import {
ApiError,
ApiResponse,
CreateResponse,
DeleteResponse,
} from "../../services/api-client/types";
import { Route } from "./types";
import service from "./service";
import actions from "./actions";
import serviceGroup from "../group/service";
import {Group} from "../group/types";
import {actions as groupActions} from "../group";
import { Group } from "../group/types";
import { actions as groupActions } from "../group";
export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>): Generator {
export function* getRoutes(
action: ReturnType<typeof actions.getRoutes.request>
): Generator {
try {
yield put(actions.setDeletedRoute({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
yield put(
actions.setDeletedRoute({
loading: false,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>)
);
const effect = yield call(service.getRoutes, action.payload);
const response = effect as ApiResponse<Route[]>;
@@ -27,36 +35,68 @@ export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>)
}
}
export function* setCreatedRoute(action: ReturnType<typeof actions.setSavedRoute>): Generator {
yield put(actions.setSavedRoute(action.payload))
export function* setCreatedRoute(
action: ReturnType<typeof actions.setSavedRoute>
): Generator {
yield put(actions.setSavedRoute(action.payload));
}
export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>): Generator {
export function* saveRoute(
action: ReturnType<typeof actions.saveRoute.request>
): Generator {
try {
yield put(actions.setSavedRoute({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<Route | null>))
yield put(
actions.setSavedRoute({
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as CreateResponse<Route | null>)
);
const routeToSave = action.payload.payload
let groupsToCreate = routeToSave.groupsToCreate
const routeToSave = action.payload.payload;
let groupsToCreate = routeToSave.groupsToCreate;
if (!groupsToCreate) {
groupsToCreate = []
groupsToCreate = [];
}
let peerGroupsToCreate = routeToSave.peerGroupsToCreate;
if (!peerGroupsToCreate) {
peerGroupsToCreate = [];
}
// first, create groups that were newly added by user
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
const responsesGroup = yield all(
groupsToCreate.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {name: g}
payload: { name: g },
})
))
)
);
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
const newGroups = [...routeToSave.groups, ...resGroups]
const responsesPeerGroup = yield all(
peerGroupsToCreate.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { name: g },
})
)
);
const resGroups = (responsesGroup as ApiResponse<Group>[])
.filter((r) => r.statusCode === 200)
.map((g) => g.body as Group)
.map((g) => g.id);
const newGroups = [...routeToSave.groups, ...resGroups];
const resPeersGroups = (responsesPeerGroup as ApiResponse<Group>[])
.filter((r) => r.statusCode === 200)
.map((g) => g.body as Group)
.map((g) => g.id);
const newPeerGroups = [...routeToSave?.peer_groups || [], ...resPeersGroups];
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
@@ -69,86 +109,145 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
network: routeToSave.network,
network_id: routeToSave.network_id,
peer: routeToSave.peer,
groups: newGroups
} as Route
}
peer_groups: newPeerGroups.length ? newPeerGroups : null,
groups: newGroups,
} as Route,
};
let effect
let effect;
if (!routeToSave.id) {
effect = yield call(service.createRoute, payloadToSave);
effect = yield call(service.createRoute, payloadToSave);
} else {
payloadToSave.payload.id = routeToSave.id
payloadToSave.payload.id = routeToSave.id;
effect = yield call(service.editRoute, payloadToSave);
}
const response = effect as ApiResponse<Route>;
yield put(actions.saveRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<Route | null>));
yield put(
actions.saveRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as CreateResponse<Route | null>)
);
yield put(groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
yield put(actions.getRoutes.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(
groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
yield put(
actions.getRoutes.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
} catch (err) {
yield put(groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
yield put(
groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
yield put(actions.saveRoute.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as CreateResponse<Route | null>));
yield put(
actions.saveRoute.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as CreateResponse<Route | null>)
);
}
}
export function* setDeleteRoute(action: ReturnType<typeof actions.setDeletedRoute>): Generator {
yield put(actions.setDeletedRoute(action.payload))
export function* setDeleteRoute(
action: ReturnType<typeof actions.setDeletedRoute>
): Generator {
yield put(actions.setDeletedRoute(action.payload));
}
export function* deleteRoute(action: ReturnType<typeof actions.deleteRoute.request>): Generator {
export function* deleteRoute(
action: ReturnType<typeof actions.deleteRoute.request>
): Generator {
try {
yield call(actions.setDeletedRoute,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
yield put(
actions.setDeletedRoute({
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>)
);
const effect = yield call(service.deletedRoute, action.payload);
const response = effect as ApiResponse<any>;
const payloadType = typeof action.payload.payload;
yield put(actions.deleteRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
if (payloadType === "string") {
const effect = yield call(service.deletedRoute, action.payload);
const response = effect as ApiResponse<any>;
const routes = (yield select(state => state.route.data)) as Route[]
yield put(actions.getRoutes.success(routes.filter((p:Route) => p.id !== action.payload.payload)))
yield put(
actions.deleteRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
const routes = (yield select((state) => state.route.data)) as Route[];
yield put(
actions.getRoutes.success(
routes.filter((p: Route) => p.id !== action.payload.payload)
)
);
} else {
const effect = yield all(
action.payload.payload.map((g: string) =>
call(service.deletedRoute, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g,
})
)
);
const response = effect as Array<ApiResponse<any>>;
yield put(
actions.deleteRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response[0].body,
} as DeleteResponse<string | null>)
);
const routes = (yield select((state) => state.route.data)) as Route[];
yield put(
actions.getRoutes.success(
routes.filter((p: Route) => !action.payload.payload.includes(p.id))
)
);
}
} catch (err) {
yield put(actions.deleteRoute.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
yield put(
actions.deleteRoute.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
@@ -156,7 +255,6 @@ export default function* sagas(): Generator {
yield all([
takeLatest(actions.getRoutes.request, getRoutes),
takeLatest(actions.saveRoute.request, saveRoute),
takeLatest(actions.deleteRoute.request, deleteRoute)
takeLatest(actions.deleteRoute.request, deleteRoute),
]);
}

View File

@@ -1,18 +1,22 @@
export interface Route {
id?: string
description: string
enabled: boolean
peer: string
network: string
network_id: string
network_type?: string
metric?: number
masquerade: boolean
groups: string[]
id?: string | null;
description: string;
enabled: boolean;
peer: string;
network: string;
network_id: string;
network_type?: string;
metric?: number;
masquerade: boolean;
groups: string[];
peer_groups?: string[];
routesGroups?: string[];
groupedRoutes?: Array<any>;
}
export interface RouteToSave extends Route
{
groupsToCreate: string[]
peerGroupsToCreate: string[]
}

View File

@@ -1,33 +1,32 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { Rule } from './types';
import {SetupKey} from "../setup-key/types";
export default {
async getRules(payload:RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
return apiClient.get<Rule[]>(
`/api/rules`,
payload
);
},
async deletedRule(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/rules/` + payload.payload,
payload
);
},
async createRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
return apiClient.post<Rule>(
`/api/rules`,
payload
);
},
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Rule>(
`/api/rules/${id}`,
payload
);
},
async getRules(payload: RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
return apiClient.get<Rule[]>(
`/api/rules`,
payload
);
},
async deletedRule(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/rules/` + payload.payload,
payload
);
},
async createRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
return apiClient.post<Rule>(
`/api/rules`,
payload
);
},
async editRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Rule>(
`/api/rules/${id}`,
payload
);
},
};

View File

@@ -1,4 +1,4 @@
import {Group} from "../group/types";
import { Group } from "../group/types";
export interface Rule {
id?: string
@@ -7,6 +7,8 @@ export interface Rule {
sources: Group[] | string[] | null
destinations: Group[] | string[] | null
flow: string
protocol: string
ports: string[]
disabled: boolean
}
@@ -14,4 +16,4 @@ export interface RuleToSave extends Rule {
sourcesNoId: string[],
destinationsNoId: string[],
groupsToSave: string[]
}
}

View File

@@ -32,7 +32,8 @@ const actions = {
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>()
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>(),
setSetupEditKeyVisible: createAction('SET_SETUP_EDIT_KEY_VISIBLE')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;

View File

@@ -5,15 +5,16 @@ import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: SetupKey[] | null;
setupKey: SetupKey | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletedSetupKey: DeleteResponse<string | null>;
revokedSetupKey: ChangeResponse<SetupKey | null>;
savedSetupKey: CreateResponse<SetupKey | null>;
setupNewKeyVisible: boolean
data: SetupKey[] | null;
setupKey: SetupKey | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletedSetupKey: DeleteResponse<string | null>;
revokedSetupKey: ChangeResponse<SetupKey | null>;
savedSetupKey: CreateResponse<SetupKey | null>;
setupNewKeyVisible: boolean;
setupEditKeyVisible: boolean;
}>;
const initialState: StateType = {
@@ -43,7 +44,8 @@ const initialState: StateType = {
error: null,
data : null
},
setupNewKeyVisible: false
setupNewKeyVisible: false,
setupEditKeyVisible: false
};
const data = createReducer<SetupKey[], ActionTypes>(initialState.data as SetupKey[])
@@ -85,13 +87,17 @@ const savedSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes
const setupNewKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewKeyVisible)
.handleAction(actions.setSetupNewKeyVisible, (store, action) => action.payload)
const setupEditKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupEditKeyVisible)
.handleAction(actions.setSetupEditKeyVisible, (store, action) => action.payload)
export default combineReducers({
data,
setupKey,
loading,
failed,
saving,
deletedSetupKey,
savedSetupKey: savedSetupKey,
setupNewKeyVisible
data,
setupKey,
loading,
failed,
saving,
deletedSetupKey,
savedSetupKey: savedSetupKey,
setupNewKeyVisible,
setupEditKeyVisible,
});

View File

@@ -63,7 +63,8 @@ export function* saveSetupKey(action: ReturnType<typeof actions.saveSetupKey.req
auto_groups: newGroups,
type: keyToSave.type,
expires_in: keyToSave.expires_in,
usage_limit: keyToSave.usage_limit
usage_limit: keyToSave.usage_limit,
ephemeral: keyToSave.ephemeral
} as SetupKeyToSave
});
} else {

View File

@@ -1,4 +1,3 @@
import {ExpiresInValue} from "../../views/ExpiresInInput";
import moment from "moment";
export interface SetupKey {
@@ -14,12 +13,12 @@ export interface SetupKey {
valid: boolean;
auto_groups: string[]
expires_in: number;
usage_limit: number;
usage_limit: any;
ephemeral: boolean;
}
export interface FormSetupKey extends SetupKey {
autoGroupNames: string[]
expiresInFormatted: ExpiresInValue
exp: moment.Moment
last: moment.Moment
}

View File

@@ -1,54 +1,72 @@
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
import {User, UserToSave} from './types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import { ActionType, createAction, createAsyncAction } from "typesafe-actions";
import { User, UserToSave } from "./types";
import {
ApiError,
CreateResponse,
DeleteResponse,
RequestPayload,
} from "../../services/api-client/types";
const actions = {
getUsers: createAsyncAction(
'GET_USERS_REQUEST',
'GET_USERS_SUCCESS',
'GET_USERS_FAILURE',
"GET_USERS_REQUEST",
"GET_USERS_SUCCESS",
"GET_USERS_FAILURE"
)<RequestPayload<null>, User[], ApiError>(),
getServiceUsers: createAsyncAction(
'GET_SERVICE_USERS_REQUEST',
'GET_SERVICE_USERS_SUCCESS',
'GET_SERVICE_USERS_FAILURE',
"GET_SERVICE_USERS_REQUEST",
"GET_SERVICE_USERS_SUCCESS",
"GET_SERVICE_USERS_FAILURE"
)<RequestPayload<null>, User[], ApiError>(),
getRegularUsers: createAsyncAction(
'GET_REGULAR_USERS_REQUEST',
'GET_REGULAR_USERS_SUCCESS',
'GET_REGULAR_USERS_FAILURE',
"GET_REGULAR_USERS_REQUEST",
"GET_REGULAR_USERS_SUCCESS",
"GET_REGULAR_USERS_FAILURE"
)<RequestPayload<null>, User[], ApiError>(),
deleteUser: createAsyncAction(
'DELETE_USER_REQUEST',
'DELETE_USER_SUCCESS',
'DELETE_USER_FAILURE',
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedUser: createAction('SET_DELETED_USER')<DeleteResponse<string | null>>(),
resetDeletedUser: createAction('RESET_DELETED_USER')<null>(),
"DELETE_USER_REQUEST",
"DELETE_USER_SUCCESS",
"DELETE_USER_FAILURE"
)<
RequestPayload<string>,
DeleteResponse<string | null>,
DeleteResponse<string | null>
>(),
setDeletedUser:
createAction("SET_DELETED_USER")<DeleteResponse<string | null>>(),
resetDeletedUser: createAction("RESET_DELETED_USER")<null>(),
// used to set a user object that was picked in the user table in the UserUpdate drawer (user update window on right-side).
setUser: createAction('SET_USER')<User>(),
setUser: createAction("SET_USER")<User>(),
// used to make the UserUpdate drawer visible in the UI.
setUpdateUserDrawerVisible: createAction('SET_UPDATE_USER_VISIBLE')<boolean>(),
setUpdateUserDrawerVisible: createAction(
"SET_UPDATE_USER_VISIBLE"
)<boolean>(),
// used to make the ViewUserPopup visible in the UI.
setInviteUserPopupVisible: createAction('SET_INVITE_USER_VISIBLE')<boolean>(),
setInviteUserPopupVisible: createAction("SET_INVITE_USER_VISIBLE")<boolean>(),
// used to make the EditUserPopup visible in the UI.
setEditUserPopupVisible: createAction('SET_EDIT_USER_VISIBLE')<boolean>(),
setEditUserPopupVisible: createAction("SET_EDIT_USER_VISIBLE")<boolean>(),
// used to make the AddServiceUserPopup visible in the UI.
setAddServiceUserPopupVisible: createAction('SET_ADD_SERVICE_USER_VISIBLE')<boolean>(),
setAddServiceUserPopupVisible: createAction(
"SET_ADD_SERVICE_USER_VISIBLE"
)<boolean>(),
// used to remember what tab was open on users page
setUserTabOpen: createAction('SET_USER_TAB_OPEN')<string>(),
setUserTabOpen: createAction("SET_USER_TAB_OPEN")<string>(),
saveUser: createAsyncAction(
'SAVE_USER_REQUEST',
'SAVE_USER_SUCCESS',
'SAVE_USER_FAILURE',
)<RequestPayload<UserToSave>, CreateResponse<User | null>, CreateResponse<User | null>>(),
setSavedUser: createAction('SET_SAVED_USER')<CreateResponse<User | null>>(),
resetSavedUser: createAction('RESET_SAVED_USER')<null>(),
"SAVE_USER_REQUEST",
"SAVE_USER_SUCCESS",
"SAVE_USER_FAILURE"
)<
RequestPayload<UserToSave>,
CreateResponse<User | null>,
CreateResponse<User | null>
>(),
setSavedUser: createAction("SET_SAVED_USER")<CreateResponse<User | null>>(),
resetSavedUser: createAction("RESET_SAVED_USER")<null>(),
};
export type ActionTypes = ActionType<typeof actions>;

View File

@@ -1,169 +1,183 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {User, UserToSave} from './types'
import service from './service';
import actions from './actions';
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import {
ApiError,
ApiResponse,
CreateResponse,
DeleteResponse,
} from "../../services/api-client/types";
import { User, UserToSave } from "./types";
import service from "./service";
import actions from "./actions";
import serviceGroup from "../group/service";
import {Group} from "../group/types";
import {actions as groupActions} from "../group";
import {PersonalAccessToken} from "../personal-access-token/types";
import { Group } from "../group/types";
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
try {
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
export function* getUsers(
action: ReturnType<typeof actions.getUsers.request>
): Generator {
try {
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
yield put(actions.getUsers.success(response.body));
} catch (err) {
yield put(actions.getUsers.failure(err as ApiError));
}
yield put(actions.getUsers.success(response.body));
} catch (err) {
yield put(actions.getUsers.failure(err as ApiError));
}
}
export function* getServiceUsers(action: ReturnType<typeof actions.getServiceUsers.request>): Generator {
try {
action.payload.queryParams = {service_user: true}
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
export function* getServiceUsers(
action: ReturnType<typeof actions.getServiceUsers.request>
): Generator {
try {
action.payload.queryParams = { service_user: true };
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
yield put(actions.getServiceUsers.success(response.body));
} catch (err) {
yield put(actions.getServiceUsers.failure(err as ApiError));
}
yield put(actions.getServiceUsers.success(response.body));
} catch (err) {
yield put(actions.getServiceUsers.failure(err as ApiError));
}
}
export function* getRegularUsers(action: ReturnType<typeof actions.getRegularUsers.request>): Generator {
try {
action.payload.queryParams = {service_user: false}
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
export function* getRegularUsers(
action: ReturnType<typeof actions.getRegularUsers.request>
): Generator {
try {
action.payload.queryParams = { service_user: false };
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
yield put(actions.getRegularUsers.success(response.body));
} catch (err) {
yield put(actions.getRegularUsers.failure(err as ApiError));
}
yield put(actions.getRegularUsers.success(response.body));
} catch (err) {
yield put(actions.getRegularUsers.failure(err as ApiError));
}
}
export function* saveUser(action: ReturnType<typeof actions.saveUser.request>): Generator {
try {
yield put(actions.setSavedUser({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<User | null>))
export function* saveUser(
action: ReturnType<typeof actions.saveUser.request>
): Generator {
try {
yield put(
actions.setSavedUser({
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as CreateResponse<User | null>)
);
const userToSave = action.payload.payload
const userToSave = action.payload.payload;
let groupsToCreate = userToSave.groupsToCreate
if (!groupsToCreate) {
groupsToCreate = []
}
// first, create groups that were newly added by user
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {name: g}
})
))
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
const newGroups = [...userToSave.auto_groups, ...resGroups]
let payload = {
name: userToSave.name,
email: userToSave.email,
role: userToSave.role,
auto_groups: newGroups,
is_service_user: userToSave.is_service_user
} as UserToSave
let effect
if (!userToSave.id) {
effect = yield call(service.createUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload
});
} else {
payload.id = userToSave.id
effect = yield call(service.editUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload
});
}
const response = effect as ApiResponse<User>;
yield put(actions.saveUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<User | null>));
yield put(groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
yield put(actions.getUsers.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
} catch (err) {
yield put(actions.saveUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<User | null>));
let groupsToCreate = userToSave.groupsToCreate;
if (!groupsToCreate) {
groupsToCreate = [];
}
// first, create groups that were newly added by user
const responsesGroup = yield all(
groupsToCreate.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { name: g },
})
)
);
const resGroups = (responsesGroup as ApiResponse<Group>[])
.filter((r) => r.statusCode === 200)
.map((g) => g.body as Group)
.map((g) => g.id);
const newGroups = [...userToSave.auto_groups, ...resGroups];
let payload = {
name: userToSave.name,
email: userToSave.email,
role: userToSave.role,
auto_groups: newGroups,
is_service_user: userToSave.is_service_user,
is_blocked: userToSave.is_blocked,
} as UserToSave;
let effect;
if (!userToSave.id) {
effect = yield call(service.createUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload,
});
} else {
payload.id = userToSave.id;
effect = yield call(service.editUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload,
});
}
const response = effect as ApiResponse<User>;
yield put(
actions.saveUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as CreateResponse<User | null>)
);
} catch (err) {
yield put(
actions.saveUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as CreateResponse<User | null>)
);
}
}
export function* deleteUser(action: ReturnType<typeof actions.deleteUser.request>): Generator {
try {
yield call(actions.setDeletedUser,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
export function* deleteUser(
action: ReturnType<typeof actions.deleteUser.request>
): Generator {
try {
yield put(
actions.setDeletedUser({
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>)
);
const effect = yield call(service.deleteUser, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deleteUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const users = (yield select(state => state.users.data)) as User[]
const regularUsers = (yield select(state => state.users.regularUsers)) as User[]
const serviceUsers = (yield select(state => state.users.serviceUsers)) as User[]
yield put(actions.getUsers.success(users.filter((p:User) => p.id !== action.payload.payload)))
yield put(actions.getRegularUsers.success(regularUsers.filter((p:User) => p.id !== action.payload.payload)))
yield put(actions.getServiceUsers.success(serviceUsers.filter((p:User) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
const effect = yield call(service.deleteUser, action.payload);
const response = effect as ApiResponse<any>;
yield put(
actions.deleteUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
} catch (err) {
yield put(
actions.deleteUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getUsers.request, getUsers),
takeLatest(actions.getServiceUsers.request, getServiceUsers),
takeLatest(actions.getRegularUsers.request, getRegularUsers),
takeLatest(actions.saveUser.request, saveUser),
takeLatest(actions.deleteUser.request, deleteUser)
]);
yield all([
takeLatest(actions.getUsers.request, getUsers),
takeLatest(actions.getServiceUsers.request, getServiceUsers),
takeLatest(actions.getRegularUsers.request, getRegularUsers),
takeLatest(actions.saveUser.request, saveUser),
takeLatest(actions.deleteUser.request, deleteUser),
]);
}

View File

@@ -7,10 +7,13 @@ export interface User {
auto_groups: string[];
is_current?: boolean;
is_service_user?: boolean;
is_blocked?: boolean;
last_login?: string;
}
export interface FormUser extends User {
autoGroupsNames: string[]
is_active?: boolean
}
export interface UserToSave extends User {

View File

@@ -1,15 +1,23 @@
export const formatOS = (os) => {
if (os.startsWith("windows 10")) {
if (os.toLowerCase().startsWith("windows 10")) {
return "Windows 10";
}
if (os.toLowerCase().startsWith("windows server 10")) {
return "Windows Server";
}
if (os.startsWith("Darwin")) {
return os.replace("Darwin", "MacOS");
}
// capitalize first letter
os = os.charAt(0).toUpperCase() + os.slice(1);
if (!os.startsWith("iOS")) {
// capitalize first letter
os = os.charAt(0).toUpperCase() + os.slice(1);
}
return os;
};
@@ -59,22 +67,22 @@ function getFormattedDate(date, preformattedDate = false, hideYear = false) {
if (minutes < 10) {
// Adding leading zero to minutes
minutes = `0${ minutes }`;
minutes = `0${minutes}`;
}
if (preformattedDate) {
// Today
// Yesterday
return `${ preformattedDate }`;
return `${preformattedDate}`;
}
if (hideYear) {
// 10. January
return `${ day }. ${ month }`;
return `${day}. ${month}`;
}
// 10. January 2017.
return `${ day }. ${ month } ${ year }`;
return `${day}. ${month} ${year}`;
}
export const fullDate = (dateParam) => {
@@ -100,24 +108,26 @@ export const timeAgo = (dateParam) => {
const isToday = today.toDateString() === date.toDateString();
const isYesterday = yesterday.toDateString() === date.toDateString();
const isThisYear = today.getFullYear() === date.getFullYear();
const never = date.getFullYear() === 1;
if (seconds < -1) {
return getFormattedDate(date, false, true);
} else if (seconds < 5) {
return 'just now';
} else if (seconds < 60) {
return `${ seconds } seconds ago`;
return `${seconds} seconds ago`;
} else if (seconds < 90) {
return 'about a minute ago';
} else if (minutes < 60) {
return `${ minutes } minutes ago`;
return `${minutes} minutes ago`;
} else if (isToday) {
return getFormattedDate(date, 'today'); // Today at 10:20
} else if (isYesterday) {
return getFormattedDate(date, 'yesterday'); // Yesterday at 10:20
} else if (isThisYear) {
return getFormattedDate(date, false, true); // 10. January at 10:20
} else if (never) {
return 'never';
}
return getFormattedDate(date); // 10. January 2017. at 10:20
@@ -133,4 +143,19 @@ export const isNetBirdHosted = () => {
export const isLocalDev = () => {
return window.location.hostname.includes("localhost")
}
}
const domainRegex =
/^(?!.*\s)[a-zA-Z0-9](?!.*\s$)(?!.*\.$)(?:(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.){1,126}(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$/;
export const domainValidator = (_, domain) => {
if (domainRegex.test(domain)) {
return Promise.resolve();
}
// setIsPrimary(false);
return Promise.reject(
new Error(
"Please enter a valid domain, e.g. example.com or intra.example.com"
)
);
};

21
src/utils/filterState.js Normal file
View File

@@ -0,0 +1,21 @@
export const storeFilterState = (page, filterName, filterValue) => {
let filterStateObj = {}
const getValue = localStorage.getItem(page)
if (getValue) {
filterStateObj = JSON.parse(getValue)
}
filterStateObj[filterName] = filterValue
localStorage.setItem(page, JSON.stringify(filterStateObj))
}
export const getFilterState = (page, filterName) => {
const getFilterObject = JSON.parse(localStorage.getItem(page))
if (getFilterObject) {
return getFilterObject[filterName]
}
return null
}

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