Compare commits

...

91 Commits

Author SHA1 Message Date
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
pascal-fischer
ac42a17b11 remove unnecessary oidc user check from users page (#166)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-04-27 13:28:50 +02:00
pascal-fischer
77ca3c6fde Adding service user support and new user overview (#164)
Changes:
user tab was split in service users and regular users
user edit view was reworked:
shows PAT box for service users and self (hides for rest)
hides email and groups for service users as no usage
reverted settings tab to only contain account settings + hide for normal users
Use navbar avatar dropdown to link to users list and open user edit page for self
extend api-client to handle requests with query parameters
use popup form for PAT creation, user invite and service user creation
validate all form fields before trying to send API call and show faulty fields

Additional fixes:
groups popup was only visible after 2nd hover after tab switch on every view, fixed to also show on first hover
fix setup keys page throwing errors from time to time and not loading
peers view was sending getRoute requests that are only allowed for admins which was throwing errors (only console) for normal users -> disabled requests for non-admin users
2023-04-22 12:57:17 +02:00
pascal-fischer
20e24b4ede Fix Routes view when updating routes (#165)
* fix filter

* filter also for android

* fix masquerade Traffic button

* remove log
2023-04-21 13:07:54 +02:00
Misha Bragin
7a29dac01c Add single line installer command for Mac and Linux (#162)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-04-04 14:20:02 +02:00
pascal-fischer
444e9ec44a fetch userID from user list and not oidc by default (#161) 2023-04-03 16:52:42 +02:00
pascal-fischer
dff0313f82 Feature/pat support (#157)
* Add working UI + API calls [missing token popup]

* use popup view to add new token

* show userID if user name not available

* switch from description to name

* show "Me" instead of own name

* removed created_by column

* update add token explanation

* use object instead of plain text for token create response

* some style changes

* disable information button for tokens

* last_used can contain nil

* fix delete popups

* lower case letters for dates

* add activity and fix visibility

* show settings tab for non admins

* remove spaces on top of setting tabs

* fix copy button size and position

* fix list footers

* continue merge changes to new files
2023-04-03 12:29:40 +02:00
Maycon Santos
11fbfb336a Clean last estimated name (#160) 2023-04-03 12:16:12 +02:00
Maycon Santos
4a0ae8f27d Add missing config change (#159) 2023-04-01 21:58:46 +02:00
Maycon Santos
9a72d8b0c4 Allow defining API token source (#158)
On many IDP providers, the access token
 is used to access the IDP's own API

 With these changes, we allow users to define the proper token to be used for
 management API calls
2023-04-01 19:44:28 +02:00
Misha Bragin
8e038cf242 Make table headers font-weight normal (#156) 2023-03-27 15:59:18 +02:00
Misha Bragin
5bd94eff56 Fix add peer popup tab layout on mobile (#154)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-03-23 12:10:27 +01:00
Misha Bragin
77f065b093 Update theme border radius (#152) 2023-03-22 16:56:36 +01:00
Misha Bragin
a4d55cfb90 Fix settings modal styles (#151) 2023-03-22 16:14:49 +01:00
Misha Bragin
cfd4c9075b Add onboarding steps (#150) 2023-03-22 15:21:52 +01:00
braginini
962180030a Fix activity docs link 2023-03-15 14:20:48 +01:00
Misha Bragin
485e1e8d79 Add more references to docs (#149) 2023-03-15 14:18:22 +01:00
Givi Khojanashvili
b11007b29f Add policy add activities (#147)
Related changes for netbirdio/netbird#700
2023-03-13 10:58:34 +01:00
Maycon Santos
bce75c1ca9 Disable banner (#146)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-03-02 15:55:31 +01:00
Maycon Santos
0c09992b38 Use user.is_current (#145)
prevent update for own user

use the is_current for label

removed unused imports
2023-03-01 22:50:26 +01:00
Maycon Santos
86b12f30d2 Use new windows release URL (#144)
removed latestVersion logic and environment support
2023-03-01 15:00:36 +01:00
Maycon Santos
d3e34d8448 Use single page size helper with new larger values (#143)
Reduce duplicated code with single helper for pagination

On larger deployments we should allow for larger pages

fix popover key issue and vertical spacing

update deprecated keys
2023-02-28 16:32:12 +01:00
Misha Bragin
76083168f6 Log error when activity event is not handled (#142) 2023-02-28 11:22:40 +01:00
Misha Bragin
a54b3687ae Display user in the user role update event (#141)
Handling of the "User role updated" event (user.role.update) 
was missing in the Activity tab.
2023-02-27 15:27:00 +01:00
Zoltan Papp
25f154dc83 Capitalize first letter of OS (#140) 2023-02-27 14:54:39 +01:00
pascal-fischer
f3c7d877f8 Add additional step in the Mac installation instructions to start the daemon (#138) 2023-02-22 20:55:36 +01:00
Misha Bragin
7cea7e7f54 Format individual peer login expiration event (#139)
Events peer.login.expiration.disable and peer.login.expiration.enable
are handled in the Activity tab now and properly displayed.
2023-02-22 20:54:56 +01:00
Misha Bragin
aaa351635f Add peer expiration setting confirmation modal (#137)
Add a confirmation dialog to notify a user of possible
consequences of the peer login expiration enabling/disabling.
2023-02-21 08:47:14 +01:00
Misha Bragin
379ff5486e Account settings view (#136)
New Settings tab added to the dashboard.
It is possible to enable or disable peer login expiration globally for an account.
As well as defining the expiration time period.
2023-02-20 08:57:24 +01:00
Misha Bragin
8bcd9918e2 Fix expires in input filed of the setup key (#135) 2023-02-17 14:30:22 +01:00
Misha Bragin
044ccd0ce6 Display login expiration activities (#134) 2023-02-16 15:36:45 +01:00
braginini
ab09ca3697 Display API error in peers view 2023-02-16 13:03:39 +01:00
Misha Bragin
1644ed5dce Add peer login expiration tooltip (#133) 2023-02-16 12:47:35 +01:00
Misha Bragin
cea459792f Add peer login expiration (#132)
Display if peer login has expired in the Peers table
Enable/Disable peer login expiration
2023-02-16 12:18:15 +01:00
Maycon Santos
a402680816 Disallow all crawlers and dashboard indexing (#131)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-02-10 18:45:11 +01:00
Givi Khojanashvili
e57e5b726d Feat add custom id claim (#129)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* Fix management API endpoint ENV var. Format README.

* Add and use id_current user flag

* Use mix of the new and old methods to detect current user.
2023-02-03 21:48:03 +01:00
Misha Bragin
2c4ada0ad8 Use peerID in the Routes view (#130)
Relates to the netbirdio/netbird PR
Use Peer.ID instead of Peer.Key as peer identifier (#664)
2023-02-03 10:34:43 +01:00
Misha Bragin
8195587c85 Handle additional activity events (#128)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-01-31 13:56:46 +01:00
Maycon Santos
adf6e1e71f Use ID token payload when oidcUser is nil (#127)
With some IDPs like MS Azure the
oidcUser is not being set, so we can fallback to id token
to validate UI and current user
2023-01-24 09:10:14 +01:00
Maycon Santos
b733a186ae Remove console.log in peers page (#125)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-01-19 14:55:42 +01:00
Maycon Santos
5d901470c2 Feature/dns settings (#126)
Add DNS settings view to the DNS tab.

Split the page into sub-tabs for Nameservers and DNS settings

Added API calls to the new DNS settings API
2023-01-18 18:12:29 +01:00
Misha Bragin
29ab28847d Add Events view (#119) 2023-01-02 17:29:11 +01:00
Maycon Santos
0361825e04 Add peer's last seem time on popover (#122)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2022-12-16 19:37:10 +01:00
Moath Qasim
2fa33ec06a Fix MacOS custom netbird up command (#121)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2022-12-11 18:54:48 +01:00
Maycon Santos
c677eeaae4 Add distribution groups to Network routes (#118)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Users can add distribution groups to network routes

Groups can be added to individual network routes or to all routes in the group

Adding a new group in the modal is restricted to individual network route operations
2022-12-08 17:24:34 +01:00
122 changed files with 16655 additions and 5844 deletions

View File

@@ -36,24 +36,43 @@ Disclaimer. We believe that proper user management system is not a trivial task
use Auth0 service that covers all our needs (user management, social login, JTW for the management API).
Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
1. install [Docker](https://docs.docker.com/get-docker/)
2. register [Auth0](https://auth0.com/) account
3. running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
1. Install [Docker](https://docs.docker.com/get-docker/)
2. Register [Auth0](https://auth0.com/) account
3. Running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
```AUTH0_DOMAIN``` ```AUTH0_CLIENT_ID``` ```AUTH0_AUDIENCE```
`AUTH0_DOMAIN` `AUTH0_CLIENT_ID` `AUTH0_AUDIENCE`
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Configure Allowed Web Origins"
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting ```WIRETRUSTEE_MGMT_API_ENDPOINT``` is required. Most likely it will be ```http://localhost:33071``` if you are hosting Management API on the same server.
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
5. Run docker container without SSL (Let's Encrypt):
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
```shell
docker run -d --name wiretrustee-dashboard \
--rm -p 80:80 -p 443:443 \
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
wiretrustee/dashboard:main
```
6. Run docker container with SSL (Let's Encrypt):
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e NGINX_SSL_PORT=443 -e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> -e LETSENCRYPT_EMAIL=<YOUR EMAIL> -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
```shell
docker run -d --name wiretrustee-dashboard \
--rm -p 80:80 -p 443:443 \
-e NGINX_SSL_PORT=443 \
-e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> \
-e LETSENCRYPT_EMAIL=<YOUR EMAIL> \
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
-e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
wiretrustee/dashboard:main
```
## How to run local development
1. Install node 16
2. create and update the src/.local-config.json file. This file should contain values to be replaced from src/config.json
2. create and update the `src/.local-config.json` file. This file should contain values to be replaced from `src/config.json`
3. run `npm install`
4. run `npm run start dev`
4. run `npm run start dev`

View File

@@ -43,6 +43,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}
@@ -52,14 +53,12 @@ export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api o
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
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}
REPO="https://github.com/netbirdio/netbird/"
# this command will fetch the latest release e.g. v0.6.3
export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest))
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_LATEST_VERSION \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI"
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"
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"

1580
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/icons": "^4.8.0",
"@axa-fr/react-oidc": "^5.14.0",
"@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.4",
@@ -18,7 +18,7 @@
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/styled-components": "^5.1.25",
"antd": "^4.20.6",
"antd": "^5.3.1",
"autoprefixer": "^10.4.4",
"axios": "^0.27.2",
"cidr-regex": "^3.1.1",
@@ -27,16 +27,19 @@
"highlight.js": "^11.2.0",
"history": "^5.0.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"postcss": "^8.4.12",
"prop-types": "^15.7.2",
"punycode": "^2.1.1",
"rc-overflow": "^1.2.8",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.1.0",
"react-hotjar": "^5.1.0",
"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",

View File

@@ -5,6 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="robots" content="noindex">
<meta
name="description"
content="NetBird Management Dashboard"

View File

@@ -1,3 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
Disallow: /

View File

@@ -4,7 +4,7 @@ import {apiClient, store} from "./store";
import {hotjar} from 'react-hotjar';
import {getConfig} from "./config";
import Banner from "./components/Banner";
import {Col, Layout, Row} from "antd";
import {Col, ConfigProvider, Layout, Row} from "antd";
import {Container} from "./components/Container";
import Navbar from "./components/Navbar";
import {Redirect, Route, Switch} from "react-router-dom";
@@ -16,11 +16,12 @@ import SetupKeys from "./views/SetupKeys";
import AccessControl from "./views/AccessControl";
import Users from "./views/Users";
import FooterComponent from "./components/FooterComponent";
import {useGetAccessTokenSilently} from "./utils/token";
import {useGetTokenSilently, useTokenSource} from "./utils/token";
import {User} from "./store/user/types";
import {SecureLoading} from "./components/Loading";
import DNS from "./views/DNS";
import Activity from "./views/Activity";
import Settings from "./views/Settings";
const {Header, Content} = Layout;
@@ -28,8 +29,9 @@ const {Header, Content} = Layout;
function App() {
const run = useRef(false)
const [show, setShow] = useState(false)
const {getAccessTokenSilently} = useGetAccessTokenSilently();
const {hotjarTrackID} = getConfig();
const {hotjarTrackID,tokenSource} = getConfig();
useTokenSource(tokenSource)
const {getTokenSilently} = useGetTokenSilently();
// @ts-ignore
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
hotjar.initialize(hotjarTrackID, 6);
@@ -54,9 +56,9 @@ function App() {
useEffect(() => {
if (!run.current) {
run.current = true
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getTokenSilently})
.then(() => {
setShow(true)
setShow(true)
})
.catch(e => {
setShow(true)
@@ -64,53 +66,66 @@ function App() {
})
}
}, [getAccessTokenSilently])
}, [getTokenSilently])
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 &&
<Layout>
<Banner/>
<Header className="header" style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
alignContent: "center"
}}>
<Row justify="space-around" align="middle">
<Col span={24}>
<Container>
<Navbar/>
</Container>
</Col>
</Row>
</Header>
<Content style={{minHeight: "100vh"}}>
<Switch>
<Route
exact
path="/"
render={() => {
return (
<Redirect to="/peers"/>
)
}}
/>
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
<Route path="/routes" component={withOidcSecure(Routes)}/>
<Route path="/users" component={withOidcSecure(Users)}/>
<Route path="/dns" component={withOidcSecure(DNS)}/>
</Switch>
</Content>
<FooterComponent/>
</Layout>
<Layout>
<Banner/>
<Header className="header" style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
alignContent: "center"
}}>
<Row justify="space-around" align="middle">
<Col span={24}>
<Container>
<Navbar/>
</Container>
</Col>
</Row>
</Header>
<Content style={{minHeight: "100vh"}}>
<Switch>
<Route
exact
path="/"
render={() => {
return (
<Redirect to="/peers"/>
)
}}
/>
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
<Route path="/routes" component={withOidcSecure(Routes)}/>
<Route path="/users" component={withOidcSecure(Users)}/>
<Route path="/dns" component={withOidcSecure(DNS)}/>
<Route path="/activity" component={withOidcSecure(Activity)}/>
<Route path="/settings" component={withOidcSecure(Settings)}/>
</Switch>
</Content>
<FooterComponent/>
</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,891 @@
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 type {CustomTagProps} from "rc-select/lib/BaseSelect";
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,
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 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.name)
: [],
tagDestinationGroups: policy.rules[0].destinations
? policy.rules[0].destinations?.map((t) => t.name)
: [],
} 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.name))
.map((g) => g.id || "") || [];
const destinations =
groups
?.filter((g) => formPolicy.tagDestinationGroups.includes(g.name))
.map((g) => g.id || "") || [];
const sourcesNoId = formPolicy.tagSourceGroups.filter(
(s) => !tagGroups.includes(s)
);
const destinationsNoId = formPolicy.tagDestinationGroups.filter(
(s) => !tagGroups.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}
// title={setupKey.name}
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=""
style={{margin: "0"}}
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: "12px 0 30px",
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: 24, 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}>
<Form.Item name="enabled" label="">
<div
style={{
display: "flex",
gap: "15px",
}}
>
<Switch
onChange={handleChangeDisabled}
defaultChecked={policy.enabled}
/>
<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}
>
{tagGroups.map((m) => (
<Option key={m}>{optionRender(m)}</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}
>
{tagGroups.map((m) => (
<Option key={m}>{optionRender(m)}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Col>
<Col span={24}>
<Paragraph
type={"secondary"}
style={{marginTop: "-15px", marginBottom: "30px"}}
>
To change traffic direction and ports, select TCP or UDP protocol below
</Paragraph>
</Col>
<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>
<Col span={24}>
<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={(formPolicy.protocol === "all" || formPolicy.protocol === "icmp") ? "Change protocol to add ports" : "Add ports"}
tagRender={grayTagRender}
onChange={handleChangePorts}
className="menlo-font"
dropdownRender={dropDownRenderPorts}
disabled={
formPolicy.protocol === "all" ||
formPolicy.protocol === "icmp"
}
>
{formPolicy &&
formPolicy.ports?.map((m) => (
<Option key={m}>
<Tag style={{marginRight: 3}}>
{m}
</Tag>
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Form>
</div>
<Container
style={{
display: "flex",
flexDirection: "row",
justifyContent: "start",
padding: 0,
gap: "10px",
marginTop: "30px",
}}
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;

View File

@@ -16,7 +16,7 @@ const AccessControlModalGroups:React.FC<Props> = ({data, title, visible, onCance
return (
<>
<Modal title={title} visible={isModalVisible} onCancel={() => onCancel()} footer={null}>
<Modal title={title} open={isModalVisible} onCancel={() => onCancel()} footer={null}>
<List
itemLayout="horizontal"
dataSource={data as Group[] | undefined}

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"
@@ -36,7 +36,7 @@ const Banner = () => {
if((!stored_banner_closed || stored_banner_closed !== 'true') ||
(!store_banner_md5 || store_banner_md5 !== announcement_md5)) {
setShow(true);
//setShow(true);
localStorage.setItem(banner_md5_key,announcement_md5);
localStorage.setItem(banner_closed_key,'false');
}

View File

@@ -32,7 +32,7 @@ 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 {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
const {Paragraph} = Typography;
@@ -41,7 +41,7 @@ interface formNSGroup extends NameServerGroup {
const NameServerGroupUpdate = () => {
const {
tagRender,
blueTagRender,
handleChangeTags,
dropDownRender,
optionRender,
@@ -51,7 +51,7 @@ const NameServerGroupUpdate = () => {
selectValidator
} = useGetGroupTagHelpers()
const dispatch = useDispatch()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const {Option} = Select;
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
@@ -220,7 +220,7 @@ const NameServerGroupUpdate = () => {
.then((values) => {
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
dispatch(nsGroupActions.saveNameServerGroup.request({
getAccessTokenSilently: getAccessTokenSilently,
getAccessTokenSilently: getTokenSilently,
payload: nsGroupToSave
}))
@@ -572,7 +572,7 @@ const NameServerGroupUpdate = () => {
<Select mode="tags"
style={{width: '100%'}}
placeholder="Associate groups with the NS group"
tagRender={tagRender}
tagRender={blueTagRender}
onChange={handleChangeTags}
dropdownRender={dropDownRender}
>
@@ -604,9 +604,7 @@ const NameServerGroupUpdate = () => {
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://netbird.io/docs/how-to-guides/nameservers"
style={{color: 'rgb(07, 114, 128)'}}>Learn
more about nameservers</Button>
href="https://docs.netbird.io/how-to/manage-dns-in-your-network">Learn more about nameservers</Button>
</Col>
</Row>
</Form>) :

View File

@@ -5,13 +5,14 @@ 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, useOidcUser} from '@axa-fr/react-oidc';
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 {useGetAccessTokenSilently} from "../utils/token";
import {useGetTokenSilently} from "../utils/token";
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
const {useBreakpoint} = Grid;
@@ -19,10 +20,11 @@ const Navbar = () => {
let location = useLocation();
const config = getConfig();
const { logout } = useOidc();
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const {oidcUser} = useOidcUser();
const {idTokenPayload} = useOidcIdToken()
const user = oidcUser;
const [currentUser, setCurrentUser] = useState({} as User)
@@ -34,22 +36,37 @@ const Navbar = () => {
const items = [
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
{label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer'},
{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="/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"]
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
const [menuItems, setMenuItems] = useState(items)
const logoutWithRedirect = () =>
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) {
@@ -78,15 +95,19 @@ const Navbar = () => {
window.location.pathname !== '/peers' &&
window.location.pathname !== '/users') {
setIsRefreshingUserState(true)
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}))
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 (oidcUser && oidcUser.sub) {
const found = users.find(u => u.id == oidcUser.sub)
if (runUser) {
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
if (found) {
setCurrentUser(found)
}
@@ -108,7 +129,7 @@ const Navbar = () => {
<Menu
items={[
{
label: <>{user?.email}</>,
label: (<Link to="/users" onClick={openPersonalUserPage}>{user?.email}</Link>),
key: '0',
},
{
@@ -142,6 +163,10 @@ const Navbar = () => {
<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>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,929 @@
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,
Divider,
Collapse,
Form,
Input,
InputNumber,
Radio,
Row,
Select,
SelectProps,
Space,
Switch,
Modal,
Typography,
} from "antd";
import {
CloseOutlined,
FlagFilled,
QuestionCircleFilled,
} from "@ant-design/icons";
import CreatableSelect from "react-select/creatable";
import { Route, RouteToSave } from "../store/route/types";
import { Header } from "antd/es/layout/layout";
import { RuleObject } from "antd/lib/form";
import cidrRegex from "cidr-regex";
import {
initPeerMaps,
masqueradeDisabledMSG,
peerToPeerIP,
routePeerSeparator,
transformGroupedDataTable,
} from "../utils/routes";
import { useGetTokenSilently } from "../utils/token";
import { useGetGroupTagHelpers } from "../utils/groups";
const { Paragraph, Text } = Typography;
const { Panel } = Collapse;
interface FormRoute extends Route {}
const RouteAddNew = (selectedPeer: any) => {
const {
blueTagRender,
handleChangeTags,
dropDownRender,
optionRender,
tagGroups,
getExistingAndToCreateGroupsLists,
getGroupNamesFromIDs,
selectValidator,
} = useGetGroupTagHelpers();
// const { optionRender, blueTagRender, grayTagRender } =
// useGetGroupTagHelpers();
const { Option } = Select;
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const setupNewRouteVisible = useSelector(
(state: RootState) => state.route.setupNewRouteVisible
);
const setupNewRouteHA = useSelector(
(state: RootState) => state.route.setupNewRouteHA
);
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 [previousRouteKey, setPreviousRouteKey] = useState("");
const [editName, setEditName] = useState(false);
const [editDescription, setEditDescription] = useState(false);
const options: SelectProps["options"] = [];
const testOptions: SelectProps["options"] = [];
const [formRoute, setFormRoute] = useState({} as FormRoute);
const [form] = Form.useForm();
const inputNameRef = useRef<any>(null);
const inputDescriptionRef = useRef<any>(null);
const defaultRoutingPeerMSG = "Routing Peer";
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG);
const defaultMasqueradeMSG = "Masquerade";
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG);
const defaultStatusMSG = "Status";
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG);
const [enableNetwork, setEnableNetwork] = useState(false);
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
const [newRoute, setNewRoute] = useState(false);
useEffect(() => {
if (!newRoute) {
setRoutingPeerMSG(defaultRoutingPeerMSG);
setMasqueradeMSG("Update Masquerade");
setStatusMSG("Update Status");
} else {
setRoutingPeerMSG(defaultRoutingPeerMSG);
setMasqueradeMSG(defaultMasqueradeMSG);
setStatusMSG(defaultStatusMSG);
setPreviousRouteKey("");
}
}, [newRoute]);
useEffect(() => {
if (editName)
inputNameRef.current!.focus({
cursor: "end",
});
}, [editName]);
useEffect(() => {
if (editDescription)
inputDescriptionRef.current!.focus({
cursor: "end",
});
}, [editDescription]);
useEffect(() => {
if (!route) return;
if (selectedPeer && selectedPeer.selectedPeer) {
options?.push({
label: peerToPeerIP(
selectedPeer.selectedPeer.name,
selectedPeer.selectedPeer.ip
),
value: peerToPeerIP(
selectedPeer.selectedPeer.name,
selectedPeer.selectedPeer.ip
),
disabled: false,
});
const udpateRoute = { ...route, peer: options[0].value } as FormRoute;
setFormRoute(udpateRoute);
form.setFieldsValue(udpateRoute);
setPreviousRouteKey(udpateRoute.network_id + udpateRoute.network);
} else {
const fRoute = {
...route,
groups: getGroupNamesFromIDs(route.groups),
} as FormRoute;
setFormRoute(fRoute);
setPreviousRouteKey(fRoute.network_id + fRoute.network);
form.setFieldsValue(fRoute);
}
if (!route.network_id) {
setNewRoute(true);
} else {
setNewRoute(false);
}
// let options = [];
}, [route]);
selectedPeer &&
selectedPeer.notPeerRoutes &&
selectedPeer.notPeerRoutes.forEach((element: any, index: number) => {
testOptions?.push({
label: element.network_id + " - " + element.network,
value: element.network_id + "+" + index,
network: element.network,
disabled: false,
key: index,
});
});
if (!selectedPeer.selectedPeer) {
peers.forEach((p) => {
let os: string;
os = p.os;
if (
!os.toLowerCase().startsWith("darwin") &&
!os.toLowerCase().startsWith("windows") &&
!os.toLowerCase().startsWith("android") &&
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 createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
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
);
return {
id: inputRoute.id,
network: inputRoute.network,
network_id: inputRoute.network_id,
description: inputRoute.description,
peer: peerID,
enabled: inputRoute.enabled,
masquerade: inputRoute.masquerade,
metric: inputRoute.metric,
groups: existingGroups,
groupsToCreate: groupsToCreate,
} as RouteToSave;
};
const handleFormSubmit = () => {
form
.validateFields()
.then(() => {
if (!setupNewRouteHA || formRoute.peer != "") {
const routeToSave = createRouteToSave(formRoute);
dispatch(
routeActions.saveRoute.request({
getAccessTokenSilently: getTokenSilently,
payload: routeToSave,
})
);
} else {
let groupedDataTable = transformGroupedDataTable(routes, peers);
groupedDataTable.forEach((group) => {
if (group.key == previousRouteKey) {
group.groupedRoutes.forEach((route) => {
let updateRoute: FormRoute = {
...formRoute,
id: route.id,
peer: route.peer,
metric: route.metric,
enabled:
formRoute.enabled != group.enabled
? formRoute.enabled
: route.enabled,
};
const routeToSave = createRouteToSave(updateRoute);
dispatch(
routeActions.saveRoute.request({
getAccessTokenSilently: getTokenSilently,
payload: routeToSave,
})
);
});
}
});
}
})
.catch((errorInfo) => {
console.log("errorInfo", errorInfo);
});
};
const setVisibleNewRoute = (status: boolean) => {
dispatch(routeActions.setSetupNewRouteVisible(status));
};
const setSetupNewRouteHA = (status: boolean) => {
dispatch(routeActions.setSetupNewRouteHA(status));
};
const onCancel = () => {
if (savedRoute.loading) return;
setEditName(false);
dispatch(
routeActions.setRoute({
network: "",
network_id: "",
description: "",
peer: "",
metric: 9999,
masquerade: false,
enabled: true,
groups: [],
} as Route)
);
setVisibleNewRoute(false);
setSetupNewRouteHA(false);
setPreviousRouteKey("");
setNewRoute(false);
};
const onChange = (data: any) => {
setFormRoute({ ...formRoute, ...data });
};
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
const toggleEditName = (status: boolean) => {
setEditName(status);
};
const toggleEditDescription = (status: boolean) => {
setEditDescription(status);
};
const networkRangeValidator = (_: RuleObject, value: string) => {
if (!cidrRegex().test(value)) {
return Promise.reject(
new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24")
);
}
if (Number(value.split("/")[1]) < 7) {
return Promise.reject(
new Error("Please enter a network mask larger than /7")
);
}
return Promise.resolve();
};
const peerValidator = (_: RuleObject, value: string) => {
if (value == "" && newRoute) {
return Promise.reject(new Error("Please select routing one peer"));
}
return Promise.resolve();
};
const selectPreValidator = (obj: RuleObject, value: string[]) => {
if (setupNewRouteHA && 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 onNetworkChange = (selectedOption: any) => {
if (selectedOption === null) {
const updateNetwork = {
...formRoute,
network: "",
network_id: "",
};
form.setFieldsValue(updateNetwork);
setFormRoute(updateNetwork);
setEnableNetwork(false)
} else if (!!selectedOption.__isNew__) {
const updateNetwork = {
...formRoute,
network: "",
network_id: selectedOption.value.split("+")[0],
};
form.setFieldsValue(updateNetwork);
setFormRoute(updateNetwork);
setEnableNetwork(false);
} else {
const updateNetwork = {
...formRoute,
network: selectedOption.network,
network_id: selectedOption.value.split("+")[0],
};
form.setFieldsValue(updateNetwork);
setFormRoute(updateNetwork);
setEnableNetwork(true);
}
};
return (
<>
{route && (
<Modal
open={setupNewRouteVisible}
onCancel={onCancel}
footer={
<Space style={{ display: "flex", justifyContent: "end" }}>
<Button onClick={onCancel} disabled={savedRoute.loading}>
Cancel
</Button>
<Button
type="primary"
disabled={savedRoute.loading}
onClick={handleFormSubmit}
>
Add Route
</Button>
</Space>
}
>
<Form
layout="vertical"
form={form}
requiredMark={false}
onValuesChange={onChange}
>
<Row gutter={16}>
<Col span={24}>
<Header
style={{
border: "none",
}}
>
<Paragraph
style={{
textAlign: "start",
whiteSpace: "pre-line",
fontSize: "18px",
margin: "0px",
fontWeight: 500,
marginBottom: "15px",
}}
>
Add Route
</Paragraph>
{!!selectedPeer.selectedPeer && (
<div style={{ lineHeight: "20px" }}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Routing Peer
</label>
<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={[{ validator: peerValidator }]}
>
<Select
showSearch
style={{ width: "100%" }}
placeholder="Select Peer"
dropdownRender={peerDropDownRender}
options={options}
allowClear={true}
disabled={!!selectedPeer.selectedPeer}
/>
</Form.Item>
</div>
)}
<Row align="top">
<Col span={24} style={{ lineHeight: "20px" }}>
{!editName && formRoute.id ? (
<div
className={
"access-control input-text ant-drawer-title"
}
onClick={() => toggleEditName(true)}
>
{formRoute.id ? formRoute.network_id : "New Route"}
</div>
) : (
<div style={{ marginBottom: "15px" }}>
{!!selectedPeer.selectedPeer && (
<>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Network Identifier
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Add a unique cryptographic key that is assigned
to each device
</Paragraph>
<CreatableSelect
isClearable
className="ant-select-selector-custom"
options={testOptions}
onChange={onNetworkChange}
placeholder="Select an existing network or add a new one"
classNamePrefix="react-select"
/>
</>
)}
{!!!selectedPeer.selectedPeer && (
<>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Network Identifier
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Add a unique cryptographic key that is assigned
to each device
</Paragraph>
<Form.Item
name="network_id"
label=""
style={{ marginBottom: "10px" }}
rules={[
{
required: true,
message:
"Please add an identifier for this access route",
whitespace: true,
},
]}
>
<Input
placeholder="for example “e.g. aws-eu-central-1-vpc”"
ref={inputNameRef}
disabled={!setupNewRouteHA && !newRoute}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)}
autoComplete="off"
maxLength={40}
/>
</Form.Item>
</>
)}
</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}
disabled={!setupNewRouteHA && !newRoute}
onPressEnter={() => toggleEditDescription(false)}
onBlur={() => toggleEditDescription(false)}
autoComplete="off"
maxLength={200}
/>
</Form.Item>
)}
</Col>
</Row>
<Row align="top">
<Col flex="auto"></Col>
</Row>
</Header>
</Col>
{/* {!!!selectedPeer.selectedPeer && (
<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: "500",
marginBottom: "0",
}}
>
You can enable or disable the route
</Paragraph>
</div>
</div>
</Form.Item>
</Col>
)} */}
<Col span={24}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Network Range
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Add a private IP address range
</Paragraph>
<Form.Item
name="network"
label=""
rules={[{ validator: networkRangeValidator }]}
>
<Input
placeholder="for example “172.16.0.0/16”"
disabled={(!setupNewRouteHA && !newRoute) || enableNetwork}
autoComplete="off"
minLength={9}
maxLength={43}
/>
</Form.Item>
</Col>
{!!!selectedPeer.selectedPeer && (
<Col span={24}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Routing Peer
</label>
<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={[{ validator: peerValidator }]}>
<Select
showSearch
style={{ width: "100%" }}
placeholder="Select Peer"
dropdownRender={peerDropDownRender}
options={options}
allowClear={true}
disabled={!!selectedPeer.selectedPeer}
/>
</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: "-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 }]}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the network route"
tagRender={blueTagRender}
onChange={handleChangeTags}
dropdownRender={dropDownRender}
>
{tagGroups.map((m) => (
<Option key={m}>{optionRender(m)}</Option>
))}
</Select>
</Form.Item>
</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}>
<Collapse
onChange={onChange}
bordered={false}
ghost={true}
style={{ padding: "0" }}
>
<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={22}>
<Form.Item name="masquerade" label="">
<div
style={{
display: "flex",
gap: "15px",
}}
>
<Switch
size={"small"}
disabled={!setupNewRouteHA && !newRoute}
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={24}>
<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>
<Col
span={24}
style={{ marginTop: "20px", marginBottom: "25px" }}
>
<Text type={"secondary"}>
Learn more about
<a
target="_blank"
rel="noreferrer"
href="https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
>
{" "}
Network Routes
</a>
</Text>
</Col>
</Row>
</Form>
</Modal>
)}
</>
);
};
export default RouteAddNew;

View File

@@ -1,404 +1,560 @@
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 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,
Divider,
Drawer,
Form,
Input,
InputNumber,
Radio,
Row,
Select,
SelectProps,
Space,
Switch,
Typography
Button,
Col,
Divider,
Drawer,
Form,
Input,
InputNumber,
Radio,
Row,
Select,
SelectProps,
Space,
Switch,
Typography,
} from "antd";
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
import {Route} from "../store/route/types";
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
import cidrRegex from 'cidr-regex';
import {
initPeerMaps,
masqueradeDisabledMSG,
peerToPeerIP,
routePeerSeparator,
transformGroupedDataTable
} from '../utils/routes'
import {useGetAccessTokenSilently} from "../utils/token";
CloseOutlined,
FlagFilled,
QuestionCircleFilled,
} from "@ant-design/icons";
import { Route, RouteToSave } from "../store/route/types";
import { Header } from "antd/es/layout/layout";
import { RuleObject } from "antd/lib/form";
import cidrRegex from "cidr-regex";
import {
initPeerMaps,
masqueradeDisabledMSG,
peerToPeerIP,
routePeerSeparator,
transformGroupedDataTable,
} from "../utils/routes";
import { useGetTokenSilently } from "../utils/token";
import { useGetGroupTagHelpers } from "../utils/groups";
const {Paragraph} = Typography;
const { Paragraph } = Typography;
interface FormRoute extends Route {
}
interface FormRoute extends Route {}
const RouteUpdate = () => {
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
const setupNewRouteHA = useSelector((state: RootState) => state.route.setupNewRouteHA)
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 [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
const [previousRouteKey, setPreviousRouteKey] = useState("")
const [editName, setEditName] = useState(false)
const [editDescription, setEditDescription] = useState(false)
const options: SelectProps['options'] = [];
const [formRoute, setFormRoute] = useState({} as FormRoute)
const [form] = Form.useForm()
const inputNameRef = useRef<any>(null)
const inputDescriptionRef = useRef<any>(null)
const {
blueTagRender,
handleChangeTags,
dropDownRender,
optionRender,
tagGroups,
getExistingAndToCreateGroupsLists,
getGroupNamesFromIDs,
selectValidator,
} = useGetGroupTagHelpers();
const { Option } = Select;
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
const setupEditRouteVisible = useSelector(
(state: RootState) => state.route.setupEditRouteVisible
);
const setupNewRouteHA = useSelector(
(state: RootState) => state.route.setupNewRouteHA
);
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 [previousRouteKey, setPreviousRouteKey] = useState("");
const [editName, setEditName] = useState(false);
const [editDescription, setEditDescription] = useState(false);
const options: SelectProps["options"] = [];
const [formRoute, setFormRoute] = useState({} as FormRoute);
const [form] = Form.useForm();
const inputNameRef = useRef<any>(null);
const inputDescriptionRef = useRef<any>(null);
const defaultRoutingPeerMSG = "Routing Peer"
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG)
const defaultMasqueradeMSG = "Masquerade"
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG)
const defaultStatusMSG = "Status"
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG)
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
const defaultRoutingPeerMSG = "Routing Peer";
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG);
const defaultMasqueradeMSG = "Masquerade";
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG);
const defaultStatusMSG = "Status";
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG);
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
const [newRoute, setNewRoute] = useState(false);
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
const optionsDisabledEnabled = [
{ label: "Enabled", value: true },
{ label: "Disabled", value: false },
];
useEffect(() => {
if (setupNewRouteHA) {
setRoutingPeerMSG("Add additional routing peer")
setMasqueradeMSG("Update Masquerade")
setStatusMSG("Update Status")
} else {
setRoutingPeerMSG(defaultRoutingPeerMSG)
setMasqueradeMSG(defaultMasqueradeMSG)
setStatusMSG(defaultStatusMSG)
setPreviousRouteKey("")
}
}, [setupNewRouteHA])
useEffect(() => {
if (!newRoute) {
setRoutingPeerMSG(defaultRoutingPeerMSG);
setMasqueradeMSG("Update Masquerade");
setStatusMSG("Update Status");
} else {
setRoutingPeerMSG(defaultRoutingPeerMSG);
setMasqueradeMSG(defaultMasqueradeMSG);
setStatusMSG(defaultStatusMSG);
setPreviousRouteKey("");
}
}, [newRoute]);
useEffect(() => {
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
useEffect(() => {
if (editName)
inputNameRef.current!.focus({
cursor: "end",
});
}, [editName]);
useEffect(() => {
if (editDescription) inputDescriptionRef.current!.focus({
cursor: 'end',
});
}, [editDescription]);
useEffect(() => {
if (editDescription)
inputDescriptionRef.current!.focus({
cursor: "end",
});
}, [editDescription]);
useEffect(() => {
if (!route) return
useEffect(() => {
if (!route) return;
const fRoute = {
...route,
} as FormRoute
setFormRoute(fRoute)
setPreviousRouteKey(fRoute.network_id + fRoute.network)
form.setFieldsValue(fRoute)
}, [route])
const fRoute = {
...route,
groups: getGroupNamesFromIDs(route.groups),
} as FormRoute;
setFormRoute(fRoute);
setPreviousRouteKey(fRoute.network_id + fRoute.network);
if (!route.network_id) {
setNewRoute(true);
} else {
setNewRoute(false);
}
form.setFieldsValue(fRoute);
}, [route]);
peers.forEach((p) => {
let os: string
os = p.os
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows")) {
options?.push({
label: peerToPeerIP(p.name, p.ip),
value: peerToPeerIP(p.name, p.ip),
disabled: false
peers.forEach((p) => {
let os: string;
os = p.os;
if (
!os.toLowerCase().startsWith("darwin") &&
!os.toLowerCase().startsWith("windows") &&
!os.toLowerCase().startsWith("android") &&
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 createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
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
);
return {
id: inputRoute.id,
network: inputRoute.network,
network_id: inputRoute.network_id,
description: inputRoute.description,
peer: peerID,
enabled: inputRoute.enabled,
masquerade: inputRoute.masquerade,
metric: inputRoute.metric,
groups: existingGroups,
groupsToCreate: groupsToCreate,
} as RouteToSave;
};
const handleFormSubmit = () => {
form
.validateFields()
.then(() => {
if (!setupNewRouteHA || formRoute.peer != "") {
const routeToSave = createRouteToSave(formRoute);
dispatch(
routeActions.saveRoute.request({
getAccessTokenSilently: getTokenSilently,
payload: routeToSave,
})
}
})
const createRouteToSave = (inputRoute: FormRoute): Route => {
let peerIDList = inputRoute.peer.split(routePeerSeparator)
let peerID: string
if (peerIDList[1]) {
peerID = peerIDList[1]
);
} else {
peerID = peerNameToIP[inputRoute.peer]
}
return {
id: inputRoute.id,
network: inputRoute.network,
network_id: inputRoute.network_id,
description: inputRoute.description,
peer: peerID,
enabled: inputRoute.enabled,
masquerade: inputRoute.masquerade,
metric: inputRoute.metric
} as Route
}
const handleFormSubmit = () => {
form.validateFields()
.then(() => {
if (!setupNewRouteHA || formRoute.peer != '') {
const routeToSave = createRouteToSave(formRoute)
dispatch(routeActions.saveRoute.request({
getAccessTokenSilently: getAccessTokenSilently,
payload: routeToSave
}))
} else {
let groupedDataTable = transformGroupedDataTable(routes, peerIPToName)
groupedDataTable.forEach((group) => {
if (group.key == previousRouteKey) {
group.groupedRoutes.forEach((route) => {
let updateRoute: FormRoute = {
...formRoute,
id: route.id,
peer: route.peer,
metric: route.metric,
enabled: (formRoute.enabled != group.enabled) ? formRoute.enabled : route.enabled
}
const routeToSave = createRouteToSave(updateRoute)
dispatch(routeActions.saveRoute.request({
getAccessTokenSilently: getAccessTokenSilently,
payload: routeToSave
}))
})
}
})
}
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const setVisibleNewRoute = (status: boolean) => {
dispatch(routeActions.setSetupNewRouteVisible(status));
}
const setSetupNewRouteHA = (status: boolean) => {
dispatch(routeActions.setSetupNewRouteHA(status));
}
const onCancel = () => {
if (savedRoute.loading) return
setEditName(false)
dispatch(routeActions.setRoute({
network: '',
network_id: '',
description: '',
peer: "",
metric: 9999,
masquerade: false,
enabled: true
} as Route))
setVisibleNewRoute(false)
setSetupNewRouteHA(false)
setPreviousRouteKey("")
}
const onChange = (data: any) => {
setFormRoute({...formRoute, ...data})
}
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
</>
)
const toggleEditName = (status: boolean) => {
setEditName(status);
}
const toggleEditDescription = (status: boolean) => {
setEditDescription(status);
}
const networkRangeValidator = (_: RuleObject, value: string) => {
if (!cidrRegex().test(value)) {
return Promise.reject(new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24"))
}
if (Number(value.split("/")[1]) < 7) {
return Promise.reject(new Error("Please enter a network mask larger than /7"))
}
return Promise.resolve()
}
return (
<>
{route &&
<Drawer
headerStyle={{display: "none"}}
forceRender={true}
visible={setupNewRouteVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
autoFocus={true}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button onClick={onCancel} disabled={savedRoute.loading}>Cancel</Button>
<Button type="primary" disabled={savedRoute.loading}
onClick={handleFormSubmit}>{`${formRoute.network_id ? 'Save' : 'Create'}`}</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
<Row gutter={16}>
<Col span={24}>
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Row align="top">
<Col flex="none" style={{display: "flex"}}>
{!editName && !editDescription && formRoute.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 && formRoute.id ? (
<div className={"access-control input-text ant-drawer-title"}
onClick={() => toggleEditName(true)}>{formRoute.id ? formRoute.network_id : 'New Route'}</div>
) : (
<Form.Item
name="network_id"
label="Network Identifier"
tooltip="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes"
rules={[{
required: true,
message: 'Please add an identifier for this access route',
whitespace: true
}]}
>
<Input placeholder="e.g. aws-eu-central-1-vpc" ref={inputNameRef}
disabled={!setupNewRouteHA}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)} autoComplete="off"
maxLength={40}/>
</Form.Item>
)}
{!editDescription ? (
<div className={"access-control input-text ant-drawer-subtitle"}
onClick={() => toggleEditDescription(true)}>{formRoute.description && formRoute.description.trim() !== "" ? formRoute.description : 'Add description...'}</div>
) : (
<Form.Item
name="description"
label="Description"
style={{marginTop: 24}}
>
<Input placeholder="Add description..." ref={inputDescriptionRef}
disabled={!setupNewRouteHA}
onPressEnter={() => toggleEditDescription(false)}
onBlur={() => toggleEditDescription(false)}
autoComplete="off" maxLength={200}/>
</Form.Item>
)}
</Col>
</Row>
<Row align="top">
<Col flex="auto">
</Col>
</Row>
</Header>
</Col>
<Col span={24}>
</Col>
<Col span={24}>
<Form.Item
name="network"
label="Network Range"
tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16"
rules={[{validator: networkRangeValidator}]}
>
<Input placeholder="e.g. 172.16.0.0/16" disabled={!setupNewRouteHA}
autoComplete="off" minLength={9} maxLength={43}/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="enabled"
label={statusMSG}
>
<Radio.Group
options={optionsDisabledEnabled}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="peer"
label={routingPeerMSG}
tooltip="Assign a peer as a routing peer for the Network CIDR"
>
<Select
showSearch
style={{width: '100%'}}
placeholder="Select Peer"
dropdownRender={dropDownRender}
options={options}
allowClear={true}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="masquerade"
label={masqueradeMSG}
tooltip={masqueradeDisabledMSG}
>
<Switch size={"small"} disabled={!setupNewRouteHA} checked={formRoute.masquerade}/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="metric"
label="Metric"
tooltip="Choose from 1 to 9999. Lower number has higher priority"
>
<InputNumber min={1} max={9999} autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Row wrap={false} gutter={12}>
<Col flex="none">
<FlagFilled/>
</Col>
<Col flex="auto">
<Paragraph>
You can enable high-availability by assigning the same network identifier
and network CIDR to multiple routes.
</Paragraph>
</Col>
</Row>
</Col>
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://netbird.io/docs/how-to-guides/network-routes"
style={{color: 'rgb(07, 114, 128)'}}>Learn
more about network routes</Button>
</Col>
</Row>
</Form>
</Drawer>
let groupedDataTable = transformGroupedDataTable(routes, peers);
groupedDataTable.forEach((group) => {
if (group.key == previousRouteKey) {
group.groupedRoutes.forEach((route) => {
let updateRoute: FormRoute = {
...formRoute,
id: route.id,
peer: route.peer,
metric: route.metric,
enabled:
formRoute.enabled != group.enabled
? formRoute.enabled
: route.enabled,
};
const routeToSave = createRouteToSave(updateRoute);
dispatch(
routeActions.saveRoute.request({
getAccessTokenSilently: getTokenSilently,
payload: routeToSave,
})
);
});
}
</>
)
}
});
}
})
.catch((errorInfo) => {
console.log("errorInfo", errorInfo);
});
};
export default RouteUpdate
const setVisibleNewRoute = (status: boolean) => {
dispatch(routeActions.setSetupEditRouteVisible(status));
};
const setSetupNewRouteHA = (status: boolean) => {
dispatch(routeActions.setSetupNewRouteHA(status));
};
const onCancel = () => {
if (savedRoute.loading) return;
setEditName(false);
dispatch(
routeActions.setRoute({
network: "",
network_id: "",
description: "",
peer: "",
metric: 9999,
masquerade: false,
enabled: true,
groups: [],
} as Route)
);
setVisibleNewRoute(false);
setSetupNewRouteHA(false);
setPreviousRouteKey("");
setNewRoute(false);
};
const onChange = (data: any) => {
setFormRoute({ ...formRoute, ...data });
};
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
const toggleEditName = (status: boolean) => {
setEditName(status);
};
const toggleEditDescription = (status: boolean) => {
setEditDescription(status);
};
const networkRangeValidator = (_: RuleObject, value: string) => {
if (!cidrRegex().test(value)) {
return Promise.reject(
new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24")
);
}
if (Number(value.split("/")[1]) < 7) {
return Promise.reject(
new Error("Please enter a network mask larger than /7")
);
}
return Promise.resolve();
};
const peerValidator = (_: RuleObject, value: string) => {
if (value == "" && newRoute) {
return Promise.reject(new Error("Please select routing one peer"));
}
return Promise.resolve();
};
const selectPreValidator = (obj: RuleObject, value: string[]) => {
if (setupNewRouteHA && 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);
};
return (
<>
{route && (
<Drawer
headerStyle={{ display: "none" }}
forceRender={true}
open={setupEditRouteVisible}
bodyStyle={{ paddingBottom: 80 }}
onClose={onCancel}
autoFocus={true}
footer={
<Space style={{ display: "flex", justifyContent: "end" }}>
<Button onClick={onCancel} disabled={savedRoute.loading}>
Cancel
</Button>
<Button
type="primary"
disabled={savedRoute.loading}
onClick={handleFormSubmit}
>{`${newRoute ? "Create" : "Save"}`}</Button>
</Space>
}
>
<Form
layout="vertical"
form={form}
requiredMark={false}
onValuesChange={onChange}
>
<Row gutter={16}>
<Col span={24}>
<Header
style={{
margin: "-32px -24px 20px -24px",
padding: "24px 24px 0 24px",
}}
>
<Row align="top">
<Col flex="none" style={{ display: "flex" }}>
{!editName && !editDescription && formRoute.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 && formRoute.id ? (
<div
className={
"access-control input-text ant-drawer-title"
}
onClick={() => toggleEditName(true)}
>
{formRoute.id ? formRoute.network_id : "New Route"}
</div>
) : (
<Form.Item
name="network_id"
label="Network Identifier"
tooltip="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes"
rules={[
{
required: true,
message:
"Please add an identifier for this access route",
whitespace: true,
},
]}
>
<Input
placeholder="e.g. aws-eu-central-1-vpc"
ref={inputNameRef}
disabled={!setupNewRouteHA && !newRoute}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)}
autoComplete="off"
maxLength={40}
/>
</Form.Item>
)}
{!editDescription ? (
<div
className={
"access-control input-text ant-drawer-subtitle"
}
onClick={() => toggleEditDescription(true)}
>
{formRoute.description &&
formRoute.description.trim() !== ""
? formRoute.description
: "Add description..."}
</div>
) : (
<Form.Item
name="description"
label="Description"
style={{ marginTop: 24 }}
>
<Input
placeholder="Add description..."
ref={inputDescriptionRef}
disabled={!setupNewRouteHA && !newRoute}
onPressEnter={() => toggleEditDescription(false)}
onBlur={() => toggleEditDescription(false)}
autoComplete="off"
maxLength={200}
/>
</Form.Item>
)}
</Col>
</Row>
<Row align="top">
<Col flex="auto"></Col>
</Row>
</Header>
</Col>
<Col span={24}></Col>
<Col span={24}>
<Form.Item
name="network"
label="Network Range"
tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16"
rules={[{ validator: networkRangeValidator }]}
>
<Input
placeholder="e.g. 172.16.0.0/16"
disabled={!setupNewRouteHA && !newRoute}
autoComplete="off"
minLength={9}
maxLength={43}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="enabled" label={statusMSG}>
<Radio.Group
options={optionsDisabledEnabled}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="peer"
label={routingPeerMSG}
tooltip="Assign a peer as a routing peer for the Network CIDR"
rules={[{ validator: peerValidator }]}
>
<Select
showSearch
style={{ width: "100%" }}
placeholder="Select Peer"
dropdownRender={peerDropDownRender}
options={options}
allowClear={true}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="masquerade"
label={masqueradeMSG}
tooltip={masqueradeDisabledMSG}
>
<Switch
size={"small"}
disabled={!setupNewRouteHA && !newRoute}
checked={formRoute.masquerade}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="metric"
label="Metric"
tooltip="Choose from 1 to 9999. Lower number has higher priority"
>
<InputNumber min={1} max={9999} autoComplete="off" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="groups"
label="Distribution groups"
tooltip="NetBird will advertise this route to peers that belong to the following groups"
rules={[{ validator: selectPreValidator }]}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the network route"
tagRender={blueTagRender}
onChange={handleChangeTags}
dropdownRender={dropDownRender}
>
{tagGroups.map((m) => (
<Option key={m}>{optionRender(m)}</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Row wrap={false} gutter={12}>
<Col flex="none">
<FlagFilled />
</Col>
<Col flex="auto">
<Paragraph>
You can enable high-availability by assigning the same
network identifier and network CIDR to multiple routes.
</Paragraph>
</Col>
</Row>
</Col>
<Col span={24}>
<Divider></Divider>
<Button
icon={<QuestionCircleFilled />}
type="link"
target="_blank"
href="https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
>
Learn more about network routes
</Button>
</Col>
</Row>
</Form>
</Drawer>
)}
</>
);
};
export default RouteUpdate;

View File

@@ -0,0 +1,466 @@
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,
InputNumber,
Row,
Select,
Breadcrumb,
Switch,
Tag,
Typography,
Card,
} from "antd";
import { RootState } from "typesafe-actions";
import {
FormSetupKey,
SetupKey,
SetupKeyToSave,
} from "../store/setup-key/types";
import { formatDate, timeAgo } from "../utils/common";
import { RuleObject } from "antd/lib/form";
import { CustomTagProps } from "rc-select/lib/BaseSelect";
import { Group } from "../store/group/types";
import { useGetTokenSilently } from "../utils/token";
import { expiresInToSeconds, ExpiresInValue } from "../views/ExpiresInInput";
import moment from "moment";
import { Container } from "./Container";
import Paragraph from "antd/es/typography/Paragraph";
import { EditOutlined, 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 ExpiresInDefault: ExpiresInValue = { number: 30, interval: "day" };
const customExpiresFormat = (value: Date): string | null => {
return formatDate(value);
};
const SetupKeyNew = () => {
const { optionRender, blueTagRender } = 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 [tagGroups, setTagGroups] = useState([] as string[]);
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey);
const inputNameRef = useRef<any>(null);
useEffect(() => {
//Unmounting component clean
return () => {
setVisibleNewSetupKey(false);
};
}, []);
useEffect(() => {
if (!editName) return;
inputNameRef.current!.focus({ cursor: "end" });
}, [editName]);
useEffect(() => {
setTagGroups(
groups?.filter((g) => g.name !== "All").map((g) => g.name) || []
);
}, [groups]);
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 ? formKeyGroups : [],
expiresInFormatted: ExpiresInDefault,
exp: moment(setupKey.expires),
last: moment(setupKey.last_used),
} as FormSetupKey;
form.setFieldsValue(fSetupKey);
setFormSetupKey(fSetupKey);
}, [setupKey]);
const createSetupKeyToSave = (): SetupKeyToSave => {
const autoGroups =
groups
?.filter((g) => formSetupKey.autoGroupNames.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 = formSetupKey.autoGroupNames.filter(
(s) => !allGroupsNames.includes(s)
);
const expiresIn = expiresInToSeconds(formSetupKey.expiresInFormatted);
return {
id: formSetupKey.id,
name: formSetupKey.name,
type: formSetupKey.type,
auto_groups: autoGroups,
revoked: formSetupKey.revoked,
groupsToCreate: groupsToCreate,
expires_in: expiresIn,
usage_limit: formSetupKey.usage_limit,
} as SetupKeyToSave;
};
const handleFormSubmit = async () => {
try {
await form.validateFields();
} catch (e) {
const errorFields = (e as any).errorFields;
return console.log("errorInfo", 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);
};
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 (
<>
<Breadcrumb
style={{ marginBottom: "25px" }}
items={[
{
title: <a onClick={onBreadcrumbUsersClick}>Setup Keys</a>,
},
{
title: setupKey.name,
},
]}
/>
<Card bordered={true} style={{ marginBottom: "7px", border: "none" }}>
<div style={{ maxWidth: "800px" }}>
<h3 style={{ fontSize: "22px", fontWeight: "500",marginBottom:"30px" }}>
{setupKey.name}
</h3>
<Form
layout="vertical"
requiredMark={false}
form={form}
onValuesChange={onChange}
initialValues={{
expiresIn: ExpiresInDefault,
usage_limit: 1,
}}
>
<Row style={{ marginTop: "10px" }}>
<Col
xs={24}
sm={24}
md={11}
lg={11}
xl={11}
xxl={11}
span={11}
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 xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<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: "30px" }}>
<Col
xs={24}
sm={24}
md={11}
lg={11}
xl={11}
xxl={11}
span={11}
style={{ paddingRight: "70px" }}
>
<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}
// enabled only when we have a new key !setupkey.id or when the key is valid
disabled={!(!setupKey.id || setupKey.valid)}
>
{tagGroups.map((m) => (
<Option key={m}>{optionRender(m)}</Option>
))}
</Select>
</Form.Item>
</Col>
</Col>
<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: "40px", marginBottom: "28px" }}>
<Text type={"secondary"}>
Learn more about
<a
target="_blank"
rel="noreferrer"
href="https://docs.netbird.io/how-to/register-machines-using-setup-keys"
>
{" "}
Setup Keys
</a>
</Text>
</Row>
</Form>
</div>
<Container
style={{
display: "flex",
flexDirection: "row",
justifyContent: "start",
padding: 0,
gap: "10px",
}}
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

666
src/components/UserEdit.tsx Normal file
View File

@@ -0,0 +1,666 @@
import {
Badge,
Breadcrumb,
Button, Card,
Col,
Divider,
Form,
Input,
List, Modal,
Row,
Select,
Skeleton,
Space, Switch, Table,
Tag, Typography
} from "antd";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as userActions} from "../store/user";
import {FormUser, User, UserToSave} from "../store/user/types";
import {useGetTokenSilently} from "../utils/token";
import React, {useEffect, useState} from "react";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import {actions as groupActions} from "../store/group";
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
import {PersonalAccessToken, PersonalAccessTokenCreate, SpecificPAT} from "../store/personal-access-token/types";
import tableSpin from "./Spin";
import AddPATPopup from "./popups/AddPATPopup";
import {fullDate} from "../utils/common";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import Column from "antd/lib/table/Column";
import {useOidcUser} from "@axa-fr/react-oidc";
import {useGetGroupTagHelpers} from "../utils/groups";
const {Option} = Select;
const {Meta} = Card;
const {Title, Paragraph, Text} = Typography;
interface TokenDataTable extends PersonalAccessToken {
key: string
status: string
created_by_email: string
}
const UserEdit = () => {
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const {
optionRender,
blueTagRender
} = useGetGroupTagHelpers()
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 savedUser = useSelector((state: RootState) => state.user.savedUser)
const personalAccessTokens = useSelector((state: RootState) => state.personalAccessToken.data);
const tab = useSelector((state: RootState) => state.user.userTabOpen)
const loading = useSelector((state: RootState) => state.user.loading);
const {oidcUser} = useOidcUser();
const [tokenTable, setTokenTable] = useState([] as TokenDataTable[]);
const [tagGroups, setTagGroups] = useState([] as string[])
const [currentGroups, setCurrentGroups] = useState([] as string[])
const [formUser, setFormUser] = useState({} as FormUser)
const [form] = Form.useForm()
const [isAdmin, setIsAdmin] = useState(false);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const onCancel = () => {
if (savedUser.loading) return
dispatch(userActions.setUser(null as unknown as User));
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
setFormUser({} as FormUser)
dispatch(userActions.setEditUserPopupVisible(false));
}
const createUserToSave = (values: any): UserToSave => {
const autoGroups = groups?.filter(g => 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))
let userID = user ? user.id : ''
let isServiceUser = user ? user.is_service_user : false
return {
id: userID,
role: values.role,
name: values.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
is_service_user: isServiceUser,
is_blocked: values.is_blocked
} as UserToSave
}
useEffect(() => {
if(users) {
let currentUser = users.find((user) => user.is_current)
if(currentUser) {
setIsAdmin(currentUser.role === "admin");
}
}
}, [users])
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
let userToSave = createUserToSave(values)
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}))
dispatch(userActions.setEditUserPopupVisible(false));
dispatch(userActions.setUser(null as unknown as User))
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const onClickAddNewPersonalAccessToken = () => {
dispatch(personalAccessTokenActions.setPersonalAccessToken({
user_id: "",
name: "",
expires_in: 7
} as PersonalAccessTokenCreate))
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(true));
}
const onBreadcrumbUsersClick = (key: string) => {
if (savedUser.loading) return
dispatch(userActions.setUser(null as unknown as User));
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
dispatch(userActions.setUserTabOpen(key))
}
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 transformTokenTable = (d: PersonalAccessToken[]): TokenDataTable[] => {
if (!d) {
return []
}
return d.map(p => ({
key: p.id,
status: Date.parse(p.expiration_date) > Date.now() ? "valid" : "expired",
created_by_email: getEmail(p),
...p
} as TokenDataTable))
}
const getEmail = (token: PersonalAccessToken): string => {
return users.find(u => u.id === token.created_by)?.email || ""
}
const showConfirmDelete = (token: TokenDataTable) => {
confirmModal.confirm({
icon: <ExclamationCircleOutlined />,
title: <span className="font-500">Delete token {token.name}</span>,
width: 600,
content: (
<Space direction="vertical" size="small">
<Paragraph>Are you sure you want to delete this token?</Paragraph>
</Space>
),
onOk() {
dispatch(
personalAccessTokenActions.deletePersonalAccessToken.request({
getAccessTokenSilently: getTokenSilently,
payload: {
user_id: user.id,
id: token.id,
name: token.name,
} as SpecificPAT,
})
);
},
onCancel() {
// noop
},
});
}
useEffect(() => {
setTokenTable(transformTokenTable(personalAccessTokens))
}, [personalAccessTokens, users])
useEffect(() => {
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
}, [groups])
useEffect(() => {
if (user) {
// @ts-ignore
setCurrentGroups(groups.filter(g => g.name != "All" && user.auto_groups.includes(g.id)).map(g => g.name) || [])
}
}, [groups, user])
useEffect(() => {
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
dispatch(groupActions.getGroups.request({
getAccessTokenSilently: getTokenSilently,
payload: null
}))
}, [])
useEffect(() => {
if (user.is_current || user.is_service_user) {
dispatch(personalAccessTokenActions.getPersonalAccessTokens.request({
getAccessTokenSilently: getTokenSilently,
payload: user.id
}))
}
}, [user])
useEffect(() => {
if (user && currentGroups) {
form.setFieldsValue({
name: user.name,
role: user.role,
email: user.email,
is_blocked: user.is_blocked,
autoGroupsNames: currentGroups,
})
}
}, [form, user, currentGroups])
return (
<>
<div style={{ paddingTop: "13px" }}>
<Breadcrumb
style={{ marginBottom: "30px" }}
items={[
{
title: (
<a onClick={() => onBreadcrumbUsersClick("Users")}>
All Users
</a>
),
},
{
title: <a onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
// menu: { items: menuItems },
},
{
title: user.name,
},
]}
/>
<Card
bordered={true}
loading={loading}
style={{ marginBottom: "7px" }}
>
<h3 style={{fontSize:"22px",fontWeight:"500",marginBottom:"25px"}}>{user.name}</h3>
<div style={{ maxWidth: "800px" }}>
<Form
layout="vertical"
hideRequiredMark
form={form}
initialValues={{
name: formUser.name,
role: formUser.role,
email: formUser.email,
is_blocked: formUser.is_blocked,
autoGroupsNames: formUser.autoGroupsNames,
}}
>
<Row style={{ paddingBottom: "15px" }}>
{!user.is_service_user && (
<Col
xs={24}
sm={24}
md={11}
lg={11}
xl={11}
xxl={11}
span={11}
>
<Form.Item
name="email"
label={<Text style={{}}>Email</Text>}
style={{ marginRight: "70px", fontWeight: "500" }}
>
<Input
disabled={user.id}
value={formUser.email}
style={{ color: "#8c8c8c" }}
autoComplete="off"
/>
</Form.Item>
</Col>
)}
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Form.Item
name="role"
label={<Text style={{fontWeight:"500"}}>Role</Text>}
style={{ marginRight: "50px", fontWeight: "500" }}
>
<Select
style={{ width: "100%" }}
disabled={user.is_current}
>
<Option value="admin">
<Text type={"secondary"}>admin</Text>
</Option>
<Option value="user">
<Text type={"secondary"}>user</Text>
</Option>
</Select>
</Form.Item>
</Col>
</Row>
{!user.is_service_user && (
<Row style={{ paddingBottom: "15px" }}>
<Col
xs={24}
sm={24}
md={11}
lg={11}
xl={11}
xxl={11}
span={11}
>
<Form.Item
name="autoGroupsNames"
label={
<Text style={{ fontWeight: "500" }}>
Auto-assigned groups
</Text>
}
tooltip="Every peer enrolled with this user will be automatically added to these groups"
rules={[{ validator: selectValidator }]}
style={{ marginRight: "70px" }}
>
<Select
mode="tags"
placeholder="Associate groups with the user"
tagRender={blueTagRender}
dropdownRender={dropDownRender}
disabled={!isAdmin}
>
{tagGroups.map((m) => (
<Option key={m}>{optionRender(m)}</Option>
))}
</Select>
</Form.Item>
</Col>
{!user.is_current && isAdmin && (
<Col
xs={24}
sm={24}
md={5}
lg={5}
xl={5}
xxl={5}
span={5}
>
<Form.Item
valuePropName="checked"
name="is_blocked"
label="Block user"
style={{ marginRight: "50px", fontWeight: "500" }}
>
<Switch />
</Form.Item>
</Col>
)}
</Row>
)}
<Space style={{ display: "flex", justifyContent: "start" }}>
<Button disabled={loading} onClick={onCancel}>
Cancel
</Button>
<Button type="primary" onClick={handleFormSubmit}>
Save
</Button>
</Space>
</Form>
</div>
</Card>
{user && (user.is_current || user.is_service_user) && (
<Card
bordered={true}
loading={loading}
style={{ marginBottom: "7px" }}
>
<div style={{ maxWidth: "800px" }}>
<Paragraph
style={{
textAlign: "left",
whiteSpace: "pre-line",
fontSize: "18px",
fontWeight: "500",
}}
>
Access tokens
</Paragraph>
<Row
gutter={21}
style={{ marginTop: "-16px", marginBottom: "10px" }}
>
<Col
xs={24}
sm={24}
md={20}
lg={20}
xl={20}
xxl={20}
span={20}
>
<Paragraph
type={"secondary"}
style={{ textAlign: "left", whiteSpace: "pre-line" }}
>
Access tokens give access to NetBird API
</Paragraph>
</Col>
<Col
xs={24}
sm={24}
md={1}
lg={1}
xl={1}
xxl={1}
span={1}
style={{ marginTop: "-16px" }}
>
{personalAccessTokens &&
personalAccessTokens.length > 0 && (
<Button
type="primary"
onClick={onClickAddNewPersonalAccessToken}
>
Create token
</Button>
)}
</Col>
</Row>
{personalAccessTokens && personalAccessTokens.length > 0 && (
<Table
size={"small"}
style={{ marginTop: "-10px" }}
showHeader={false}
scroll={{ x: 800 }}
pagination={false}
loading={tableSpin(loading)}
dataSource={tokenTable}
>
<Column
className={"non-highlighted-table-column"}
sorter={(a, b) =>
(a as TokenDataTable).created_at.localeCompare(
(b as TokenDataTable).created_at
)
}
defaultSortOrder="descend"
render={(text, record, index) => {
return (
<>
<Row>
<Col>
<Badge
status={
(record as TokenDataTable).status ===
"valid"
? "success"
: "error"
}
style={{
marginTop: "1px",
marginRight: "5px",
marginLeft: "0px",
}}
/>
</Col>
<Col>
<Paragraph
style={{
margin: "0px",
padding: "0px",
}}
>
{(record as TokenDataTable).name}
</Paragraph>
<Paragraph
type={"secondary"}
style={{
fontSize: "13px",
fontWeight: "400",
margin: "0px",
marginTop: "-2px",
padding: "0px",
}}
>
{"Created" +
((record as TokenDataTable)
.created_by_email && user.is_service_user
? " by " +
(record as TokenDataTable)
.created_by_email
: "") +
" on " +
fullDate(
(record as TokenDataTable).created_at
)}
</Paragraph>
</Col>
</Row>
</>
);
}}
/>
<Column
render={(text, record, index) => {
return (
<>
<Paragraph
type={"secondary"}
style={{ textAlign: "left", fontSize: "11px" }}
>
Expires on
</Paragraph>
<Paragraph
type={"secondary"}
style={{
textAlign: "left",
marginTop: "-10px",
marginBottom: "0",
fontSize: "15px",
}}
>
{fullDate(
(record as TokenDataTable).expiration_date
)}
</Paragraph>
</>
);
}}
/>
<Column
render={(text, record, index) => {
return (
<>
<Paragraph
type={"secondary"}
style={{ textAlign: "left", fontSize: "11px" }}
>
Last used
</Paragraph>
<Paragraph
type={"secondary"}
style={{
textAlign: "left",
marginTop: "-10px",
marginBottom: "0",
fontSize: "15px",
}}
>
{(record as TokenDataTable).last_used
? fullDate((record as TokenDataTable).last_used)
: "Never"}
</Paragraph>
</>
);
}}
/>
<Column
align="right"
render={(text, record, index) => {
return (
<Button
danger={true}
type={"text"}
onClick={() => {
showConfirmDelete(record as TokenDataTable);
}}
>
Delete
</Button>
);
}}
/>
</Table>
)}
<Divider style={{ marginTop: "-12px" }}></Divider>
{(personalAccessTokens === null ||
personalAccessTokens.length === 0) && (
<Space
direction="vertical"
size="small"
align="start"
style={{
display: "flex",
padding: "35px 0px",
marginTop: "-40px",
justifyContent: "center",
}}
>
<Paragraph
style={{ textAlign: "start", whiteSpace: "pre-line" }}
>
You dont have any access tokens yet
</Paragraph>
<Button
type="primary"
onClick={onClickAddNewPersonalAccessToken}
>
Create token
</Button>
</Space>
)}
</div>
</Card>
)}
</div>
<AddPATPopup />
{confirmModalContextHolder}
</>
);
}
export default UserEdit;

View File

@@ -1,413 +0,0 @@
import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {Alert, Button, Col, Divider, Drawer, Form, Input, Modal, Row, Select, Space, Tag, Typography} from "antd";
import {RootState} from "typesafe-actions";
import {CloseOutlined, EditOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import {Header} from "antd/es/layout/layout";
import {Group} from "../store/group/types";
import {FormUser, User, UserToSave} from "../store/user/types";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import {actions as userActions} from "../store/user";
import {useGetAccessTokenSilently} from "../utils/token";
import {useOidcUser} from "@axa-fr/react-oidc";
const {Paragraph, Text} = Typography;
const {confirm} = Modal;
const {Option} = Select;
const UserUpdate = () => {
const {oidcUser} = useOidcUser();
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
const user = useSelector((state: RootState) => state.user.user)
const savedUser = useSelector((state: RootState) => state.user.savedUser)
const groups = useSelector((state: RootState) => state.group.data)
const users = useSelector((state: RootState) => state.user.data)
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
const [tagGroups, setTagGroups] = useState([] as string[])
const [editName, setEditName] = useState(false)
const inputNameRef = useRef<any>(null)
const [formUser, setFormUser] = useState({} as FormUser)
const [currentUser, setCurrentUser] = useState({} as User)
const [form] = Form.useForm()
useEffect(() => {
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
const toggleEditName = (status: boolean) => {
setEditName(status);
}
useEffect(() => {
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
}, [groups])
useEffect(() => {
if (oidcUser && oidcUser.sub) {
const found = users.find(u => u.id == oidcUser.sub)
if (found) {
setCurrentUser(found)
}
} else {
setCurrentUser({} as User)
}
}, [oidcUser, users])
useEffect(() => {
if (!user) return
let allGroups = new Map<string, Group>();
groups.forEach(g => {
allGroups.set(g.id!, g)
})
if (!user.auto_groups) {
user.auto_groups = []
}
let formKeyGroups = user.auto_groups.filter(g => allGroups.get(g)).map(g => allGroups.get(g)!.name)
const fUser = {
...user,
autoGroupsNames: user.auto_groups ? formKeyGroups : [],
} as FormUser
setFormUser(fUser)
form.setFieldsValue(fUser)
}, [user])
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 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 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 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 handleChangeTags = (value: string[]) => {
let validatedValues: string[] = []
value.forEach(function (v) {
if (v.trim().length) {
validatedValues.push(v)
}
})
setSelectedTagGroups(validatedValues)
};
const createUserToSave = (): UserToSave => {
const autoGroups = groups?.filter(g => formUser.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 = formUser.autoGroupsNames.filter(s => !allGroupsNames.includes(s))
return {
id: formUser.id,
email: formUser.email,
role: formUser.role,
name: formUser.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
} as UserToSave
}
const showConfirmChangeRole = (userToSave: UserToSave) => {
let content = <Paragraph>With this action, you will remove the administrative privileges of your user.
Your user will be limited to read-only operations in this account. Are you sure?</Paragraph>
let contentModule = <div>{content}</div>
let name = formUser ? formUser.email : ''
confirm({
icon: <ExclamationCircleOutlined/>,
title: "Update user \"" + name + "\"",
width: 600,
content: contentModule,
okType: 'danger',
onOk() {
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getAccessTokenSilently,
payload: userToSave
}))
},
onCancel() {
},
});
}
// check if currentUser (who is doing the modification) removes the administrative privileges from themselves
const isShowConfirmWarning = (userToSave: UserToSave): boolean => {
return currentUser.id == userToSave.id && currentUser.role === "admin" && userToSave.role !== "admin"
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
let userToSave = createUserToSave()
if (isShowConfirmWarning(userToSave)) {
showConfirmChangeRole(userToSave)
} else {
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getAccessTokenSilently,
payload: userToSave
}))
}
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const onCancel = () => {
if (savedUser.loading) return
dispatch(userActions.setUser({
id: "",
email: "",
role: "",
status: "",
auto_groups: [],
name: user.name
} as User));
setFormUser({} as FormUser)
toggleEditName(false)
dispatch(userActions.setUpdateUserDrawerVisible(false));
}
const onChange = (data: any) => {
setFormUser({...formUser, ...data})
}
const changesDetected = (): boolean => {
return emailChanged() || nameChanged() || groupsChanged() || roleChanged()
}
const emailChanged = (): boolean => {
return formUser.email !== user.email
}
const roleChanged = (): boolean => {
return formUser.role !== user.role
}
const nameChanged = (): boolean => {
return formUser.name !== user.name
}
const groupsChanged = (): boolean => {
if (!formUser.autoGroupsNames) {
return false
}
if (formUser.autoGroupsNames.length != user.auto_groups.length) {
return true
}
const formGroupIds = groups?.filter(g => formUser.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
return user.auto_groups?.filter(g => !formGroupIds.includes(g)).length > 0
}
return (
<>
{user &&
<Drawer
forceRender={true}
headerStyle={{display: "none"}}
open={updateUserDrawerVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button disabled={savedUser.loading} onClick={onCancel}>Cancel</Button>
<Button type="primary" disabled={savedUser.loading || !changesDetected()}
onClick={handleFormSubmit}>{`${formUser.id ? 'Save' : 'Invite'}`}</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}
initialValues={{
["role"]: formUser.role
}}
>
<Row gutter={16}>
<Col span={24}>
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Row align="top">
{/*Close Icon*/}
<Col flex="none" style={{display: "flex"}}>
{!editName && user.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>
{/* Name Label*/}
<Col flex="auto">
{!editName && user.id && formUser.name !== "" ? (
<div className={"access-control input-text ant-drawer-title"}
onClick={() => toggleEditName(true)}>{formUser.name ? formUser.name : formUser.name}
<EditOutlined/></div>
) : (
<Form.Item
name="name"
label="Name"
rules={[{
required: false,
message: 'Please add a new name for this user',
whitespace: true
}]}
>
<Input
placeholder={formUser.name}
ref={inputNameRef}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)}
autoComplete="off"/>
</Form.Item>)}
</Col>
</Row>
</Header>
</Col>
<Col span={24}>
<Form.Item
name="email"
label="Email"
>
<Input
disabled={user.id}
value={formUser.email}
style={{color: "#5a5c5a"}}
autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="role"
label="Role"
>
<Select
style={{width: '100%'}}
disabled={currentUser.role != null && currentUser.role !== "admin"}>
<Option value="admin">admin</Option>
<Option value="user">user</Option>
</Select>
</Form.Item>
</Col>
<Col span={24}>
<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}
disabled={currentUser.role != null && currentUser.role !== "admin"}
dropdownRender={dropDownRender}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Divider></Divider>
</Col>
{currentUser && currentUser.role !== "admin" && (
<div>
<Col span={24}>
<Alert
message={<div style={{color: "#5a5c5a"}}>
You are not an administrator, therefore you can't update users.</div>}
showIcon={false}
type="warning"/>
</Col>
<br></br>
</div>
)}
</Row>
</Form>
</Drawer>
}
</>
)
}
export default UserUpdate

View File

@@ -1,55 +0,0 @@
import React, {useState} from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
export const LinuxTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and install Brew (package manager)',
commands: (
<Button 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: 'Run NetBird and log in the browser:',
commands: [
`sudo netbird up`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Get your IP address:',
commands: [
`sudo ifconfig utun100`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand
])
return (
<TabSteps stepsItems={steps}/>
)
}
export default LinuxTab

View File

@@ -1,82 +0,0 @@
import React, { useState } from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import { getConfig } from "../../config";
import Paragraph from 'antd/lib/skeleton/Paragraph';
const { grpcApiOrigin } = getConfig();
export const UbuntuTab = () => {
const formatNetBirdUP = () => {
let cmd = "sudo netbird up"
if (grpcApiOrigin) {
cmd = "sudo netbird up --management-url " + grpcApiOrigin
}
return [
cmd
].join('\n')
}
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: true
} 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: true
} as StepCommand,
{
key: 3,
title: 'Run NetBird and log in the browser:',
commands: formatNetBirdUP(),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Get your IP address:',
commands: [
`ip addr show wt0`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
])
/*const clickTest = () => {
steps.push({
key: steps.length+1,
title: `Test ${steps.length+1}`,
commands: [`hi lorena!`].join('\n'),
copied: false
})
console.log(steps)
setSteps([...steps])
}*/
return (
<TabSteps stepsItems={steps} />
)
}
export default UbuntuTab

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M507,211.16c-1.42-1.19-14.25-10.94-41.79-10.94a132.55,132.55,0,0,0-21.61,1.9c-5.22-36.4-35.38-54-36.57-55l-7.36-4.28-4.75,6.9a101.65,101.65,0,0,0-13.06,30.45c-5,20.7-1.9,40.2,8.55,56.85-12.59,7.14-33,8.8-37.28,9H15.94A15.93,15.93,0,0,0,0,262.07,241.25,241.25,0,0,0,14.75,348.9C26.39,379.35,43.72,402,66,415.74,91.22,431.2,132.3,440,178.6,440a344.23,344.23,0,0,0,62.45-5.71,257.44,257.44,0,0,0,81.69-29.73,223.55,223.55,0,0,0,55.57-45.67c26.83-30.21,42.74-64,54.38-94h4.75c29.21,0,47.26-11.66,57.23-21.65a63.31,63.31,0,0,0,15.2-22.36l2.14-6.18Z"/><path d="M47.29,236.37H92.4a4,4,0,0,0,4-4h0V191.89a4,4,0,0,0-4-4H47.29a4,4,0,0,0-4,4h0v40.44a4.16,4.16,0,0,0,4,4h0"/><path d="M109.5,236.37h45.12a4,4,0,0,0,4-4h0V191.89a4,4,0,0,0-4-4H109.5a4,4,0,0,0-4,4v40.44a4.16,4.16,0,0,0,4,4"/><path d="M172.9,236.37H218a4,4,0,0,0,4-4h0V191.89a4,4,0,0,0-4-4H172.9a4,4,0,0,0-4,4h0v40.44a3.87,3.87,0,0,0,4,4h0"/><path d="M235.36,236.37h45.12a4,4,0,0,0,4-4V191.89a4,4,0,0,0-4-4H235.36a4,4,0,0,0-4,4h0v40.44a4,4,0,0,0,4,4h0"/><path d="M109.5,178.57h45.12a4.16,4.16,0,0,0,4-4V134.09a4,4,0,0,0-4-4H109.5a4,4,0,0,0-4,4v40.44a4.34,4.34,0,0,0,4,4"/><path d="M172.9,178.57H218a4.16,4.16,0,0,0,4-4V134.09a4,4,0,0,0-4-4H172.9a4,4,0,0,0-4,4h0v40.44a4,4,0,0,0,4,4"/><path d="M235.36,178.57h45.12a4.16,4.16,0,0,0,4-4V134.09a4.16,4.16,0,0,0-4-4H235.36a4,4,0,0,0-4,4h0v40.44a4.16,4.16,0,0,0,4,4"/><path d="M235.36,120.53h45.12a4,4,0,0,0,4-4V76a4.16,4.16,0,0,0-4-4H235.36a4,4,0,0,0-4,4h0v40.44a4.17,4.17,0,0,0,4,4"/><path d="M298.28,236.37H343.4a4,4,0,0,0,4-4V191.89a4,4,0,0,0-4-4H298.28a4,4,0,0,0-4,4h0v40.44a4.16,4.16,0,0,0,4,4"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><title/><path d="M432,32H80A64.07,64.07,0,0,0,16,96V416a64.07,64.07,0,0,0,64,64H432a64.07,64.07,0,0,0,64-64V96A64.07,64.07,0,0,0,432,32ZM96,256a16,16,0,0,1-10-28.49L150.39,176,86,124.49a16,16,0,1,1,20-25l80,64a16,16,0,0,1,0,25l-80,64A16,16,0,0,1,96,256Zm160,0H192a16,16,0,0,1,0-32h64a16,16,0,0,1,0,32Z"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1,204 @@
import {useGetTokenSilently} from "../../utils/token";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {Button, Col, Divider, Form, Input, InputNumber, message, Modal, Row, Space, Typography} from "antd";
import {Container} from "../Container";
import {CheckOutlined, CopyOutlined, QuestionCircleFilled} from "@ant-design/icons";
import SyntaxHighlighter from "react-syntax-highlighter";
import React, {useEffect, useRef, useState} from "react";
import {actions as personalAccessTokenActions} from "../../store/personal-access-token";
import {PersonalAccessTokenCreate} from "../../store/personal-access-token/types";
import {copyToClipboard} from "../../utils/common";
const {Title, Text, Paragraph} = Typography;
const ExpiresInDefault = 30
const styleNotification = {marginTop: 85}
const AddPATPopup = () => {
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const user = useSelector((state: RootState) => state.user.user)
const addTokenModalOpen = useSelector((state: RootState) => state.personalAccessToken.newPersonalAccessTokenPopupVisible)
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const [showPlainToken, setShowPlainToken] = useState(false);
const [tokenCopied, setTokenCopied] = useState(false);
const [plainToken, setPlainToken] = useState("")
const inputNameRef = useRef<any>(null)
const [form] = Form.useForm()
const savedPersonalAccessToken = useSelector((state: RootState) => state.personalAccessToken.savedPersonalAccessToken);
const onCopyClick = (text: string, copied: boolean) => {
copyToClipboard(text)
setTokenCopied(true)
if (copied) {
setTimeout(() => {
onCopyClick(text, false)
}, 2000)
}
}
const onCancel = () => {
setShowPlainToken(false)
setTokenCopied(false)
if (savedPersonalAccessToken.loading) return
dispatch(personalAccessTokenActions.setPersonalAccessToken({
user_id: "",
name: "",
expires_in: 7
} as PersonalAccessTokenCreate))
form.resetFields()
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(false));
dispatch(personalAccessTokenActions.setSavedPersonalAccessToken({...savedPersonalAccessToken, success: false}));
dispatch(personalAccessTokenActions.resetSavedPersonalAccessToken(null))
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
let personalAccessTokenToSave = {
user_id: user.id,
name: values.name,
expires_in: values.expires_in,
} as PersonalAccessTokenCreate
dispatch(personalAccessTokenActions.savePersonalAccessToken.request({
getAccessTokenSilently: getTokenSilently,
payload: personalAccessTokenToSave
}))
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const createKey = 'saving';
useEffect(() => {
if (savedPersonalAccessToken.loading) {
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
} else if (savedPersonalAccessToken.success) {
message.destroy(createKey)
setPlainToken(savedPersonalAccessToken.data.plain_token)
setShowPlainToken(true)
form.resetFields()
} else if (savedPersonalAccessToken.error) {
message.error({
content: 'Failed to create personal access token. You might not have enough permissions.',
key: createKey,
duration: 2,
style: styleNotification
});
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(false));
setShowPlainToken(false)
setTokenCopied(false)
dispatch(personalAccessTokenActions.setSavedPersonalAccessToken({...savedPersonalAccessToken, error: null}));
dispatch(personalAccessTokenActions.resetSavedPersonalAccessToken(null))
}
}, [savedPersonalAccessToken])
return (
<>
<Modal
open={addTokenModalOpen}
onCancel={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
{!showPlainToken && <Button disabled={savedPersonalAccessToken.loading} onClick={onCancel}>{"Cancel"}</Button>}
{!showPlainToken && <Button type="primary" disabled={showPlainToken}
onClick={handleFormSubmit}>{"Create token"}</Button>}
{showPlainToken && <Button type="primary" disabled={!showPlainToken} onClick={onCancel}>Done</Button>}
</Space>
}
width={460}
>
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "18px",fontWeight:"500"}}>
{showPlainToken ? "Token created successfully!" : "Create token"}
</Paragraph>
{!showPlainToken && <Paragraph type={"secondary"}
style={{
textAlign: "start",
whiteSpace: "pre-line",
marginTop: "-23px",
paddingBottom: "25px",
}}>
{"Use this token to access NetBird's public API"}
</Paragraph>}
{showPlainToken && <Paragraph type={"secondary"} style={{
textAlign: "start",
whiteSpace: "pre-line",
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,
}}
>
<Row gutter={16}>
<Col span={24}>
<Row align="top">
<Col flex="auto">
<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 token',
whitespace: true
}]}
>
<Input
placeholder={"for example \"Infra token\""}
ref={inputNameRef}
autoComplete="off"/>
</Form.Item>
</Col>
</Row>
</Col>
<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"}}
rules={[{
type: 'number',
min: 1,
max: 365,
message: 'The expiration should be set between 1 and 365 days'
}]}>
<InputNumber addonAfter=" Days" style={{maxWidth: "150px"}}/>
</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://docs.netbird.io/how-to/access-netbird-public-api">Learn more about personal access tokens</Button>
</Col>
</Row>
</Form>}
{showPlainToken &&
<Input style={{marginTop: "-15px", marginBottom: "25px"}} suffix={
!tokenCopied ? <Button type="text" size="middle" className="btn-copy-code" icon={<CopyOutlined/>}
style={{color: "rgb(107, 114, 128)", marginTop: "-1px"}}
onClick={() => onCopyClick(plainToken, true)}/>
: <Button type="text" size="middle" className="btn-copy-code" icon={<CheckOutlined/>}
style={{color: "green", marginTop: "-1px"}}/>
}
defaultValue={plainToken}
readOnly={true}
></Input>}
</Container>
</Modal>
{confirmModalContextHolder}
</>
)
}
export default AddPATPopup

View File

@@ -0,0 +1,362 @@
import {
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";
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
const AddServiceUserPopup = () => {
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
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 [confirmModal, confirmModalContextHolder] = Modal.useModal();
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,
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>
</>
);
};
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

@@ -0,0 +1,366 @@
import {
Button,
Col,
Divider,
Form,
Input,
Modal,
Row,
Select,
Space,
Tag,
Typography,
} from "antd";
import { Container } from "../Container";
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 { 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 InviteUserPopup = () => {
const { optionRender, blueTagRender } = useGetGroupTagHelpers();
const { getTokenSilently } = useGetTokenSilently();
const dispatch = useDispatch();
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 [confirmModal, confirmModalContextHolder] = Modal.useModal();
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 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>
</>
);
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}
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://docs.netbird.io/how-to/access-netbird-public-api"
>
Learn more about user
</Button>
</Col>
</Row>
</Form>
</Container>
</Modal>
{confirmModalContextHolder}
</>
);
};
export default InviteUserPopup;

View File

@@ -0,0 +1,101 @@
import React, {useState} from 'react';
import {Tabs, TabsProps} from "antd";
import Icon, {AndroidFilled, AppleFilled, WindowsFilled} from "@ant-design/icons";
import {ReactComponent as LinuxSVG} from "../../../icons/terminal_icon.svg";
import UbuntuTab from "./UbuntuTab";
import {ReactComponent as DockerSVG} from "../../../icons/docker_icon.svg";
import Paragraph from "antd/lib/typography/Paragraph";
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;
headline: string;
};
const detectOSTab = () => {
let os = 1;
if (navigator.userAgent.indexOf("Win") !== -1) os = 2;
if (navigator.userAgent.indexOf("Mac") !== -1) os = 3;
if (navigator.userAgent.indexOf("X11") !== -1) os = 1;
if (navigator.userAgent.indexOf("Linux") !== -1) os = 1
return os
}
export const AddPeerPopup: React.FC<Props> = ({
greeting,
headline,
}) => {
const [openTab, setOpenTab] = useState(detectOSTab);
const [width, setWidth] = useState<number>(window.innerWidth);
const isMobile = width <= 768;
const items: TabsProps['items'] = [
{
key: "1",
label: <span><Icon component={LinuxSVG}/>Linux</span>,
children: <UbuntuTab/>,
},
{
key: "2",
label: <span><WindowsFilled/>Windows</span>,
children: <WindowsTab/>,
},
{
key: "3",
label: <span><AppleFilled/>MacOS</span>,
children: <MacTab/>,
},
{
key: "4",
label: <span><AndroidFilled/>Android</span>,
children: <AndroidTab/>,
},
{
key: "5",
label: <span><Icon component={DockerSVG}/>Docker</span>,
children: <DockerTab/>,
}
];
return <>
{greeting && <Paragraph
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em", marginBottom: -10}}>
{greeting}
</Paragraph>}
<Paragraph
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em"}}>
{headline}
</Paragraph>
<Paragraph type={"secondary"}
style={{
marginTop: "-15px",
textAlign: "center",
whiteSpace: "pre-line",
}}>
To get started, install NetBird and log in using your {"\n"} email account.
</Paragraph>
<Tabs centered={!isMobile}
defaultActiveKey={openTab.toString()} tabPosition="top" animated={{inkBar: true, tabPane: false}}
items={items}/>
<Paragraph type={"secondary"}
style={{
marginTop: "15px",
}}>
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://docs.netbird.io/how-to/getting-started#installation"}>installation
guide</Link>}
</Paragraph>
</>
}
export default AddPeerPopup

View File

@@ -0,0 +1,51 @@
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';
const {Text} = Typography;
export const AndroidTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and install the application from Google Play Store:',
commands: (
<a 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,
{
key: 2,
title: 'Run the application and click on the "Connect" button in the middle of the screen',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3,
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

@@ -0,0 +1,63 @@
import React, {useState} from 'react';
import {StepCommand} from "./types"
import {formatDockerCommand, formatNetBirdUP} from "./common"
import SyntaxHighlighter from "react-syntax-highlighter";
import TabSteps from "./TabSteps";
import {Button, Typography} from "antd";
import Link from "antd/lib/typography/Link";
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://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}}>
{/*<Text style={{fontWeight: "bold"}}>
Run in Docker
</Text>
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
<SyntaxHighlighter language="bash">
{formatDockerCommand()}
</SyntaxHighlighter>
</div>*/}
<Text style={{fontWeight: "bold"}}>
Install on Ubuntu
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
)
}
export default DockerTab

View File

@@ -12,7 +12,7 @@ export const OtherTab = () => {
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">
<Button type="primary" href={`https://docs.netbird.io/how-to/getting-started#binary-install`} target="_blank">
Documentation
</Button>
),

View File

@@ -0,0 +1,136 @@
import React, {useState} from 'react';
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 SyntaxHighlighter from "react-syntax-highlighter";
import {QuestionCircleOutlined} from "@ant-design/icons";
const {Panel} = Collapse;
const {Text} = Typography;
export const LinuxTab = () => {
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 style={{marginRight: "10px"}} type="primary" href="https://pkgs.netbird.io/macos/amd64">Download
for Intel</Button>
<Button style={{marginRight: "10px"}} type="default" href="https://pkgs.netbird.io/macos/arm64">Download
for M1 & M2</Button>
</Row>
),
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'),
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 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}}>
<SyntaxHighlighter language="bash">
curl -fsSL https://pkgs.netbird.io/install.sh | sh
</SyntaxHighlighter>
</div>
<Text style={{fontWeight: "bold"}}>
Or install manually with HomeBrew
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
</Panel>
</Collapse>
</div>
)
}
export default LinuxTab

View File

@@ -2,16 +2,16 @@ import "highlight.js/styles/mono-blue.css";
import "highlight.js/lib/languages/bash";
import { StepCommand } from './types'
import SyntaxHighlighter from 'react-syntax-highlighter';
import { monoBlue } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import {
Typography,
Space,
Steps, Button
Steps, Button, Popover, StepsProps
} from "antd";
import {copyToClipboard} from "../../utils/common";
import {copyToClipboard} from "../../../../utils/common";
import {CheckOutlined, CopyOutlined} from "@ant-design/icons";
import React, {useEffect, useState} from "react";
const { Step } = Steps;
const {Text} = Typography;
type Props = {
stepsItems: Array<StepCommand>
@@ -36,35 +36,22 @@ const TabSteps:React.FC<Props> = ({stepsItems}) => {
}, 2000)
}
}
return (
<Steps direction="vertical" current={0}>
<Steps direction="vertical" size={"small"}>
{steps.map(c =>
<Step
status={"process"}
key={c.key}
title={c.title}
title={<Text>{c.title}</Text>}
description={
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex"}}>
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex", fontSize: ".85em"}}>
{ (c.commands && (typeof c.commands === 'string')) ? (
<SyntaxHighlighter language="bash" style={monoBlue}>
<SyntaxHighlighter language="bash">
{c.commands}
</SyntaxHighlighter>
) : (
c.commands
)}
{ c.showCopyButton &&
<>
{ !c.copied ? (
<Button type="text" size="large" className="btn-copy-code" icon={<CopyOutlined/>}
style={{color: "rgb(107, 114, 128)"}}
onClick={() => onCopyClick(c.key, c.commands, true)}/>
): (
<Button type="text" size="large" className="btn-copy-code" icon={<CheckOutlined/>}
style={{color: "green"}}/>
)}
</>
}
</Space>
}
/>

View File

@@ -0,0 +1,68 @@
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";
const {Title, Paragraph, Text} = Typography;
export const UbuntuTab = () => {
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
])
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>
</div>
)
}
export default UbuntuTab

View File

@@ -1,33 +1,31 @@
import React, {useState} from 'react';
import { Button } from "antd";
import {Button, Typography} from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import {getConfig} from "../../config";
const {latestVersion} = getConfig();
const {Text} = Typography;
export const WindowsTab = () => {
const releaseVersion = latestVersion ? latestVersion.replace("v", "") : "0.6.3"
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and run Windows installer:',
commands: (
<Button type="primary" href={`https://github.com/netbirdio/netbird/releases/download/v${releaseVersion}/netbird_installer_${releaseVersion}_windows_amd64.exe`} target="_blank">Download NetBird</Button>
<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.',
title: 'Click on "Connect" from the NetBird icon in your system tray',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3,
title: 'Log in your browser.\n',
title: 'Sign up using your email address',
commands: '',
copied: false,
showCopyButton: false
@@ -35,7 +33,15 @@ export const WindowsTab = () => {
])
return (
<TabSteps stepsItems={steps}/>
<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on Windows
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import {getConfig} from "../../../../config";
const { grpcApiOrigin } = getConfig();
export const formatNetBirdUP = () => {
let cmd = "netbird up"
if (grpcApiOrigin) {
cmd = "netbird up --management-url " + grpcApiOrigin
}
return [
cmd
].join('\n')
}
export const formatDockerCommand = () => {
let cmd = ["docker run --rm -d",
" --cap-add=NET_ADMIN",
" -e NB_SETUP_KEY=SETUP_KEY",
" -v netbird-client:/etc/netbird"]
if (grpcApiOrigin) {
cmd.push(" -e NB_MANAGEMENT_URL="+grpcApiOrigin)
}
cmd.push(" netbirdio/netbird:latest")
return cmd.join(' \\\n')
}

View File

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

View File

@@ -2,13 +2,14 @@
"auth0Auth": "$USE_AUTH0",
"authAuthority": "$AUTH_AUTHORITY",
"authClientId": "$AUTH_CLIENT_ID",
"authClientSecret": "$AUTH_CLIENT_SECRET",
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
"authAudience": "$AUTH_AUDIENCE",
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
"latestVersion": "$NETBIRD_LATEST_VERSION",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"redirectURI": "$AUTH_REDIRECT_URI",
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI"
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
"tokenSource": "$NETBIRD_TOKEN_SOURCE"
}

View File

@@ -9,7 +9,7 @@ if (process.env.NODE_ENV !== 'production') {
const defaultRedirectURI = '/#callback';
const defaultSilentRedirectURI = '/#silent-callback'
const defaultTokenSource = "accessToken"
export function getConfig() {
let redirectURI = defaultRedirectURI
if (configJson.redirectURI) {
@@ -21,17 +21,23 @@ export function getConfig() {
silentRedirectURI = configJson.silentRedirectURI
}
let tokenSource = defaultTokenSource
if (configJson.tokenSource) {
tokenSource = configJson.tokenSource
}
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,
latestVersion: configJson.latestVersion,
audience: configJson.authAudience,
hotjarTrackID: configJson.hotjarTrackID,
redirectURI: redirectURI,
silentRedirectURI: silentRedirectURI,
tokenSource: tokenSource,
};
}

View File

@@ -1,14 +1,19 @@
@import '~antd/dist/antd.css';
@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';
/*@tailwind base;*/
/*@tailwind components;*/
/*@tailwind utilities;*/
html,
body,
* {
font-family: 'Arimo', sans-serif !important;
}
body {
font-size: 16px;
background-color: #f0f2f5;
}
.ant-layout-header {
background: #fff;
padding: 0;
@@ -27,13 +32,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;
}
@@ -51,23 +57,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;
}
@@ -79,9 +87,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;
}
@@ -92,20 +101,6 @@ body {
align-self: center;
}
.ant-steps-item-tail::after {
background-color: #1890ff !important;
}
.ant-steps-item-icon {
background: #1890ff !important;
color: #ffffff !important;
border: none;
}
.ant-steps-icon {
color: #ffffff !important;
}
.nb-code {
margin-bottom: 1em;
}
@@ -135,4 +130,145 @@ body {
.access-control.ant-drawer-subtitle {
line-height: 22px;
margin: 24px 0;
}
.ant-steps-item-icon {
background: #EBEBEB !important;
border-color: #EBEBEB !important;
}
.ant-steps-icon-dot {
background: #EBEBEB !important;
border-color: #EBEBEB !important;
}
.ant-steps-icon {
background: #EBEBEB !important;
border: none;
color: black !important;
}
.ant-steps-item-tail::after {
background: #EBEBEB !important;
}
.ant-steps-item-tail {
border: none;
}
td.non-highlighted-table-column {
background-color: #FFFFFF !important;
}
.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;
}
.ant-modal-content {
border-radius: 0px !important;
}
.ant-form-item-explain-error {
font-weight: 500;
}
.menlo-font,
.menlo-font * {
font-family: 'Menlo', monospace !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;
}

View File

@@ -34,7 +34,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)
...(config.audience ? {extras: {audience: config.audience}} : null),
...(config.clientSecret ? {token_request_extras: {client_secret: config.clientSecret}} : null)
};
const root = ReactDOM.createRoot(

View File

@@ -3,12 +3,6 @@ import axios from 'axios';
import {ApiError, ApiRequestParams, ApiResponse} from './types';
import {headersFactory, RequestHeader} from './header-factory';
/*axios.interceptors.response.use(undefined, err => {
let res = err.response;
if (res.status === 401) {
}
})*/
async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>> {
const data = params.data ? (params.data as any).payload : undefined;
const url = `${params.urlBase}${params.url}`;
@@ -26,8 +20,10 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
statusCode: -1
};
let queryParams = (params.data as any).queryParams ? (params.data as any).queryParams : {};
try {
response = await axios.request({url, data, method: params.method, headers: builtHeader as any});
response = await axios.request({url, data, method: params.method, headers: builtHeader as any, params: queryParams});
} catch (err: any) {
error = <ApiError>{
code: err ? err.code : '-1',

View File

@@ -2,6 +2,7 @@ import { Method } from 'axios';
export interface RequestPayload<T> {
getAccessTokenSilently: any | null;
queryParams?: any | null;
payload:T;
}
@@ -27,6 +28,7 @@ export interface RequestConfig {
export interface ApiRequestParams extends RequestConfig {
method: Method;
url: string;
params?: any,
data: unknown;
urlBase: string;
}

View File

@@ -1,29 +1,30 @@
export enum StorageKey {
token
token,
hadFirstRun
}
const setLocalItem = async <T>(key: StorageKey, value: T): Promise<void> => {
try {
localStorage.setItem(`@net-bird:${key}`, JSON.stringify(value));
} catch (err) {
console.log(err);
}
try {
localStorage.setItem(`@net-bird:${key}`, JSON.stringify(value));
} catch (err) {
console.log(err);
}
};
const getLocalItem = async <T>(key: StorageKey): Promise<T | null> => {
try {
const item = localStorage.getItem(`@net-bird:${key}`);
if (!item) {
return null;
try {
const item = localStorage.getItem(`@net-bird:${key}`);
if (!item) {
return null;
}
return JSON.parse(item) as T;
} catch (err) {
console.log(err);
}
return JSON.parse(item) as T;
} catch (err) {
console.log(err);
}
return null;
return null;
};
export {
getLocalItem,
setLocalItem
getLocalItem,
setLocalItem
}

View File

@@ -0,0 +1,23 @@
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
import {Account} from './types';
import {ApiError, ChangeResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getAccounts: createAsyncAction(
'GET_ACCOUNTS_REQUEST',
'GET_ACCOUNTS_SUCCESS',
'GET_ACCOUNTS_FAILURE',
)<RequestPayload<null>, Account[], ApiError>(),
updateAccount: createAsyncAction(
'UPDATE_ACCOUNT',
'UPDATE_ACCOUNT_SUCCESS',
'UPDATE_ACCOUNT_FAILURE',
)<RequestPayload<Account>, ChangeResponse<Account | null>, ChangeResponse<Account | null>>(),
setUpdateAccount: createAction('SET_UPDATED_ACCOUNT')<ChangeResponse<Account | null>>(),
resetUpdateAccount: createAction('RESET_UPDATED_ACCOUNT')<null>(),
};
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,54 @@
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";
type StateType = Readonly<{
data: Account[] | null;
loading: boolean;
failed: ApiError | null;
savedAccount: ChangeResponse<Account | null>;
}>;
const initialState: StateType = {
data: [],
loading: false,
failed: null,
savedAccount: <ChangeResponse<Account | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
};
const data = createReducer<Account[], ActionTypes>(initialState.data as Account[])
.handleAction(actions.getAccounts.success,(_, action) => action.payload)
.handleAction(actions.getAccounts.failure, () => []);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getAccounts.request, () => true)
.handleAction(actions.getAccounts.success, () => false)
.handleAction(actions.getAccounts.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getAccounts.request, () => null)
.handleAction(actions.getAccounts.success, () => null)
.handleAction(actions.getAccounts.failure, (store, action) => action.payload);
const updatedAccount = createReducer<ChangeResponse<Account | null>, ActionTypes>(initialState.savedAccount)
.handleAction(actions.updateAccount.request, () => initialState.savedAccount)
.handleAction(actions.updateAccount.success, (store, action) => action.payload)
.handleAction(actions.updateAccount.failure, (store, action) => action.payload)
.handleAction(actions.setUpdateAccount, (store, action) => action.payload)
.handleAction(actions.resetUpdateAccount, () => initialState.savedAccount)
export default combineReducers({
data,
loading,
failed,
updatedAccount
});

View File

@@ -0,0 +1,64 @@
import {all, call, put, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, ChangeResponse} from '../../services/api-client/types';
import {Account} from './types'
import service from './service';
import actions from './actions';
export function* getAccounts(action: ReturnType<typeof actions.getAccounts.request>): Generator {
try {
const effect = yield call(service.getAccounts, action.payload);
const response = effect as ApiResponse<Account[]>;
yield put(actions.getAccounts.success(response.body));
} catch (err) {
yield put(actions.getAccounts.failure(err as ApiError));
}
}
export function* updateAccount(action: ReturnType<typeof actions.updateAccount.request>): Generator {
try {
yield put(actions.setUpdateAccount({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))
const account = action.payload.payload
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: account
}
const effect = yield call(service.updateAccount, payloadToSave)
const response = effect as ApiResponse<Account>;
yield put(actions.updateAccount.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as ChangeResponse<Account | null>));
} catch (err) {
console.log(err)
yield put(actions.updateAccount.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Account | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getAccounts.request, getAccounts),
takeLatest(actions.updateAccount.request, updateAccount),
]);
}

View File

@@ -0,0 +1,19 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import {Account} from './types';
export default {
async getAccounts(payload:RequestPayload<null>): Promise<ApiResponse<Account[]>> {
return apiClient.get<Account[]>(
`/api/accounts`,
payload
);
},
async updateAccount(payload:RequestPayload<Account>): Promise<ApiResponse<Account>> {
const id = payload.payload.id
return apiClient.put<Account>(
`/api/accounts/${id}`,
payload
);
}
};

View File

@@ -0,0 +1,11 @@
import {ExpiresInValue} from "../../views/ExpiresInInput";
export interface Account {
id: string;
settings: { peer_login_expiration_enabled: boolean, peer_login_expiration: number}
}
export interface FormAccount extends Account {
peer_login_expiration_enabled: boolean
peer_login_expiration_formatted : ExpiresInValue
}

View File

@@ -0,0 +1,26 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {DNSSettings, DNSSettingsToSave} from './types';
import {ApiError, CreateResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getDNSSettings: createAsyncAction(
'GET_DNSSettings_REQUEST',
'GET_DNSSettings_SUCCESS',
'GET_DNSSettings_FAILURE',
)<RequestPayload<null>, DNSSettings, ApiError>(),
saveDNSSettings: createAsyncAction(
'SAVE_DNSSettings_REQUEST',
'SAVE_DNSSettings_SUCCESS',
'SAVE_DNSSettings_FAILURE',
)<RequestPayload<DNSSettingsToSave>, CreateResponse<DNSSettings | null>, CreateResponse<DNSSettings | null>>(),
setSavedDNSSettings: createAction('SET_CREATE_DNSSettings')<CreateResponse<DNSSettings | null>>(),
resetSavedDNSSettings: createAction('RESET_CREATE_DNSSettings')<null>(),
setDNSSettings: createAction('SET_DNSSettings')<DNSSettings>(),
setSetupNewDNSSettingsVisible: createAction('SET_SETUP_NEW_DNSSettings_VISIBLE')<boolean>(),
setSetupNewDNSSettingsHA: createAction('SET_SETUP_NEW_DNSSettings_HA')<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,79 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { DNSSettings } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, CreateResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: DNSSettings | null;
dnsSettings: DNSSettings | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
savedDNSSettings: CreateResponse<DNSSettings | null>;
setupNewDNSSettingsVisible: boolean;
setupNewDNSSettingsHA: boolean
}>;
const initialState: StateType = {
data: null,
dnsSettings: null,
loading: false,
failed: null,
saving: false,
savedDNSSettings: <CreateResponse<DNSSettings | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
setupNewDNSSettingsVisible: false,
setupNewDNSSettingsHA: false
};
const data = createReducer<DNSSettings, ActionTypes>(initialState.data as DNSSettings)
.handleAction(actions.getDNSSettings.success,(settings, action) => action.payload)
.handleAction(actions.getDNSSettings.failure,(settings, _) => settings);
const dnsSettings = createReducer<DNSSettings, ActionTypes>(initialState.dnsSettings as DNSSettings)
.handleAction(actions.setDNSSettings, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getDNSSettings.request, () => true)
.handleAction(actions.getDNSSettings.success, () => false)
.handleAction(actions.getDNSSettings.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getDNSSettings.request, () => null)
.handleAction(actions.getDNSSettings.success, () => null)
.handleAction(actions.getDNSSettings.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getDNSSettings.request, () => true)
.handleAction(actions.getDNSSettings.success, () => false)
.handleAction(actions.getDNSSettings.failure, () => false);
const savedDNSSettings = createReducer<CreateResponse<DNSSettings | null>, ActionTypes>(initialState.savedDNSSettings)
.handleAction(actions.saveDNSSettings.request, () => initialState.savedDNSSettings)
.handleAction(actions.saveDNSSettings.success, (store, action) => action.payload)
.handleAction(actions.saveDNSSettings.failure, (store, action) => action.payload)
.handleAction(actions.setSavedDNSSettings, (store, action) => action.payload)
.handleAction(actions.resetSavedDNSSettings, () => initialState.savedDNSSettings)
const setupNewDNSSettingsVisible = createReducer<boolean, ActionTypes>(initialState.setupNewDNSSettingsVisible)
.handleAction(actions.setSetupNewDNSSettingsVisible, (store, action) => action.payload)
const setupNewDNSSettingsHA = createReducer<boolean, ActionTypes>(initialState.setupNewDNSSettingsHA)
.handleAction(actions.setSetupNewDNSSettingsHA, (store, action) => action.payload)
export default combineReducers({
data,
dnsSettings,
loading,
failed,
saving,
savedDNSSettings,
setupNewDNSSettingsVisible,
setupNewDNSSettingsHA
});

View File

@@ -0,0 +1,95 @@
import {all, call, put, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse} from '../../services/api-client/types';
import {DNSSettings} 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";
export function* getDNSSettings(action: ReturnType<typeof actions.getDNSSettings.request>): Generator {
try {
const effect = yield call(service.getDNSSettings, action.payload);
const response = effect as ApiResponse<DNSSettings>;
yield put(actions.getDNSSettings.success(response.body));
} catch (err) {
yield put(actions.getDNSSettings.failure(err as ApiError));
}
}
export function* saveDNSSettings(action: ReturnType<typeof actions.saveDNSSettings.request>): Generator {
try {
yield put(actions.setSavedDNSSettings({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<DNSSettings | null>))
const settingsToSave = action.payload.payload
let groupsToCreate = settingsToSave.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 = [...settingsToSave.disabled_management_groups, ...resGroups]
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
disabled_management_groups: newGroups,
} as DNSSettings
}
let effect = yield call(service.editDNSSettings, payloadToSave);
const response = effect as ApiResponse<DNSSettings>;
yield put(actions.saveDNSSettings.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<DNSSettings | null>));
yield put(groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
yield put(actions.getDNSSettings.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
} catch (err) {
yield put(groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
yield put(actions.saveDNSSettings.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as CreateResponse<DNSSettings | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getDNSSettings.request, getDNSSettings),
takeLatest(actions.saveDNSSettings.request, saveDNSSettings),
]);
}

View File

@@ -0,0 +1,18 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { DNSSettings } from './types';
export default {
async getDNSSettings(payload:RequestPayload<null>): Promise<ApiResponse<DNSSettings>> {
return apiClient.get<DNSSettings>(
`/api/dns/settings`,
payload
);
},
async editDNSSettings(payload:RequestPayload<DNSSettings>): Promise<ApiResponse<DNSSettings>> {
return apiClient.put<DNSSettings>(
`/api/dns/settings`,
payload
);
},
};

View File

@@ -0,0 +1,8 @@
export interface DNSSettings {
disabled_management_groups: string[]
}
export interface DNSSettingsToSave extends DNSSettings
{
groupsToCreate: string[]
}

View File

@@ -0,0 +1,14 @@
import {ActionType, createAsyncAction} from 'typesafe-actions';
import {Event} from './types';
import {ApiError, RequestPayload} from '../../services/api-client/types';
const actions = {
getEvents: createAsyncAction(
'GET_EVENTS_REQUEST',
'GET_EVENTS_SUCCESS',
'GET_EVENTS_FAILURE',
)<RequestPayload<null>, Event[], ApiError>(),
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

7
src/store/event/index.ts Normal file
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,38 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Event } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, CreateResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: Event[] | null;
loading: boolean;
failed: ApiError | null;
}>;
const initialState: StateType = {
data: [],
loading: false,
failed: null,
};
const data = createReducer<Event[], ActionTypes>(initialState.data as Event[])
.handleAction(actions.getEvents.success,(_, action) => action.payload)
.handleAction(actions.getEvents.failure, () => []);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getEvents.request, () => true)
.handleAction(actions.getEvents.success, () => false)
.handleAction(actions.getEvents.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getEvents.request, () => null)
.handleAction(actions.getEvents.success, () => null)
.handleAction(actions.getEvents.failure, (store, action) => action.payload);
export default combineReducers({
data,
loading,
failed,
});

23
src/store/event/sagas.ts Normal file
View File

@@ -0,0 +1,23 @@
import {all, call, put, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse} from '../../services/api-client/types';
import {Event} from './types'
import service from './service';
import actions from './actions';
export function* getEvents(action: ReturnType<typeof actions.getEvents.request>): Generator {
try {
const effect = yield call(service.getEvents, action.payload);
const response = effect as ApiResponse<Event[]>;
yield put(actions.getEvents.success(response.body));
} catch (err) {
yield put(actions.getEvents.failure(err as ApiError));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getEvents.request, getEvents),
]);
}

View File

@@ -0,0 +1,12 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import {Event} from './types';
export default {
async getEvents(payload:RequestPayload<null>): Promise<ApiResponse<Event[]>> {
return apiClient.get<Event[]>(
`/api/events`,
payload
);
}
};

9
src/store/event/types.ts Normal file
View File

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

View File

@@ -5,10 +5,15 @@ 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';
import { sagas as nameserverGroupSagas } from './nameservers';
import { sagas as eventSagas } from './event';
import { sagas as dnsSettingsSagas } from './dns-settings';
import { sagas as accountSagas } from './account';
import { sagas as personalAccessTokenSagas } from './personal-access-token';
import rootReducer from './root-reducer';
import { apiClient } from '../services/api-client';
@@ -24,8 +29,13 @@ sagaMiddleware.run(peerSagas);
sagaMiddleware.run(setupKeySagas);
sagaMiddleware.run(userSagas);
sagaMiddleware.run(ruleSagas);
sagaMiddleware.run(policySagas);
sagaMiddleware.run(groupSagas);
sagaMiddleware.run(routeSagas);
sagaMiddleware.run(nameserverGroupSagas);
sagaMiddleware.run(eventSagas);
sagaMiddleware.run(dnsSettingsSagas);
sagaMiddleware.run(accountSagas);
sagaMiddleware.run(personalAccessTokenSagas);
export { apiClient, rootReducer, store };
export { apiClient, rootReducer, store };

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

@@ -59,7 +59,7 @@ export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.reques
} as DeleteResponse<string | null>));
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.ip !== action.payload.payload)))
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deletedPeer.failure({
loading: false,

View File

@@ -14,6 +14,9 @@ export interface Peer {
user_id?: string,
ui_version?: string,
dns_label: string,
last_login: string,
login_expired: boolean,
login_expiration_enabled: boolean
}
export interface FormPeer extends Peer {
@@ -40,6 +43,10 @@ export interface PeerIPToName {
[key: string]: string;
}
export interface PeerIPToID {
[key: string]: string;
}
export interface PeerDataTable extends Peer {
key: string;
groups: Group[];

View File

@@ -0,0 +1,41 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated, SpecificPAT} from './types';
import {
ApiError,
CreateResponse,
DeleteResponse,
RequestPayload
} from '../../services/api-client/types';
const actions = {
getPersonalAccessTokens: createAsyncAction(
'GET_PERSONAL_ACCESS_TOKEN_REQUEST',
'GET_PERSONAL_ACCESS_TOKEN_SUCCESS',
'GET_PERSONAL_ACCESS_TOKEN_FAILURE',
)<RequestPayload<string>, PersonalAccessToken[], ApiError>(),
resetPersonalAccessTokens: createAction('RESET_PERSONAL_ACCESS_TOKENS')<null>(),
savePersonalAccessToken: createAsyncAction(
'SAVE_PERSONAL_ACCESS_TOKEN_REQUEST',
'SAVE_PERSONAL_ACCESS_TOKEN_SUCCESS',
'SAVE_PERSONAL_ACCESS_TOKEN_FAILURE',
)<RequestPayload<PersonalAccessTokenCreate>, CreateResponse<PersonalAccessTokenGenerated | null>, CreateResponse<PersonalAccessTokenGenerated | null>>(),
setSavedPersonalAccessToken: createAction('SET_PERSONAL_ACCESS_TOKEN_KEY')<CreateResponse<PersonalAccessTokenGenerated | null>>(),
resetSavedPersonalAccessToken: createAction('RESET_PERSONAL_ACCESS_TOKEN_KEY')<null>(),
deletePersonalAccessToken: createAsyncAction(
'DELETE_PERSONAL_ACCESS_TOKEN_REQUEST',
'DELETE_PERSONAL_ACCESS_TOKEN_SUCCESS',
'DELETE_PERSONAL_ACCESS_TOKEN_FAILURE'
)<RequestPayload<SpecificPAT>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletePersonalAccessToken: createAction('SET_DELETE_PERSONAL_ACCESS_TOKEN')<DeleteResponse<string | null>>(),
resetDeletedPersonalAccessToken: createAction('RESET_DELETE_PERSONAL_ACCESS_TOKEN')<null>(),
removePersonalAccessToken: createAction('REMOVE_PERSONAL_ACCESS_TOKEN')<string>(),
setPersonalAccessToken: createAction('SET_PERSONAL_ACCESS_TOKEN')<PersonalAccessTokenCreate>(),
setNewPersonalAccessTokenVisible: createAction('SET_NEW_PERSONAL_ACCESS_TOKEN_VISIBLE')<boolean>(),
setNewPersonalAccessTokenPopupVisible: createAction('SET_NEW_PERSONAL_ACCESS_TOKEN_POPUP_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,104 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated} from "./types";
type StateType = Readonly<{
data: PersonalAccessToken[] | null;
personalAccessToken: PersonalAccessTokenCreate | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deletedPersonalAccessToken: DeleteResponse<string | null>;
revokedPersonalAccessToken: ChangeResponse<string | null>;
savedPersonalAccessToken: CreateResponse<PersonalAccessTokenGenerated | null>;
newPersonalAccessTokenVisible: boolean
newPersonalAccessTokenPopupVisible: boolean
}>;
const initialState: StateType = {
data: [],
personalAccessToken: null,
loading: false,
failed: null,
saving: false,
deletedPersonalAccessToken: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
revokedPersonalAccessToken: <ChangeResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
savedPersonalAccessToken: <CreateResponse<PersonalAccessTokenGenerated | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
newPersonalAccessTokenVisible: false,
newPersonalAccessTokenPopupVisible: false
};
const data = createReducer<PersonalAccessToken[], ActionTypes>(initialState.data as PersonalAccessToken[])
.handleAction(actions.getPersonalAccessTokens.success,(_, action) => action.payload)
.handleAction(actions.getPersonalAccessTokens.failure, () => [])
.handleAction(actions.resetPersonalAccessTokens, () => []);
const personalAccessToken = createReducer<PersonalAccessTokenCreate, ActionTypes>(initialState.personalAccessToken as PersonalAccessTokenCreate)
.handleAction(actions.setPersonalAccessToken, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getPersonalAccessTokens.request, () => true)
.handleAction(actions.getPersonalAccessTokens.success, () => false)
.handleAction(actions.getPersonalAccessTokens.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getPersonalAccessTokens.request, () => null)
.handleAction(actions.getPersonalAccessTokens.success, () => null)
.handleAction(actions.getPersonalAccessTokens.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getPersonalAccessTokens.request, () => true)
.handleAction(actions.getPersonalAccessTokens.success, () => false)
.handleAction(actions.getPersonalAccessTokens.failure, () => false);
const deletedPersonalAccessToken = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedPersonalAccessToken)
.handleAction(actions.deletePersonalAccessToken.request, () => initialState.deletedPersonalAccessToken)
.handleAction(actions.deletePersonalAccessToken.success, (store, action) => action.payload)
.handleAction(actions.deletePersonalAccessToken.failure, (store, action) => action.payload)
.handleAction(actions.setDeletePersonalAccessToken, (store, action) => action.payload)
.handleAction(actions.resetDeletedPersonalAccessToken, (store, action) => initialState.deletedPersonalAccessToken);
const savedPersonalAccessToken = createReducer<CreateResponse<PersonalAccessTokenGenerated | null>, ActionTypes>(initialState.savedPersonalAccessToken)
.handleAction(actions.savePersonalAccessToken.request, () => initialState.savedPersonalAccessToken)
.handleAction(actions.savePersonalAccessToken.success, (store, action) => action.payload)
.handleAction(actions.savePersonalAccessToken.failure, (store, action) => action.payload)
.handleAction(actions.setSavedPersonalAccessToken, (store, action) => action.payload)
.handleAction(actions.resetSavedPersonalAccessToken, () => initialState.savedPersonalAccessToken)
const newPersonalAccessTokenVisible = createReducer<boolean, ActionTypes>(initialState.newPersonalAccessTokenVisible)
.handleAction(actions.setNewPersonalAccessTokenVisible, (store, action) => action.payload)
const newPersonalAccessTokenPopupVisible = createReducer<boolean, ActionTypes>(initialState.newPersonalAccessTokenPopupVisible)
.handleAction(actions.setNewPersonalAccessTokenPopupVisible, (store, action) => action.payload)
export default combineReducers({
data,
personalAccessToken: personalAccessToken,
loading,
failed,
saving,
deletedPersonalAccessToken: deletedPersonalAccessToken,
savedPersonalAccessToken: savedPersonalAccessToken,
newPersonalAccessTokenVisible: newPersonalAccessTokenVisible,
newPersonalAccessTokenPopupVisible: newPersonalAccessTokenPopupVisible
});

View File

@@ -0,0 +1,101 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated} from './types'
import service from './service';
import actions from './actions';
export function* getPersonalAccessTokens(action: ReturnType<typeof actions.getPersonalAccessTokens.request>): Generator {
try {
const effect = yield call(service.getAllPersonalAccessTokens, action.payload);
const response = effect as ApiResponse<PersonalAccessToken[]>;
yield put(actions.getPersonalAccessTokens.success(response.body));
} catch (err) {
yield put(actions.getPersonalAccessTokens.failure(err as ApiError));
}
}
export function* savePersonalAccessToken(action: ReturnType<typeof actions.savePersonalAccessToken.request>): Generator {
try {
yield put(actions.setSavedPersonalAccessToken({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<PersonalAccessTokenGenerated | null>))
const tokenToSave = action.payload.payload
let effect = yield call(service.createPersonalAccessToken, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
user_id: tokenToSave.user_id,
name: tokenToSave.name,
expires_in: tokenToSave.expires_in,
} as PersonalAccessTokenCreate
});
const response = effect as ApiResponse<PersonalAccessTokenGenerated>;
yield put(actions.savePersonalAccessToken.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<PersonalAccessTokenGenerated | null>));
yield put(actions.getPersonalAccessTokens.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: tokenToSave.user_id }));
} catch (err) {
yield put(actions.savePersonalAccessToken.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<PersonalAccessTokenGenerated | null>));
}
}
export function* deletePersonalAccessToken(action: ReturnType<typeof actions.deletePersonalAccessToken.request>): Generator {
try {
yield call(actions.setDeletePersonalAccessToken,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deletePersonalAccessToken, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deletePersonalAccessToken.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const personalAccessTokens = (yield select(state => state.personalAccessToken.data)) as PersonalAccessToken[]
yield put(actions.getPersonalAccessTokens.success(personalAccessTokens.filter((p:PersonalAccessToken) => p.id !== action.payload.payload.id)))
} catch (err) {
yield put(actions.deletePersonalAccessToken.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.getPersonalAccessTokens.request, getPersonalAccessTokens),
takeLatest(actions.savePersonalAccessToken.request, savePersonalAccessToken),
takeLatest(actions.deletePersonalAccessToken.request, deletePersonalAccessToken)
]);
}

View File

@@ -0,0 +1,34 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import {
PersonalAccessToken,
PersonalAccessTokenCreate, PersonalAccessTokenGenerated,
SpecificPAT
} from './types';
export default {
async getAllPersonalAccessTokens(payload:RequestPayload<string>): Promise<ApiResponse<PersonalAccessToken[]>> {
return apiClient.get<PersonalAccessToken[]>(
`/api/users/` + payload.payload + `/tokens`,
payload
);
},
async getPersonalAccessToken(payload:RequestPayload<SpecificPAT>): Promise<ApiResponse<PersonalAccessToken>> {
return apiClient.get<PersonalAccessToken>(
`/api/users/` + payload.payload.user_id + `/tokens/` + payload.payload.id,
payload
);
},
async deletePersonalAccessToken(payload:RequestPayload<SpecificPAT>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/users/` + payload.payload.user_id + `/tokens/` + payload.payload.id,
payload
);
},
async createPersonalAccessToken(payload:RequestPayload<PersonalAccessTokenCreate>): Promise<ApiResponse<PersonalAccessTokenGenerated>> {
return apiClient.post<PersonalAccessTokenGenerated>(
`/api/users/` + payload.payload.user_id + `/tokens`,
payload
);
},
};

View File

@@ -0,0 +1,26 @@
export interface PersonalAccessToken {
id: string;
name: string;
expiration_date: string;
created_by: string;
created_at: string;
last_used: string;
}
export interface SpecificPAT {
name: string,
user_id: string,
id: string,
}
export interface PersonalAccessTokenGenerated {
plain_token: string,
personal_access_token: PersonalAccessToken
}
export interface PersonalAccessTokenCreate {
user_id: string,
name: string,
expires_in: number,
}

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

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

@@ -0,0 +1,223 @@
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 call(actions.setDeletedPolicy, {
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | 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

@@ -1,10 +1,14 @@
import { actions as PeerActions } from './peer';
import { actions as SetupKeyActions } from './setup-key';
import { actions as UserActions } from './user';
import { actions as GroupActions } from './group';
import { actions as RuleActions } from './rule';
import { actions as RouteActions } from './route';
import { actions as NameServerGroupActions } from './nameservers';
import {actions as PeerActions} from './peer';
import {actions as SetupKeyActions} from './setup-key';
import {actions as UserActions} from './user';
import {actions as GroupActions} from './group';
import {actions as RuleActions} from './rule';
import {actions as RouteActions} from './route';
import {actions as NameServerGroupActions} from './nameservers';
import {actions as EventActions} from './event';
import {actions as DNSSettingsActions} from './dns-settings';
import {actions as AccountActions} from './account';
import {actions as PersonalAccessTokenActions} from './personal-access-token';
export default {
peer: PeerActions,
@@ -13,5 +17,9 @@ export default {
group: GroupActions,
rule: RuleActions,
route: RouteActions,
nameserverGroup: NameServerGroupActions
nameserverGroup: NameServerGroupActions,
event: EventActions,
dnsSettings: DNSSettingsActions,
account: AccountActions,
personalAccessToken: PersonalAccessTokenActions
};

View File

@@ -5,15 +5,25 @@ 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';
import { reducer as dnsSettings } from './dns-settings';
import { reducer as account } from './account';
import { reducer as personalAccessToken } from './personal-access-token';
export default combineReducers({
peer,
setupKey,
user,
group,
rule,
route,
nameserverGroup
peer,
setupKey,
user,
group,
rule,
policy,
route,
nameserverGroup,
event,
dnsSettings,
account,
personalAccessToken
});

View File

@@ -1,5 +1,5 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {Route} from './types';
import {Route, RouteToSave} from './types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
@@ -13,7 +13,7 @@ const actions = {
'SAVE_ROUTE_REQUEST',
'SAVE_ROUTE_SUCCESS',
'SAVE_ROUTE_FAILURE',
)<RequestPayload<Route>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
)<RequestPayload<RouteToSave>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
setSavedRoute: createAction('SET_CREATE_ROUTE')<CreateResponse<Route | null>>(),
resetSavedRoute: createAction('RESET_CREATE_ROUTE')<null>(),
@@ -28,6 +28,7 @@ const actions = {
setRoute: createAction('SET_ROUTE')<Route>(),
setSetupNewRouteVisible: createAction('SET_SETUP_NEW_ROUTE_VISIBLE')<boolean>(),
setSetupEditRouteVisible: createAction('SET_SETUP_EDIT_ROUTE_VISIBLE')<boolean>(),
setSetupNewRouteHA: createAction('SET_SETUP_NEW_ROUTE_HA')<boolean>()
};

View File

@@ -13,7 +13,8 @@ type StateType = Readonly<{
deleteRoute: DeleteResponse<string | null>;
savedRoute: CreateResponse<Route | null>;
setupNewRouteVisible: boolean;
setupNewRouteHA: boolean
setupNewRouteHA: boolean;
setupEditRouteVisible: boolean;
}>;
const initialState: StateType = {
@@ -27,17 +28,18 @@ 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,
};
const data = createReducer<Route[], ActionTypes>(initialState.data as Route[])
@@ -79,6 +81,13 @@ 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 setupNewRouteHA = createReducer<boolean, ActionTypes>(initialState.setupNewRouteHA)
.handleAction(actions.setSetupNewRouteHA, (store, action) => action.payload)
@@ -91,5 +100,6 @@ export default combineReducers({
deletedRoute,
savedRoute,
setupNewRouteVisible,
setupNewRouteHA
setupNewRouteHA,
setupEditRouteVisible,
});

View File

@@ -3,6 +3,9 @@ import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../servi
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";
export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>): Generator {
try {
@@ -40,6 +43,21 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
const routeToSave = action.payload.payload
let groupsToCreate = routeToSave.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 = [...routeToSave.groups, ...resGroups]
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
@@ -50,7 +68,8 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
metric: routeToSave.metric,
network: routeToSave.network,
network_id: routeToSave.network_id,
peer: routeToSave.peer
peer: routeToSave.peer,
groups: newGroups
} as Route
}
@@ -72,8 +91,19 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
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 }));
} catch (err) {
yield put(groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
yield put(actions.saveRoute.failure({
loading: false,
success: false,

View File

@@ -1,5 +1,6 @@
export interface Route {
id?: string
id?: string | null
description: string
enabled: boolean
peer: string
@@ -8,4 +9,10 @@ export interface Route {
network_type?: string
metric?: number
masquerade: boolean
groups: string[]
}
export interface RouteToSave extends Route
{
groupsToCreate: 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

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

View File

@@ -1,6 +1,6 @@
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
import {User, UserToSave} from './types';
import {ApiError, CreateResponse, RequestPayload} from '../../services/api-client/types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getUsers: createAsyncAction(
@@ -9,10 +9,38 @@ const actions = {
'GET_USERS_FAILURE',
)<RequestPayload<null>, User[], ApiError>(),
getServiceUsers: createAsyncAction(
'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',
)<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>(),
// 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>(),
// used to make the UserUpdate drawer visible in the UI.
setUpdateUserDrawerVisible: createAction('SET_UPDATE_USER_VISIBLE')<boolean>(),
// used to make the ViewUserPopup visible in the UI.
setInviteUserPopupVisible: createAction('SET_INVITE_USER_VISIBLE')<boolean>(),
// used to make the EditUserPopup visible in the UI.
setEditUserPopupVisible: createAction('SET_EDIT_USER_VISIBLE')<boolean>(),
// used to make the AddServiceUserPopup visible in the UI.
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>(),
saveUser: createAsyncAction(
'SAVE_USER_REQUEST',

View File

@@ -2,24 +2,44 @@ import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { User } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, CreateResponse} from "../../services/api-client/types";
import {ApiError, CreateResponse, DeleteResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: User[] | null;
serviceUsers: User[] | null;
regularUsers: User[] | null;
loading: boolean;
failed: ApiError | null;
user: User | null;
deletedUser: DeleteResponse<string | null>;
savedUser: CreateResponse<User | null>;
updateUserDrawerVisible: boolean
editUserPopupVisible: boolean
inviteUserPopupVisible: boolean
addServiceUserPopupVisible: boolean
usersTabOpen: string
}>;
const initialState: StateType = {
data: [],
serviceUsers: [],
regularUsers: [],
loading: false,
failed: null,
user: null,
deletedUser: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
// right-sided user update drawer
updateUserDrawerVisible: false,
editUserPopupVisible: false,
inviteUserPopupVisible: false,
addServiceUserPopupVisible: false,
usersTabOpen: 'Users',
savedUser: <CreateResponse<User | null>>{
loading: false,
success: false,
@@ -33,6 +53,14 @@ const data = createReducer<User[], ActionTypes>(initialState.data as User[])
.handleAction(actions.getUsers.success,(_, action) => action.payload)
.handleAction(actions.getUsers.failure, () => []);
const serviceUsers = createReducer<User[], ActionTypes>(initialState.serviceUsers as User[])
.handleAction(actions.getServiceUsers.success,(_, action) => action.payload)
.handleAction(actions.getServiceUsers.failure, () => []);
const regularUsers = createReducer<User[], ActionTypes>(initialState.regularUsers as User[])
.handleAction(actions.getRegularUsers.success,(_, action) => action.payload)
.handleAction(actions.getRegularUsers.failure, () => []);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getUsers.request, () => true)
.handleAction(actions.getUsers.success, () => false)
@@ -45,9 +73,29 @@ const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
const user = createReducer<User, ActionTypes>(initialState.user as User)
.handleAction(actions.setUser, (store, action) => action.payload);
const deletedUser = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedUser)
.handleAction(actions.deleteUser.request, () => initialState.deletedUser)
.handleAction(actions.deleteUser.success, (store, action) => action.payload)
.handleAction(actions.deleteUser.failure, (store, action) => action.payload)
.handleAction(actions.setDeletedUser, (store, action) => action.payload)
.handleAction(actions.resetDeletedUser, (store, action) => initialState.deletedUser);
const updateUserDrawerVisible = createReducer<boolean, ActionTypes>(initialState.updateUserDrawerVisible)
.handleAction(actions.setUpdateUserDrawerVisible, (store, action) => action.payload);
const inviteUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.inviteUserPopupVisible)
.handleAction(actions.setInviteUserPopupVisible, (store, action) => action.payload);
const editUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.editUserPopupVisible)
.handleAction(actions.setEditUserPopupVisible, (store, action) => action.payload);
const addServiceUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.addServiceUserPopupVisible)
.handleAction(actions.setAddServiceUserPopupVisible, (store, action) => action.payload);
const userTabOpen = createReducer<string, ActionTypes>(initialState.usersTabOpen)
.handleAction(actions.setUserTabOpen, (store, action) => action.payload);
const savedUser = createReducer<CreateResponse<User | null>, ActionTypes>(initialState.savedUser)
.handleAction(actions.saveUser.request, () => initialState.savedUser)
.handleAction(actions.saveUser.success, (store, action) => action.payload)
@@ -57,9 +105,16 @@ const savedUser = createReducer<CreateResponse<User | null>, ActionTypes>(initia
export default combineReducers({
data,
serviceUsers,
regularUsers,
loading,
failed,
user,
savedUser,
updateUserDrawerVisible
deletedUser,
updateUserDrawerVisible,
inviteUserPopupVisible,
editUserPopupVisible,
addServiceUserPopupVisible,
userTabOpen
});

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