Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bd94eff56 | ||
|
|
77f065b093 | ||
|
|
a4d55cfb90 | ||
|
|
cfd4c9075b | ||
|
|
962180030a | ||
|
|
485e1e8d79 | ||
|
|
b11007b29f | ||
|
|
bce75c1ca9 | ||
|
|
0c09992b38 | ||
|
|
86b12f30d2 | ||
|
|
d3e34d8448 | ||
|
|
76083168f6 | ||
|
|
a54b3687ae | ||
|
|
25f154dc83 | ||
|
|
f3c7d877f8 | ||
|
|
7cea7e7f54 | ||
|
|
aaa351635f | ||
|
|
379ff5486e | ||
|
|
8bcd9918e2 | ||
|
|
044ccd0ce6 | ||
|
|
ab09ca3697 | ||
|
|
1644ed5dce | ||
|
|
cea459792f | ||
|
|
a402680816 | ||
|
|
e57e5b726d | ||
|
|
2c4ada0ad8 | ||
|
|
8195587c85 | ||
|
|
adf6e1e71f | ||
|
|
b733a186ae | ||
|
|
5d901470c2 | ||
|
|
29ab28847d | ||
|
|
0361825e04 | ||
|
|
2fa33ec06a | ||
|
|
c677eeaae4 | ||
|
|
7fb4b0b145 | ||
|
|
57f60a2fbf | ||
|
|
ec949da416 | ||
|
|
43710b8ada | ||
|
|
247665a846 | ||
|
|
010657c594 | ||
|
|
2d911af97f | ||
|
|
afdfed0160 | ||
|
|
6b86da3716 | ||
|
|
425fac8e9c | ||
|
|
fa2413f937 | ||
|
|
feec057933 | ||
|
|
9127686df7 | ||
|
|
479911ded8 | ||
|
|
69dcd6fadd | ||
|
|
0b7b34b490 | ||
|
|
fff93a3820 | ||
|
|
da21784c73 | ||
|
|
2e03a39b3e | ||
|
|
8e626cdd96 | ||
|
|
472704ad59 | ||
|
|
94c7288016 | ||
|
|
957ff98cec | ||
|
|
80178f66c3 | ||
|
|
37324cbcfc | ||
|
|
9dd362a8a4 | ||
|
|
90605a2067 | ||
|
|
18cfddbbe7 | ||
|
|
17e671200e | ||
|
|
bb94342cc8 | ||
|
|
b86cf8b99f | ||
|
|
f472c06cbf | ||
|
|
c58834309b | ||
|
|
75fdd3e17f | ||
|
|
568c5eccda | ||
|
|
363f226a1c | ||
|
|
bf447b1ada | ||
|
|
90cb05bd2d | ||
|
|
a98d6d9ce1 | ||
|
|
f83e39d734 | ||
|
|
52c1909229 | ||
|
|
f0d893c689 | ||
|
|
c8339c4be1 | ||
|
|
954d697b5f | ||
|
|
ace2bb61ef | ||
|
|
f389862931 | ||
|
|
521df658ad | ||
|
|
4db17c119a | ||
|
|
230a4cb05e | ||
|
|
06316239de | ||
|
|
59eff85339 | ||
|
|
ffabdf8a1a | ||
|
|
0fe5aa13b1 | ||
|
|
7166eb6e2f | ||
|
|
c9f1955d6a | ||
|
|
c3236d05a1 | ||
|
|
25e8a52465 |
14
.github/workflows/build_and_push.yml
vendored
14
.github/workflows/build_and_push.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
build_n_push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: setup-node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
@@ -27,26 +27,26 @@ jobs:
|
||||
run: CI=false npm run build
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: wiretrustee/dashboard
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
-
|
||||
name: Docker build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
|
||||
37
README.md
37
README.md
@@ -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`
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "AUTH0_DOMAIN environment variable must be set"
|
||||
exit 1
|
||||
if [[ -z "${AUTH_AUTHORITY}" ]]; then
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "AUTH_AUTHORITY or AUTH0_DOMAIN environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${AUTH0_CLIENT_ID}" ]]; then
|
||||
echo "AUTH0_CLIENT_ID environment variable must be set"
|
||||
exit 1
|
||||
if [[ -z "${AUTH_CLIENT_ID}" ]]; then
|
||||
if [[ -z "${AUTH0_CLIENT_ID}" ]]; then
|
||||
echo "AUTH_CLIENT_ID or AUTH0_CLIENT_ID environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${AUTH0_AUDIENCE}" ]]; then
|
||||
echo "AUTH0_AUDIENCE environment variable must be set"
|
||||
exit 1
|
||||
if [[ -z "${AUTH_AUDIENCE}" ]]; then
|
||||
if [[ -z "${AUTH0_AUDIENCE}" ]]; then
|
||||
echo "AUTH_AUDIENCE or AUTH0_AUDIENCE environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${AUTH_SUPPORTED_SCOPES}" ]]; then
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "AUTH_SUPPORTED_SCOPES environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${USE_AUTH0}" ]]; then
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "USE_AUTH0 environment variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
|
||||
@@ -21,19 +41,23 @@ if [[ -z "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AUTH0_DOMAIN=${AUTH0_DOMAIN}
|
||||
AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
|
||||
AUTH0_AUDIENCE=${AUTH0_AUDIENCE}
|
||||
NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
|
||||
NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
export AUTH_AUTHORITY=${AUTH_AUTHORITY:-https://$AUTH0_DOMAIN}
|
||||
export AUTH_CLIENT_ID=${AUTH_CLIENT_ID:-$AUTH0_CLIENT_ID}
|
||||
export AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH0_AUDIENCE}
|
||||
export AUTH_REDIRECT_URI=${AUTH_REDIRECT_URI}
|
||||
export AUTH_SILENT_REDIRECT_URI=${AUTH_SILENT_REDIRECT_URI}
|
||||
export USE_AUTH0=${USE_AUTH0:-true}
|
||||
export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api offline_access email_verified}
|
||||
|
||||
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}
|
||||
|
||||
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="\$\$AUTH0_DOMAIN \$\$AUTH0_CLIENT_ID \$\$AUTH0_AUDIENCE \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI"
|
||||
|
||||
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
cp "$MAIN_JS" "$MAIN_JS".copy
|
||||
|
||||
7552
package-lock.json
generated
7552
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -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,19 +18,24 @@
|
||||
"@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",
|
||||
"copyfiles": "^2.4.1",
|
||||
"heroicons": "^1.0.6",
|
||||
"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",
|
||||
@@ -41,6 +46,7 @@
|
||||
"redux-saga": "^1.1.3",
|
||||
"styled-components": "^5.3.5",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"ts-md5": "^1.3.1",
|
||||
"typesafe-actions": "^5.1.0",
|
||||
"typescript": "^4.6.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Disallow: /
|
||||
|
||||
18
run-local-keycloak.sh
Executable file
18
run-local-keycloak.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
MGMT_PORT=$1
|
||||
|
||||
npm run build
|
||||
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
|
||||
|
||||
docker rm -f netbird-dashboard
|
||||
docker run -d --name netbird-dashboard \
|
||||
-p 3000:80 -p 443:443 \
|
||||
-e AUTH_AUDIENCE=netbird-client \
|
||||
-e AUTH_AUTHORITY=http://localhost:8080/realms/netbird \
|
||||
-e AUTH_CLIENT_ID=netbird-client \
|
||||
-e USE_AUTH0=false \
|
||||
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access' \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
netbird/dashboard-local:latest
|
||||
16
run-local-legacy.sh
Executable file
16
run-local-legacy.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
MGMT_PORT=$1
|
||||
|
||||
npm run build
|
||||
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
|
||||
|
||||
docker rm -f netbird-dashboard
|
||||
docker run -d --name netbird-dashboard \
|
||||
-p 3000:80 -p 443:443 \
|
||||
-e AUTH0_AUDIENCE=http://localhost:3000/ \
|
||||
-e AUTH0_DOMAIN=netbird-localdev.eu.auth0.com \
|
||||
-e AUTH0_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
netbird/dashboard-local:latest
|
||||
18
run-local.sh
Executable file
18
run-local.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
MGMT_PORT=$1
|
||||
|
||||
npm run build
|
||||
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
|
||||
|
||||
docker rm -f netbird-dashboard
|
||||
docker run -d --name netbird-dashboard \
|
||||
-p 3000:80 -p 443:443 \
|
||||
-e AUTH_AUDIENCE=http://localhost:3000/ \
|
||||
-e AUTH_AUTHORITY=https://netbird-localdev.eu.auth0.com \
|
||||
-e AUTH_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
|
||||
-e USE_AUTH0=true \
|
||||
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access email_verified' \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
|
||||
netbird/dashboard-local:latest
|
||||
157
src/App.tsx
157
src/App.tsx
@@ -1,22 +1,40 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {Provider} from "react-redux";
|
||||
import {Redirect, Route, Switch} from 'react-router-dom';
|
||||
import Navbar from './components/Navbar';
|
||||
import Peers from './views/Peers';
|
||||
import FooterComponent from './components/FooterComponent';
|
||||
import SetupKeys from "./views/SetupKeys";
|
||||
import AddPeer from "./views/AddPeer";
|
||||
import Users from './views/Users';
|
||||
import AccessControl from './views/AccessControl';
|
||||
import {apiClient, store} from "./store";
|
||||
import {hotjar} from 'react-hotjar';
|
||||
import {getConfig} from "./config";
|
||||
import Banner from "./components/Banner";
|
||||
import {store} from "./store";
|
||||
import { Col, Layout, Row} from 'antd';
|
||||
import {Col, ConfigProvider, Layout, Row} from "antd";
|
||||
import {Container} from "./components/Container";
|
||||
import {withOidcSecure} from '@axa-fr/react-oidc';
|
||||
import Navbar from "./components/Navbar";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
import {withOidcSecure} from "@axa-fr/react-oidc";
|
||||
import Peers from "./views/Peers";
|
||||
import Routes from "./views/Routes";
|
||||
import AddPeer from "./views/AddPeer";
|
||||
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 {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;
|
||||
|
||||
function App() {
|
||||
const run = useRef(false)
|
||||
const [show, setShow] = useState(false)
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently();
|
||||
const {hotjarTrackID} = getConfig();
|
||||
// @ts-ignore
|
||||
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
|
||||
hotjar.initialize(hotjarTrackID, 6);
|
||||
}
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -24,7 +42,6 @@ function App() {
|
||||
const hideMenu = () => {
|
||||
if (window.innerWidth > 768 && isOpen) {
|
||||
setIsOpen(false);
|
||||
console.log('i resized');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,48 +50,82 @@ function App() {
|
||||
return () => {
|
||||
window.removeEventListener('resize', hideMenu);
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!run.current) {
|
||||
run.current = true
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
|
||||
.then(() => {
|
||||
setShow(true)
|
||||
})
|
||||
.catch(e => {
|
||||
setShow(true)
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
|
||||
}, [getAccessTokenSilently])
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<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="/users" component={withOidcSecure(Users)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
</Provider>
|
||||
);
|
||||
<>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
colorPrimary: "#1890ff",
|
||||
fontFamily: "Arial"
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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="/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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -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}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import { actions as ruleActions } from '../store/rule';
|
||||
import {actions as ruleActions} from '../store/rule';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Row,
|
||||
Typography,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Button, Drawer, Form, Divider, Select, Tag, Radio, RadioChangeEvent
|
||||
Tag,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {ArrowRightOutlined, CheckOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
|
||||
import {Rule, RuleToSave} from "../store/rule/types";
|
||||
import { uniq } from "lodash"
|
||||
import {uniq} from "lodash"
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
|
||||
interface FormRule extends Rule {
|
||||
tagSourceGroups: string[]
|
||||
@@ -28,11 +34,11 @@ interface FormRule extends Rule {
|
||||
}
|
||||
|
||||
const AccessControlNew = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const rule = useSelector((state: RootState) => state.rule.rule)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const rule = useSelector((state: RootState) => state.rule.rule)
|
||||
const savedRule = useSelector((state: RootState) => state.rule.savedRule)
|
||||
|
||||
const [editName, setEditName] = useState(false)
|
||||
@@ -72,7 +78,7 @@ const AccessControlNew = () => {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
const createRuleToSave = ():RuleToSave => {
|
||||
const createRuleToSave = (): RuleToSave => {
|
||||
const sources = groups?.filter(g => formRule.tagSourceGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const destinations = groups?.filter(g => formRule.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s))
|
||||
@@ -96,14 +102,17 @@ const AccessControlNew = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const ruleToSave = createRuleToSave()
|
||||
dispatch(ruleActions.saveRule.request({getAccessTokenSilently:accessToken, payload: ruleToSave}))
|
||||
dispatch(ruleActions.saveRule.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: ruleToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRule = (status:boolean) => {
|
||||
const setVisibleNewRule = (status: boolean) => {
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(status));
|
||||
}
|
||||
|
||||
@@ -121,7 +130,7 @@ const AccessControlNew = () => {
|
||||
setVisibleNewRule(false)
|
||||
}
|
||||
|
||||
const onChange = (data:any) => {
|
||||
const onChange = (data: any) => {
|
||||
setFormRule({...formRule, ...data})
|
||||
}
|
||||
|
||||
@@ -139,7 +148,7 @@ const AccessControlNew = () => {
|
||||
})
|
||||
};
|
||||
|
||||
const handleChangeDisabled = ({ target: { value } }: RadioChangeEvent) => {
|
||||
const handleChangeDisabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setFormRule({
|
||||
...formRule,
|
||||
disabled: value
|
||||
@@ -147,7 +156,7 @@ const AccessControlNew = () => {
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const { label, value, closable, onClose } = props;
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -159,7 +168,7 @@ const AccessControlNew = () => {
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
@@ -169,12 +178,12 @@ const AccessControlNew = () => {
|
||||
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'} `
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
@@ -186,25 +195,27 @@ const AccessControlNew = () => {
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<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"/>
|
||||
<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 toggleEditName = (status:boolean) => {
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
const toggleEditDescription = (status:boolean) => {
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
}
|
||||
|
||||
@@ -217,10 +228,10 @@ const AccessControlNew = () => {
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter ate least one group"))
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
value.forEach(function(v: string) {
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
@@ -248,7 +259,8 @@ const AccessControlNew = () => {
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRule.loading} onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
|
||||
<Button type="primary" disabled={savedRule.loading}
|
||||
onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
@@ -258,37 +270,51 @@ const AccessControlNew = () => {
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && !editDescription && formRule.id &&
|
||||
{!editName && !editDescription && formRule.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">
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{ !editName && formRule.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
|
||||
{!editName && formRule.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{required: true, message: 'Please add a name for this access rule', whitespace: true}]}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a name for this access rule',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input placeholder="Add rule name..." ref={inputNameRef} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off"/>
|
||||
<Input placeholder="Add rule name..." ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{ !editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"} onClick={() => toggleEditDescription(true)}>{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}</div>
|
||||
{!editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"}
|
||||
onClick={() => toggleEditDescription(true)}>
|
||||
{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef} onPressEnter={() => toggleEditDescription(false)} onBlur={() => toggleEditDescription(false)} autoComplete="off"/>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
@@ -308,14 +334,7 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="disabled"
|
||||
label="Status"
|
||||
//valuePropName="checked"
|
||||
>
|
||||
{/*<Switch
|
||||
checkedChildren={<CheckOutlined />}
|
||||
unCheckedChildren={<CloseOutlined />}
|
||||
|
||||
onChange={handleChangeDisabled}
|
||||
/>*/}
|
||||
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
@@ -329,11 +348,10 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="tagSourceGroups"
|
||||
label="Source groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{display: 'flex'}}
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
style={{width: '100%'}}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeSource}
|
||||
@@ -351,11 +369,10 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="tagDestinationGroups"
|
||||
label="Destination groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{display: 'flex'}}
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select
|
||||
mode="tags" style={{ width: '100%' }}
|
||||
mode="tags" style={{width: '100%'}}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeDestination}
|
||||
@@ -376,10 +393,14 @@ const AccessControlNew = () => {
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
At the moment access rules are bi-directional by default, this means both source and destination can talk to each-other in both directions. However destination peers will not be able to communicate with each other, nor will the source peers.
|
||||
At the moment access rules are bi-directional by default, this means both
|
||||
source and destination can talk to each-other in both directions. However
|
||||
destination peers will not be able to communicate with each other, nor will
|
||||
the source peers.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
If you want to enable all peers of the same group to talk to each other - you can add that group both as a receiver and as a destination.
|
||||
If you want to enable all peers of the same group to talk to each other -
|
||||
you can add that group both as a receiver and as a destination.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -387,8 +408,7 @@ const AccessControlNew = () => {
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/docs/overview/acls" style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about access controls</Button>
|
||||
href="https://docs.netbird.io/docs/overview/acls">Learn more about access controls</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Button, Col, Row, Space, Typography} from "antd";
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import {Md5} from "ts-md5";
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const Banner = () => {
|
||||
const [show, setShow] = useState(true);
|
||||
const [show, setShow] = useState(false);
|
||||
const banner_md5_key = 'banner_md5'
|
||||
const banner_closed_key = 'banner_closed'
|
||||
|
||||
const dismiss = () => {
|
||||
setShow(false);
|
||||
localStorage.setItem(banner_closed_key,'true');
|
||||
};
|
||||
|
||||
const announcement = "New Release! Manage DNS with NetBird."
|
||||
|
||||
const announcement_md5 = Md5.hashStr(announcement)
|
||||
|
||||
const linkLearnMore = () => {
|
||||
return (
|
||||
<a
|
||||
href="https://netbird.io/blog/introducing-access-control"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
className="font-bold underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -22,12 +30,24 @@ const Banner = () => {
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
let store_banner_md5 = localStorage.getItem(banner_md5_key);
|
||||
let stored_banner_closed = localStorage.getItem(banner_closed_key);
|
||||
|
||||
if((!stored_banner_closed || stored_banner_closed !== 'true') ||
|
||||
(!store_banner_md5 || store_banner_md5 !== announcement_md5)) {
|
||||
//setShow(true);
|
||||
localStorage.setItem(banner_md5_key,announcement_md5);
|
||||
localStorage.setItem(banner_closed_key,'false');
|
||||
}
|
||||
},[])
|
||||
|
||||
return show ? (
|
||||
<div className="relative bg-indigo-600 white" color="white" style={{position: "relative", padding: "0.3rem"}} >
|
||||
<Row>
|
||||
<Col xs={24} sm={0} lg={0}>
|
||||
<Text className="ant-col-md-0" style={{color: "#ffffff"}}>
|
||||
Big news! Introducing NetBird Access Control.
|
||||
{announcement}
|
||||
</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={0} lg={0}>
|
||||
@@ -38,7 +58,7 @@ const Banner = () => {
|
||||
<Col xs={0} sm={24}>
|
||||
<Space align="center" style={{display: "flex", justifyContent: "center"}}>
|
||||
<Text style={{color: "#ffffff"}}>
|
||||
Big news! Introducing NetBird Access Control.
|
||||
{announcement}
|
||||
</Text>
|
||||
<span>
|
||||
{linkLearnMore()}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import {copyToClipboard} from "../utils/common";
|
||||
import {Button, message} from "antd";
|
||||
import {StepCommand} from "./addpeer/types";
|
||||
import React from "react";
|
||||
import {Button, message, Typography} from "antd";
|
||||
import React, {ReactNode} from "react";
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
type Props = {
|
||||
keyMessage: string;
|
||||
text: string;
|
||||
toCopy: string;
|
||||
body: ReactNode;
|
||||
messageText: string;
|
||||
styleNotification?: any;
|
||||
style?: any;
|
||||
className?:any;
|
||||
className?: any;
|
||||
};
|
||||
|
||||
const ButtonCopyMessage:React.FC<Props> = ({ keyMessage, text, messageText, styleNotification, style, className}) => {
|
||||
const ButtonCopyMessage: React.FC<Props> = ({
|
||||
keyMessage,
|
||||
toCopy,
|
||||
body,
|
||||
messageText,
|
||||
styleNotification,
|
||||
style,
|
||||
className
|
||||
}) => {
|
||||
const copyTextMessage = () => {
|
||||
copyToClipboard(text)
|
||||
message.success({ content: `${messageText}`, key: keyMessage, duration: 1, style: (styleNotification || {}) });
|
||||
copyToClipboard(toCopy)
|
||||
message.success({content: `${messageText}`, key: keyMessage, duration: 1, style: (styleNotification || {})});
|
||||
}
|
||||
return (
|
||||
<Button type="text" onClick={copyTextMessage} style={style || {}} className={className}>{text}</Button>
|
||||
<Button type="text" onClick={copyTextMessage} style={style || {}} className={className}>
|
||||
{body}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import React from "react";
|
||||
import loading from "../assets/bars.svg";
|
||||
import {Space} from "antd";
|
||||
import {OidcSecure} from "@axa-fr/react-oidc";
|
||||
|
||||
type Props = {
|
||||
type LoadingProps = {
|
||||
padding?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
const Loading:React.FC<Props> = ({padding, width, height}) => (
|
||||
<Space direction="vertical" align="center" style={{display: 'flex', padding: `${padding || `.25em`}`}}>
|
||||
<img src={loading} alt="Loading" style={{width: `${width || '25px'}`, height: `${height || '25px'}`}}/>
|
||||
const Loading: React.FC<LoadingProps> = ({padding, width, height}) => (
|
||||
<Space direction="vertical" align="center" style={{
|
||||
marginTop: `-${height ? (height / 2) + "px" : '-25px'}`,
|
||||
marginLeft: `-${width ? (width / 2) + "px" : '-25px'}`,
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
display: 'flex'
|
||||
}}>
|
||||
<img src={loading} alt="Loading"
|
||||
style={{width: `${width ? width + "px" : '25px'}`, height: `${height ? height + "px" : '25px'}`}}/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
// Wrapper of Loading to handle cases when it is shown within the authenticated layout and has to trigger authentication when token expires.
|
||||
export const SecureLoading = (props: LoadingProps) => (
|
||||
<OidcSecure>
|
||||
<Loading {...props} />
|
||||
</OidcSecure>
|
||||
);
|
||||
|
||||
@@ -1,35 +1,57 @@
|
||||
import {OidcUserStatus, useOidc, useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {Button, Result} from "antd";
|
||||
import {useOidc, useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {Anchor, Button, Col, Result, Row, Space} from "antd";
|
||||
import React from "react";
|
||||
import {getConfig} from "../config";
|
||||
import {ResultStatusType} from "antd/lib/result";
|
||||
|
||||
const {Link} = Anchor;
|
||||
|
||||
function LoginError() {
|
||||
const { logout } = useOidc();
|
||||
const {logout} = useOidc();
|
||||
const config = getConfig();
|
||||
const { oidcUserLoadingState } = useOidcUser();
|
||||
const {oidcUserLoadingState} = useOidcUser();
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
if (urlParams.get("error") === "access_denied") {
|
||||
|
||||
let title = urlParams.get("error_description")
|
||||
let status: ResultStatusType = "warning"
|
||||
// this comes from the auth0 rule that links accounts
|
||||
if (title === "account linked successfully") {
|
||||
status = "success"
|
||||
title = "Your account has been linked successfully. Please log in again to complete the setup."
|
||||
}
|
||||
|
||||
return <Result
|
||||
status="warning"
|
||||
title={urlParams.get("error_description")}
|
||||
status={status}
|
||||
title={title}
|
||||
extra={<>
|
||||
<a href={window.location.origin}>
|
||||
<Button type="primary">
|
||||
Try again
|
||||
</Button>
|
||||
</a>
|
||||
<Button type="primary" onClick={function () {
|
||||
logout("",{client_id:config.clientId})
|
||||
<Space style={{
|
||||
display: "flex-inline",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
Log out
|
||||
<h4>Already verified your email address?</h4>
|
||||
<a href={window.location.origin}>
|
||||
<Button type="primary">
|
||||
Continue
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<Button type="link" onClick={function () {
|
||||
logout("", {client_id: config.clientId})
|
||||
}}>
|
||||
Trouble logging in? Try again.
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
return <div>{"Login Error: User state: "+oidcUserLoadingState}</div>
|
||||
return <div>{"Login Error: User state: " + oidcUserLoadingState}</div>
|
||||
}
|
||||
|
||||
export default LoginError;
|
||||
662
src/components/NameServerGroupUpdate.tsx
Normal file
662
src/components/NameServerGroupUpdate.tsx
Normal file
@@ -0,0 +1,662 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as nsGroupActions} from '../store/nameservers';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
FormListFieldData,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
FlagFilled,
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleFilled,
|
||||
QuestionCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {NameServer, NameServerGroup, NameServerGroupToSave} from "../store/nameservers/types";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups"
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
interface formNSGroup extends NameServerGroup {
|
||||
}
|
||||
|
||||
const NameServerGroupUpdate = () => {
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const dispatch = useDispatch()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {Option} = Select;
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
|
||||
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
const savedNSGroup = useSelector((state: RootState) => state.nameserverGroup.savedNameServerGroup)
|
||||
const nsGroupData = useSelector((state: RootState) => state.nameserverGroup.data);
|
||||
|
||||
const [formNSGroup, setFormNSGroup] = useState({} as formNSGroup)
|
||||
const [form] = Form.useForm()
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [isPrimary, setIsPrimary] = useState(false)
|
||||
const [editDescription, setEditDescription] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const inputDescriptionRef = useRef<any>(null)
|
||||
const [selectCustom, setSelectCustom] = useState(false)
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
const optionsPrimary = [{label: 'Yes', value: true}, {label: 'No', value: false}]
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nsGroup) return
|
||||
|
||||
let newFormGroup = {
|
||||
...nsGroup,
|
||||
groups: getGroupNamesFromIDs(nsGroup.groups),
|
||||
} as formNSGroup
|
||||
setFormNSGroup(newFormGroup)
|
||||
form.setFieldsValue(newFormGroup)
|
||||
if (nsGroup.id) {
|
||||
setSelectCustom(true)
|
||||
}
|
||||
if (nsGroup.primary !== undefined) {
|
||||
setIsPrimary(nsGroup.primary)
|
||||
}
|
||||
}, [nsGroup])
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(nsGroupActions.setNameServerGroup(
|
||||
{
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
primary: false,
|
||||
domains: [],
|
||||
nameservers: [] as NameServer[],
|
||||
groups: [],
|
||||
enabled: false,
|
||||
} as NameServerGroup
|
||||
))
|
||||
setEditName(false)
|
||||
setSelectCustom(false)
|
||||
setIsPrimary(false)
|
||||
}
|
||||
|
||||
const onChange = (changedValues: any) => {
|
||||
if (changedValues.primary !== undefined) {
|
||||
setIsPrimary(changedValues.primary)
|
||||
}
|
||||
setFormNSGroup({...formNSGroup, ...changedValues})
|
||||
}
|
||||
|
||||
let googleChoice = 'Google DNS'
|
||||
let cloudflareChoice = 'Cloudflare DNS'
|
||||
let quad9Choice = 'Quad9 DNS'
|
||||
let customChoice = 'Add custom nameserver'
|
||||
|
||||
let defaultDNSOptions: NameServerGroup[] = [
|
||||
{
|
||||
name: googleChoice,
|
||||
description: 'Google DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "8.8.8.8",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "8.8.4.4",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: cloudflareChoice,
|
||||
description: 'Cloudflare DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "1.1.1.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "1.0.0.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: quad9Choice,
|
||||
description: 'Quad9 DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "9.9.9.9",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "149.112.112.112",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
console.log(`selected ${value}`);
|
||||
let nsGroupLocal = {} as NameServerGroup
|
||||
if (value === customChoice) {
|
||||
nsGroupLocal = nsGroup
|
||||
} else {
|
||||
defaultDNSOptions.forEach((nsg) => {
|
||||
if (value === nsg.name) {
|
||||
nsGroupLocal = nsg
|
||||
}
|
||||
})
|
||||
}
|
||||
let newFormGroup = {
|
||||
...nsGroupLocal,
|
||||
groups: getGroupNamesFromIDs(nsGroupLocal.groups),
|
||||
} as formNSGroup
|
||||
setFormNSGroup(newFormGroup)
|
||||
form.setFieldsValue(newFormGroup)
|
||||
setSelectCustom(true)
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
|
||||
dispatch(nsGroupActions.saveNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: nsGroupToSave
|
||||
}))
|
||||
|
||||
})
|
||||
.then(() => onCancel())
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createNSGroupToSave = (values: NameServerGroup): NameServerGroupToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(values.groups)
|
||||
return {
|
||||
id: formNSGroup.id || null,
|
||||
name: values.name ? values.name : formNSGroup.name,
|
||||
description: values.description ? values.description : formNSGroup.description,
|
||||
primary: values.primary,
|
||||
domains: values.primary ? [] : values.domains,
|
||||
nameservers: values.nameservers,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: newGroups,
|
||||
enabled: values.enabled,
|
||||
} as NameServerGroupToSave
|
||||
}
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status)
|
||||
}
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status)
|
||||
}
|
||||
|
||||
const domainRegex = /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/;
|
||||
|
||||
const domainValidator = (_: RuleObject, domain: string) => {
|
||||
if (domainRegex.test(domain)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
setIsPrimary(false)
|
||||
return Promise.reject(new Error("Please enter a valid domain, e.g. example.com or intra.example.com"))
|
||||
}
|
||||
|
||||
const nameValidator = (_: RuleObject, value: string) => {
|
||||
const found = nsGroupData.find(u => u.name == value && u.id !== formNSGroup.id)
|
||||
if (found) {
|
||||
return Promise.reject(new Error("Please enter a unique name for your nameserver configuration"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const ipValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value + "/32")) {
|
||||
return Promise.reject(new Error("Please enter a valid IP, e.g. 192.168.1.1 or 8.8.8.8"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const formListValidator = (_: RuleObject, names) => {
|
||||
if (names.length >= 3) {
|
||||
return Promise.reject(new Error("Exceeded maximum number of Nameservers. (Max is 2)"));
|
||||
}
|
||||
if (names.length < 1) {
|
||||
return Promise.reject(new Error("You should add at least 1 Nameserver"));
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const primaryValidator = (_: RuleObject, primary: boolean) => {
|
||||
if (!primary && form.getFieldValue("domains").length === 0) {
|
||||
return Promise.reject(new Error("You should select between Resolve all domains or add one Match domain"));
|
||||
}
|
||||
|
||||
if (primary && form.getFieldValue("domains").length > 0) {
|
||||
return Promise.reject(new Error("You should remove all match domains before setting this to yes"));
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const renderNSList = (fields: FormListFieldData[], {add, remove}, {errors}) => (
|
||||
<>
|
||||
<Row>Nameservers</Row>
|
||||
{!!fields.length && (
|
||||
|
||||
<Row align='middle'>
|
||||
<Col span={6} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Protocol</Typography.Text>
|
||||
</Col>
|
||||
<Col span={10} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Nameserver IP</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Port</Typography.Text>
|
||||
</Col>
|
||||
<Col span={2}/>
|
||||
</Row>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index}>
|
||||
<Col span={6} style={{textAlign: 'center'}}>
|
||||
<Form.Item style={{margin: '3px'}}
|
||||
name={[field.name, 'ns_type']}
|
||||
rules={[{required: true, message: 'Missing first protocol'}]}
|
||||
initialValue={"udp"}
|
||||
>
|
||||
<Select disabled style={{width: '100%'}}>
|
||||
<Option value="udp">UDP</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10} style={{margin: '1px'}}>
|
||||
<Form.Item style={{margin: '1px'}}
|
||||
name={[field.name, 'ip']}
|
||||
rules={[{validator: ipValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. X.X.X.X" style={{width: '100%'}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4} style={{textAlign: 'center'}}>
|
||||
<Form.Item style={{margin: '1px'}}
|
||||
name={[field.name, 'port']}
|
||||
rules={[{required: true, message: 'Missing port'}]}
|
||||
initialValue={53}
|
||||
>
|
||||
<InputNumber placeholder="Port" style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} style={{textAlign: 'center'}}>
|
||||
<MinusCircleOutlined onClick={() => remove(field.name)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined/>}>
|
||||
Add nameserver
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</>
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
const renderDomains = (fields: FormListFieldData[], {add, remove}, {errors}) => (
|
||||
<>
|
||||
<Row>
|
||||
<Space>
|
||||
<Col>
|
||||
Match domains
|
||||
</Col>
|
||||
<Col>
|
||||
<Tooltip title="Only queries to domains specified here will be resolved by these nameservers."
|
||||
className={"ant-form-item-tooltip"}>
|
||||
<QuestionCircleOutlined style={{color: "rgba(0, 0, 0, 0.45)", cursor: "help"}}/>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Space>
|
||||
</Row>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index}>
|
||||
<Col span={20} style={{margin: '1px'}}>
|
||||
<Form.Item hidden={isPrimary} style={{margin: '1px'}}
|
||||
{...field}
|
||||
rules={[{validator: domainValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. example.com" style={{width: '100%'}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} style={{textAlign: 'center'}}>
|
||||
<MinusCircleOutlined hidden={isPrimary} className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="dashed" disabled={isPrimary} onClick={() => add()} block icon={<PlusOutlined/>}>
|
||||
Add domain
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{nsGroup &&
|
||||
<Drawer
|
||||
headerStyle={{display: "none"}}
|
||||
forceRender={true}
|
||||
open={setupNewNameServerGroupVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedNSGroup.loading}>Cancel</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit} disabled={savedNSGroup.loading}
|
||||
>{`${formNSGroup.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{selectCustom ?
|
||||
(<Form layout="vertical" requiredMark={false} 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 && formNSGroup.id &&
|
||||
<button type="button" aria-label="Close"
|
||||
className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && formNSGroup.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formNSGroup.id ? formNSGroup.name : 'New nameserver group'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
tooltip="Add a nameserver group name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please add an identifier for this nameserver group',
|
||||
whitespace: true
|
||||
},
|
||||
{
|
||||
validator: nameValidator
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder="e.g. Public DNS" ref={inputNameRef}
|
||||
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)}>
|
||||
{formNSGroup.description && formNSGroup.description.trim() !== "" ? formNSGroup.description : 'Add description...'}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label="Status"
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24} flex="auto">
|
||||
<Form.List
|
||||
name="nameservers"
|
||||
rules={[{validator: formListValidator}]}
|
||||
>
|
||||
{renderNSList}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="primary"
|
||||
label="Resolve all domains"
|
||||
rules={[{validator: primaryValidator}]}
|
||||
dependencies={['domains']} // trigger primaryValidation if domains is updated
|
||||
tooltip="Defines if the nameservers are resolvers for all domains"
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsPrimary}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24} flex="auto">
|
||||
<Form.List
|
||||
name="domains"
|
||||
>
|
||||
{renderDomains}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Distribution groups"
|
||||
tooltip="Distribution groups define to which group of peers these settings will be distributed to"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the NS group"
|
||||
tagRender={tagRender}
|
||||
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>
|
||||
Nameservers let you define resolvers for your DNS queries.
|
||||
Because not all operating systems support match-only domain resolution,
|
||||
you should define at least one set of nameservers to resolve all domains
|
||||
per distribution group.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers">Learn more about nameservers</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>) :
|
||||
(
|
||||
<Space direction={"vertical"} style={{width: '100%'}}>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<span className="ant-form-item">Select a predefined nameserver</span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'center'}}>
|
||||
<Select
|
||||
style={{width: '100%'}}
|
||||
onChange={handleSelectChange}
|
||||
options={[
|
||||
{
|
||||
value: googleChoice,
|
||||
label: googleChoice,
|
||||
},
|
||||
{
|
||||
value: cloudflareChoice,
|
||||
label: cloudflareChoice,
|
||||
},
|
||||
{
|
||||
value: quad9Choice,
|
||||
label: quad9Choice,
|
||||
},
|
||||
{
|
||||
value: customChoice,
|
||||
label: customChoice,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<span className="ant-form-item"><Typography.Link
|
||||
onClick={() => handleSelectChange(customChoice)}>Or add custom</Typography.Link></span>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NameServerGroupUpdate
|
||||
@@ -1,48 +1,62 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Link, useLocation} from 'react-router-dom';
|
||||
import logo from "../assets/logo.png";
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd'
|
||||
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 {UserOutlined} from '@ant-design/icons';
|
||||
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "../config";
|
||||
const { Text } = Typography
|
||||
const { useBreakpoint } = Grid;
|
||||
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";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const {
|
||||
isAuthenticated,
|
||||
logout,
|
||||
} = useOidc();
|
||||
const { logout } = useOidc();
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { oidcUser } = useOidcUser();
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {idTokenPayload} = useOidcIdToken()
|
||||
const user = oidcUser;
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const [hideMenuUser, setHideMenuUser] = useState(false)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const [isRefreshingUserState, setIsRefreshingUserState] = useState(false)
|
||||
|
||||
const items = [
|
||||
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
|
||||
{label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys'},
|
||||
{label: (<Link to="/acls">Access Control</Link>), key: '/acls'},
|
||||
{label: (<Link to="/routes">Network Routes</Link>), key: '/routes'},
|
||||
{ label: (<Link to="/dns">DNS</Link>), key: '/dns' },
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'},
|
||||
{label: (<Link to="/activity">Activity</Link>), key: '/activity'},
|
||||
{label: (<Link to="/settings">Settings</Link>), key: '/settings'}
|
||||
] as ItemType[]
|
||||
|
||||
const userEmailKey = 'user-email'
|
||||
const userLogoutKey = 'user-logout'
|
||||
const userDividerKey = 'user-divider'
|
||||
const [menuItems, setMenuItems] = useState([
|
||||
{ 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="/users">Users</Link>), key: '/users' }
|
||||
] as ItemType[])
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
|
||||
const [menuItems, setMenuItems] = useState(items)
|
||||
const logoutWithRedirect = () =>
|
||||
logout("",{client_id:config.clientId});
|
||||
logout("/", {client_id: config.clientId});
|
||||
|
||||
useEffect(() => {
|
||||
const fs = menuItems.filter(m => m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
const fs = items.filter(m => showTab(m?.key?.toString(), currentUser) && m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
if (screens.xs === true) {
|
||||
setHideMenuUser(false)
|
||||
fs.push({ type: 'divider', key: userDividerKey })
|
||||
fs.push({type: 'divider', key: userDividerKey})
|
||||
fs.push({
|
||||
label: (
|
||||
<Link to="#">{user?.name}</Link>
|
||||
@@ -50,13 +64,51 @@ const Navbar = () => {
|
||||
icon: createAvatar("small"),
|
||||
key: userEmailKey
|
||||
})
|
||||
fs.push({ label: (<Button type="link" block onClick={logoutWithRedirect}>Logout</Button>), key: userLogoutKey })
|
||||
fs.push({
|
||||
label: (<Button type="link" block onClick={logoutWithRedirect}>Logout</Button>),
|
||||
key: userLogoutKey
|
||||
})
|
||||
setMenuItems([...fs])
|
||||
return
|
||||
}
|
||||
setMenuItems([...fs])
|
||||
setHideMenuUser(true)
|
||||
}, [screens])
|
||||
}, [screens, currentUser])
|
||||
|
||||
useEffect(() => {
|
||||
if (users.length === 0 && !isRefreshingUserState &&
|
||||
window.location.pathname !== '/peers' &&
|
||||
window.location.pathname !== '/users') {
|
||||
setIsRefreshingUserState(true)
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}))
|
||||
return
|
||||
}
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
return
|
||||
}
|
||||
let runUser = oidcUser
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload
|
||||
}
|
||||
setIsRefreshingUserState(false)
|
||||
if (runUser) {
|
||||
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
}
|
||||
}, [users, oidcUser])
|
||||
|
||||
const showTab = (key: string | undefined, user: User | undefined) => {
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (user.role?.toLowerCase() === "admin") {
|
||||
return true
|
||||
}
|
||||
return !adminOnlyTabs.find(t => t === key)
|
||||
}
|
||||
|
||||
const menuUser = (
|
||||
<Menu
|
||||
@@ -66,16 +118,16 @@ const Navbar = () => {
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: (<Link to="/logout" onClick={logoutWithRedirect}>Logout</Link>),
|
||||
label: (<Link to="/logout" onClick={logoutWithRedirect}>Logout</Link>),
|
||||
key: '1',
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const createAvatar = (size:AvatarSize) => {
|
||||
const createAvatar = (size: AvatarSize) => {
|
||||
return user?.picture ? (
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined />} />
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined/>}/>
|
||||
) : (
|
||||
<Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
|
||||
)
|
||||
@@ -95,7 +147,8 @@ const Navbar = () => {
|
||||
</Col>
|
||||
<Col flex="1 1 auto">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]} defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]}
|
||||
defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
</div>
|
||||
</Col>
|
||||
{hideMenuUser &&
|
||||
|
||||
@@ -2,23 +2,29 @@ import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as peerActions} from '../store/peer';
|
||||
import {Button, Col, Divider, Drawer, Form, Input, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {Button, Col, Collapse, Divider, Drawer, Form, Input, Radio, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
|
||||
import {Peer, PeerGroupsToSave} from "../store/peer/types";
|
||||
import {FormPeer, Peer, PeerGroupsToSave} from "../store/peer/types";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import {CloseOutlined, EditOutlined, FlagFilled} from "@ant-design/icons";
|
||||
import {CloseOutlined, EditOutlined} from "@ant-design/icons";
|
||||
import {RuleObject} from 'antd/lib/form';
|
||||
import { useOidcAccessToken } from '@axa-fr/react-oidc';
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {timeAgo} from "../utils/common";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const {Panel} = Collapse;
|
||||
const punycode = require('punycode/')
|
||||
|
||||
const PeerUpdate = () => {
|
||||
const { accessToken } = useOidcAccessToken()
|
||||
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const peer = useSelector((state: RootState) => state.peer.peer)
|
||||
const [formPeer, setFormPeer] = useState({} as Peer)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const peer: Peer = useSelector((state: RootState) => state.peer.peer)
|
||||
const [formPeer, setFormPeer] = useState({} as FormPeer)
|
||||
const updateGroupsVisible = useSelector((state: RootState) => state.peer.updateGroupsVisible)
|
||||
const savedGroups = useSelector((state: RootState) => state.peer.savedGroups)
|
||||
const updatedPeers = useSelector((state: RootState) => state.peer.updatedPeer)
|
||||
@@ -28,6 +34,7 @@ const PeerUpdate = () => {
|
||||
const [peerGroups, setPeerGroups] = useState([] as GroupPeer[])
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [estimatedName, setEstimatedName] = useState("")
|
||||
const [callingPeerAPI, setCallingPeerAPI] = useState(false)
|
||||
const [callingGroupAPI, setCallingGroupAPI] = useState(false)
|
||||
const [isSubmitRunning, setSubmitRunning] = useState(false)
|
||||
@@ -46,21 +53,21 @@ const PeerUpdate = () => {
|
||||
if (callingPeerAPI && updatedPeers.success) {
|
||||
setCallingPeerAPI(false)
|
||||
}
|
||||
},[updatedPeers])
|
||||
}, [updatedPeers])
|
||||
|
||||
// wait save groups to succeed
|
||||
useEffect(() => {
|
||||
if (callingGroupAPI && savedGroups.success) {
|
||||
setCallingGroupAPI(false)
|
||||
}
|
||||
},[savedGroups])
|
||||
}, [savedGroups])
|
||||
|
||||
// clean temp state and close
|
||||
useEffect(() => {
|
||||
if (isSubmitRunning && !callingGroupAPI && !callingPeerAPI) {
|
||||
onCancel()
|
||||
}
|
||||
},[callingGroupAPI,callingPeerAPI])
|
||||
}, [callingGroupAPI, callingPeerAPI])
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
@@ -74,18 +81,26 @@ const PeerUpdate = () => {
|
||||
const gs_name = gs?.map(g => g.name) as string[]
|
||||
setPeerGroups(gs)
|
||||
setSelectedTagGroups(gs_name)
|
||||
setFormPeer(peer)
|
||||
form.setFieldsValue({
|
||||
name: formPeer.name ? formPeer.name: peer.name,
|
||||
groups: gs_name
|
||||
})
|
||||
const fPeer = {
|
||||
...peer,
|
||||
name: formPeer.name ? formPeer.name : peer.name,
|
||||
groupsNames: gs_name,
|
||||
userEmail: users?.find(u => u.id === peer.user_id)?.email,
|
||||
last_seen: peer.connected ? "just now" : String(timeAgo(peer.last_seen)),
|
||||
ui_version: peer.ui_version ? peer.ui_version.replace("netbird-desktop-ui/", "") : ""
|
||||
} as FormPeer
|
||||
setFormPeer(fPeer)
|
||||
form.setFieldsValue(fPeer)
|
||||
}, [peer])
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
const toggleEditName = (status:boolean) => {
|
||||
useEffect(() => {
|
||||
}, [users])
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status)
|
||||
}
|
||||
|
||||
@@ -103,7 +118,7 @@ const PeerUpdate = () => {
|
||||
}, [selectedTagGroups])
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const { label, value, closable, onClose } = props;
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -120,7 +135,7 @@ const PeerUpdate = () => {
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={tagClosable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
@@ -130,12 +145,12 @@ const PeerUpdate = () => {
|
||||
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'} `
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
@@ -147,21 +162,23 @@ const PeerUpdate = () => {
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<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"/>
|
||||
<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 setUpdateGroupsVisible = (status:boolean) => {
|
||||
const setUpdateGroupsVisible = (status: boolean) => {
|
||||
dispatch(peerActions.setUpdateGroupsVisible(status));
|
||||
}
|
||||
|
||||
@@ -170,27 +187,31 @@ const PeerUpdate = () => {
|
||||
setUpdateGroupsVisible(false)
|
||||
setEditName(false)
|
||||
// setSaveBtnDisabled(true)
|
||||
setFormPeer({} as Peer)
|
||||
setFormPeer({} as FormPeer)
|
||||
setCallingPeerAPI(false)
|
||||
setCallingPeerAPI(false)
|
||||
setSubmitRunning(false)
|
||||
}
|
||||
|
||||
const noUpdateToGroups = ():Boolean => {
|
||||
const noUpdateToGroups = (): Boolean => {
|
||||
return !peerGroupsToSave.groupsToRemove.length && !peerGroupsToSave.groupsToAdd.length && !peerGroupsToSave.groupsNoId.length
|
||||
}
|
||||
|
||||
const noUpdateToName = ():Boolean => {
|
||||
const noUpdateToName = (): Boolean => {
|
||||
return !formPeer.name || formPeer.name === peer.name
|
||||
}
|
||||
|
||||
const onChange = (data:any) => {
|
||||
const noUpdateToLoginExpiration = (): Boolean => {
|
||||
return formPeer.login_expiration_enabled === peer.login_expiration_enabled
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormPeer({...formPeer, ...data})
|
||||
}
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function(v) {
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
@@ -198,11 +219,26 @@ const PeerUpdate = () => {
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const createPeerToSave = ():Peer => {
|
||||
const nameValidator = (_: RuleObject, value: string) => {
|
||||
let punyName = punycode.toASCII(value.toLowerCase())
|
||||
let domain = ""
|
||||
if (formPeer.dns_label) {
|
||||
let labelList = formPeer.dns_label.split(".")
|
||||
if (labelList.length > 1) {
|
||||
labelList.splice(0,1)
|
||||
domain = "." + labelList.join(".")
|
||||
}
|
||||
}
|
||||
setEstimatedName(punyName+domain)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const createPeerToSave = (): Peer => {
|
||||
return {
|
||||
id: formPeer.id,
|
||||
ssh_enabled: formPeer.ssh_enabled,
|
||||
name: formPeer.name
|
||||
name: formPeer.name,
|
||||
login_expiration_enabled: formPeer.login_expiration_enabled
|
||||
} as Peer
|
||||
}
|
||||
|
||||
@@ -210,14 +246,20 @@ const PeerUpdate = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
setSubmitRunning(true)
|
||||
if (!noUpdateToName()) {
|
||||
if (!noUpdateToName() || !noUpdateToLoginExpiration()) {
|
||||
const peerUpdate = createPeerToSave()
|
||||
setCallingPeerAPI(true)
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently:accessToken, payload: peerUpdate}))
|
||||
dispatch(peerActions.updatePeer.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: peerUpdate
|
||||
}))
|
||||
}
|
||||
if (peerGroupsToSave.groupsToRemove.length || peerGroupsToSave.groupsToAdd.length || peerGroupsToSave.groupsNoId.length) {
|
||||
if (!noUpdateToGroups()) {
|
||||
setCallingGroupAPI(true)
|
||||
dispatch(peerActions.saveGroups.request({getAccessTokenSilently:accessToken, payload: peerGroupsToSave}))
|
||||
dispatch(peerActions.saveGroups.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: peerGroupsToSave
|
||||
}))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
@@ -233,7 +275,7 @@ const PeerUpdate = () => {
|
||||
return Promise.reject(new Error("Please enter ate least one group"))
|
||||
}
|
||||
|
||||
value.forEach(function(v: string) {
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
@@ -259,25 +301,28 @@ const PeerUpdate = () => {
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
visible={true}
|
||||
open={true}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedGroups.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName())} onClick={handleFormSubmit}>Save</Button>
|
||||
<Button type="primary"
|
||||
disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName() && noUpdateToLoginExpiration())}
|
||||
onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Form layout="vertical" requiredMark={false} 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 && peer.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
<button type="button" aria-label="Close" autoFocus={true}
|
||||
className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
@@ -293,46 +338,135 @@ const PeerUpdate = () => {
|
||||
onClick={() => toggleEditName(true)}>{formPeer.name ? formPeer.name : peer.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Update Name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Input
|
||||
placeholder={peer.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
// onChange={(e) => handleChangeName(e.)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
<Row>
|
||||
<Space direction={"vertical"} size="small">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
style={{margin: '1px'}}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
},{validator:nameValidator}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={peer.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
max={59}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="New peer domain name preview"
|
||||
tooltip="If the domain name already exists, we add an increment number suffix to it"
|
||||
style={{margin: '1px'}}
|
||||
>
|
||||
<Paragraph>
|
||||
<Tag>
|
||||
{estimatedName}
|
||||
</Tag>
|
||||
</Paragraph>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="ip"
|
||||
label={<>
|
||||
<span style={{
|
||||
marginRight: "5px",
|
||||
}}>NetBird IP</span>
|
||||
<Tag
|
||||
color={formPeer.connected ? "green" : "red"}>{formPeer.connected ? "online" : "offline"}</Tag>
|
||||
</>}
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.ip}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="last_seen"
|
||||
label="Last seen"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.last_seen}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Select groups to associate with this peer"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{display: 'flex'}}
|
||||
name="dns_label"
|
||||
label="Domain name"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.userEmail}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
{formPeer.user_id && (
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="userEmail"
|
||||
label="User"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.userEmail}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="login_expiration_enabled"
|
||||
label="Login expiration"
|
||||
tooltip="When login expires, the user has to re-authenticate this peer. This only applies to peers added with the SSO login."
|
||||
>
|
||||
<Radio.Group
|
||||
options={[{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
disabled={!formPeer.user_id}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groupsNames"
|
||||
label="Select peer groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
style={{width: '100%'}}
|
||||
placeholder="Select groups..."
|
||||
tagRender={tagRender}
|
||||
// onDropdownVisibleChange={evaluateSubmit}
|
||||
// onSelect={evaluateSubmit}
|
||||
|
||||
dropdownRender={dropDownRender}
|
||||
onChange={handleChangeTags}>
|
||||
@@ -345,16 +479,62 @@ const PeerUpdate = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
Every peer is part of the group All, thus you can't remove it.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Collapse onChange={onChange} bordered={false} ghost={true}
|
||||
style={{color: "#5a5c5a"}}>
|
||||
<Panel key="0" header="System Info">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="hostname"
|
||||
label="Hostname"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.hostname}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="os"
|
||||
label="Operating system"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.os}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="version"
|
||||
label="Agent version"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.os}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{formPeer.ui_version && (
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="ui_version"
|
||||
label="UI version"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.ui_version}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>)}
|
||||
</Row>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
467
src/components/RouteUpdate.tsx
Normal file
467
src/components/RouteUpdate.tsx
Normal file
@@ -0,0 +1,467 @@
|
||||
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
|
||||
} from "antd";
|
||||
import {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 {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
interface FormRoute extends Route {
|
||||
}
|
||||
|
||||
const RouteUpdate = () => {
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const {Option} = Select;
|
||||
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 [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, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false)
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: 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
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator)
|
||||
let peerID: string
|
||||
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: getAccessTokenSilently,
|
||||
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: 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("")
|
||||
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={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}>{`${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={tagRender}
|
||||
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://netbird.io/docs/how-to-guides/network-routes">Learn more about network routes</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RouteUpdate
|
||||
@@ -1,139 +1,486 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import { actions as setupKeyActions } from '../store/setup-key';
|
||||
import {actions as setupKeyActions} from '../store/setup-key';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Row,
|
||||
Typography,
|
||||
DatePicker,
|
||||
DatePickerProps,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
Space,
|
||||
InputNumber,
|
||||
List, Modal,
|
||||
Radio,
|
||||
Button, Drawer, Form, List, Divider
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {SetupKey} from "../store/setup-key/types";
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
const { Text } = Typography;
|
||||
import {CloseOutlined, EditOutlined, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {FormSetupKey, SetupKey, SetupKeyToSave} from "../store/setup-key/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {checkExpiresIn, 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 {useGetAccessTokenSilently} from "../utils/token";
|
||||
import ExpiresInInput, {expiresInToSeconds, ExpiresInValue} from "../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
const ExpiresInDefault: ExpiresInValue = {number: 30, interval: "day"}
|
||||
|
||||
const customExpiresFormat: DatePickerProps['format'] = value => {
|
||||
return formatDate(value)
|
||||
}
|
||||
|
||||
const customLastUsedFormat: DatePickerProps['format'] = value => {
|
||||
if (value.year() == 1) {
|
||||
// 1st of Jan 0001
|
||||
return "never"
|
||||
}
|
||||
let ago = timeAgo(value.toString())
|
||||
if (!ago) {
|
||||
return "unused"
|
||||
}
|
||||
return ago
|
||||
}
|
||||
|
||||
const SetupKeyNew = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
|
||||
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey)
|
||||
const createdSetupKey = useSelector((state: RootState) => state.setupKey.createdSetupKey)
|
||||
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 [editName, setEditName] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
|
||||
const [formSetupKey, setFormSetupKey] = useState({} as SetupKey)
|
||||
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
setFormSetupKey({ ...setupKey } as SetupKey)
|
||||
form.setFieldsValue(setupKey)
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
useEffect(() => {
|
||||
if (!setupKey) return
|
||||
|
||||
let allGroups = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
allGroups.set(g.id!, g)
|
||||
})
|
||||
|
||||
let formKeyGroups: string[] = []
|
||||
|
||||
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
|
||||
setFormSetupKey(fSetupKey)
|
||||
form.setFieldsValue(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))
|
||||
|
||||
let 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 = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
dispatch(setupKeyActions.createSetupKey.request({getAccessTokenSilently:accessToken, payload: formSetupKey}))
|
||||
let setupKeyToSave = createSetupKeyToSave()
|
||||
dispatch(setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: setupKeyToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewSetupKey = (status:boolean) => {
|
||||
|
||||
const setVisibleNewSetupKey = (status: boolean) => {
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(status));
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (createdSetupKey.loading) return
|
||||
if (savedSetupKey.loading) return
|
||||
dispatch(setupKeyActions.setSetupKey({
|
||||
name: '',
|
||||
type: 'reusable'
|
||||
name: "",
|
||||
type: "one-off",
|
||||
key: "",
|
||||
last_used: "",
|
||||
expires: "",
|
||||
state: "valid",
|
||||
auto_groups: new Array(),
|
||||
usage_limit: 0,
|
||||
used_times: 0,
|
||||
expires_in: 0
|
||||
} as SetupKey))
|
||||
setFormSetupKey({} as FormSetupKey)
|
||||
setVisibleNewSetupKey(false)
|
||||
}
|
||||
|
||||
const onChange = (data:any) => {
|
||||
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 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 inputLabel = (text: any) => (
|
||||
<>
|
||||
<span>{text}</span>
|
||||
<Tag color="red">{formSetupKey.state}</Tag>
|
||||
</>
|
||||
)
|
||||
|
||||
const changesDetected = (): boolean => {
|
||||
return formSetupKey.name == null || formSetupKey.name !== setupKey.name || groupsChanged()
|
||||
|| formSetupKey.usage_limit !== setupKey.usage_limit
|
||||
}
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (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
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{setupKey &&
|
||||
<Drawer
|
||||
title="New setup key"
|
||||
forceRender={true}
|
||||
// width={512}
|
||||
visible={setupNewKeyVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={createdSetupKey.loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button disabled={createdSetupKey.loading} type="primary" onClick={handleFormSubmit}>Create</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="Name"
|
||||
label="Name"
|
||||
rules={[{required: true, message: 'Please enter key name'}]}
|
||||
>
|
||||
<Input placeholder="Please enter key name" autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="Type"
|
||||
label="Type"
|
||||
rules={[{required: true, message: 'Please enter key type'}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Radio.Group style={{display: 'flex'}}>
|
||||
<Space direction="vertical" style={{flex: 1}}>
|
||||
<List
|
||||
size="large"
|
||||
bordered
|
||||
{setupKey &&
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
open={setupNewKeyVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={savedSetupKey.loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedSetupKey.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}>{`${formSetupKey.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}
|
||||
initialValues={{
|
||||
expiresInFormatted: ExpiresInDefault,
|
||||
}}
|
||||
>
|
||||
<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 && setupKey.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 && setupKey.id && formSetupKey.name ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formSetupKey.name ? formSetupKey.name : setupKey.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={setupKey.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
{setupKey.id && formSetupKey.name &&
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label={<>
|
||||
<span style={{
|
||||
marginRight: "5px",
|
||||
}}>Key</span>
|
||||
<Tag
|
||||
color={formSetupKey.state === "valid" ? "green" : "red"}>{formSetupKey.state}</Tag>
|
||||
</>}
|
||||
>
|
||||
<List.Item>
|
||||
<Radio value={"reusable"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>Reusable</Text>
|
||||
<Text>This type of a setup key allows to setup multiple
|
||||
machine</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Radio value={"one-off"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>One-off</Text>
|
||||
<Text>This key can be used only once</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
</List>
|
||||
<Input
|
||||
disabled={true}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/docs/overview/setup-keys" style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about setup keys</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
{setupKey.id && formSetupKey.name &&
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="exp"
|
||||
label="Expires"
|
||||
tooltip="The expiration date of the key"
|
||||
>
|
||||
<DatePicker disabled={true}
|
||||
style={{width: "100%", color: "#5a5c5a"}}
|
||||
format={customExpiresFormat}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
{setupKey.id && formSetupKey.name &&
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="last"
|
||||
label="Last Used"
|
||||
tooltip="The last time the key was used"
|
||||
>
|
||||
<DatePicker disabled={true}
|
||||
style={{width: "100%", color: "#5a5c5a"}}
|
||||
format={customLastUsedFormat}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="Type"
|
||||
rules={[{required: true, message: 'Please enter key type'}]}
|
||||
>
|
||||
<Radio.Group style={{display: 'flex'}} disabled={setupKey.id}>
|
||||
<Space direction="vertical" style={{flex: 1}}>
|
||||
<List
|
||||
size="large"
|
||||
bordered
|
||||
>
|
||||
<List.Item>
|
||||
<Radio value={"one-off"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>One-off</Text>
|
||||
<Text>This key can be used only once</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Radio value={"reusable"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>Reusable</Text>
|
||||
<Text>This type of a setup key allows to enroll multiple
|
||||
machines</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{!setupKey.id &&
|
||||
<Col span={24}>
|
||||
<Form.Item name="expiresInFormatted" label="Expires In"
|
||||
rules={[{validator: checkExpiresIn}]}>
|
||||
<ExpiresInInput options={
|
||||
Array.of(
|
||||
{key: "day", title: "Days"},
|
||||
{key: "month", title: "Months"},
|
||||
{key: "year", title: "Years"})
|
||||
}/>
|
||||
</Form.Item>
|
||||
</Col>}
|
||||
<Col span={12}>
|
||||
<Form.Item name="usage_limit"
|
||||
label="Usage Limit"
|
||||
tooltip="Limit the number of times this key can be used. Use 0 for unlimited use."
|
||||
>
|
||||
<InputNumber min={0} defaultValue={0}
|
||||
disabled={setupKey.id || formSetupKey.type !== "reusable"}
|
||||
style={{width: "100%"}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="used_times"
|
||||
label="Used Times"
|
||||
>
|
||||
<InputNumber min={0} defaultValue={0} disabled={true}
|
||||
style={{width: "100%"}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="autoGroupNames"
|
||||
label="Auto-assigned groups"
|
||||
tooltip="Every peer enrolled with this key will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the key"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
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 span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/overview/setup-keys">Learn more about setup keys</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
414
src/components/UserUpdate.tsx
Normal file
414
src/components/UserUpdate.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
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} = 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,
|
||||
is_current: user.is_current,
|
||||
} 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") || formUser.is_current}>
|
||||
<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
|
||||
100
src/components/addpeer/AddPeerPopup.tsx
Normal file
100
src/components/addpeer/AddPeerPopup.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
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";
|
||||
|
||||
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}/>Ubuntu</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: <></>,
|
||||
},*/
|
||||
{
|
||||
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://netbird.io/docs/getting-started/installation"}>installation
|
||||
guide</Link>}
|
||||
</Paragraph>
|
||||
</>
|
||||
}
|
||||
|
||||
export default AddPeerPopup
|
||||
63
src/components/addpeer/DockerTab.tsx
Normal file
63
src/components/addpeer/DockerTab.tsx
Normal 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://netbird.io/docs/getting-started/installation#running-netbird-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
|
||||
@@ -1,17 +1,49 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import {Button, Typography} from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import {StepCommand} from "./types"
|
||||
import {formatNetBirdUP} from "./common"
|
||||
import {Collapse} from "antd";
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
export const LinuxTab = () => {
|
||||
|
||||
const [quickSteps, setQuickSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and run installer:',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64"
|
||||
target="_blank">Download NetBird</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Click on "Connect" from the NetBird icon in your system tray',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}
|
||||
])
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install Brew (package manager)',
|
||||
title: 'Download and install Homebrew',
|
||||
commands: (
|
||||
<Button type="primary" href="https://brew.sh/" target="_blank">Download Brew</Button>
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://brew.sh/" target="_blank">Download
|
||||
Brew</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
@@ -29,26 +61,49 @@ export const LinuxTab = () => {
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
title: 'Start NetBird daemon:',
|
||||
commands: [
|
||||
`sudo netbird up`
|
||||
`sudo netbird service install`,
|
||||
`sudo netbird service start`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Get your IP address:',
|
||||
commands: [
|
||||
`sudo ifconfig utun100`
|
||||
].join('\n'),
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps}/>
|
||||
/*<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on macOS
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={quickSteps}/>
|
||||
</div>
|
||||
<Collapse style={{marginLeft: -15}} bordered={false}>
|
||||
<Panel header={<Text style={{fontWeight: "bold"}}>
|
||||
Or install manually with Homebrew
|
||||
</Text>} key="1">
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>*/
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on macOS with Homebrew
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import React, {useState} from 'react';
|
||||
import {StepCommand} from "./types"
|
||||
import {formatNetBirdUP} from "./common"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import { getConfig } from "../../config";
|
||||
import Paragraph from 'antd/lib/skeleton/Paragraph';
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
import {Typography} from "antd";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
|
||||
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:',
|
||||
title: 'Add repository',
|
||||
commands: [
|
||||
`sudo apt install ca-certificates curl gnupg -y`,
|
||||
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -`,
|
||||
`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
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
title: 'Install NetBird',
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`# for CLI only`,
|
||||
@@ -43,39 +32,39 @@ export const UbuntuTab = () => {
|
||||
`sudo apt-get install netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
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,
|
||||
showCopyButton: false
|
||||
} 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} />
|
||||
<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://netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
</div>*/}
|
||||
{/*<Text style={{fontWeight: "bold"}}>*/}
|
||||
{/* Or install manually*/}
|
||||
{/*</Text>*/}
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on Ubuntu
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
25
src/components/addpeer/common.ts
Normal file
25
src/components/addpeer/common.ts
Normal 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')
|
||||
}
|
||||
@@ -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
|
||||
|
||||
1
src/components/icons/docker_icon.svg
Normal file
1
src/components/icons/docker_icon.svg
Normal 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 |
1
src/components/icons/terminal_icon.svg
Normal file
1
src/components/icons/terminal_icon.svg
Normal 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 |
@@ -1,8 +1,13 @@
|
||||
{
|
||||
"domain": "$AUTH0_DOMAIN",
|
||||
"clientId": "$AUTH0_CLIENT_ID",
|
||||
"audience": "$AUTH0_AUDIENCE",
|
||||
"auth0Auth": "$USE_AUTH0",
|
||||
"authAuthority": "$AUTH_AUTHORITY",
|
||||
"authClientId": "$AUTH_CLIENT_ID",
|
||||
"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"
|
||||
}
|
||||
@@ -7,24 +7,30 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
configJson = require("./config.json");
|
||||
}
|
||||
|
||||
const defaultRedirectURI = '/#callback';
|
||||
const defaultSilentRedirectURI = '/#silent-callback'
|
||||
|
||||
export function getConfig() {
|
||||
// Configure the audience here. By default, it will take whatever is in the config
|
||||
// (specified by the `audience` key) unless it's the default value of "YOUR_API_IDENTIFIER" (which
|
||||
// is what you get sometimes by using the Auth0 sample download tool from the quickstart page, if you
|
||||
// don't have an API).
|
||||
// If this resolves to `null`, the API page changes to show some helpful info about what to do
|
||||
// with the audience.
|
||||
const audience =
|
||||
configJson.audience && configJson.audience !== "YOUR_API_IDENTIFIER"
|
||||
? configJson.audience
|
||||
: null;
|
||||
let redirectURI = defaultRedirectURI
|
||||
if (configJson.redirectURI) {
|
||||
redirectURI = configJson.redirectURI
|
||||
}
|
||||
|
||||
let silentRedirectURI = defaultSilentRedirectURI
|
||||
if (configJson.silentRedirectURI) {
|
||||
silentRedirectURI = configJson.silentRedirectURI
|
||||
}
|
||||
|
||||
return {
|
||||
domain: configJson.domain,
|
||||
clientId: configJson.clientId,
|
||||
auth0Auth: configJson.auth0Auth == "true", //due to substitution we can't use boolean in the config
|
||||
authority: configJson.authAuthority,
|
||||
clientId: configJson.authClientId,
|
||||
scopesSupported: configJson.authScopesSupported,
|
||||
apiOrigin: configJson.apiOrigin,
|
||||
grpcApiOrigin: configJson.grpcApiOrigin,
|
||||
latestVersion: configJson.latestVersion,
|
||||
...(audience ? { audience } : null),
|
||||
audience: configJson.authAudience,
|
||||
hotjarTrackID: configJson.hotjarTrackID,
|
||||
redirectURI: redirectURI,
|
||||
silentRedirectURI: silentRedirectURI,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
@import '~antd/dist/antd.css';
|
||||
|
||||
/*@tailwind base;*/
|
||||
/*@tailwind components;*/
|
||||
/*@tailwind utilities;*/
|
||||
@import 'antd/dist/reset.css';
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
@@ -92,20 +88,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 +117,26 @@ 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;
|
||||
}
|
||||
@@ -4,59 +4,61 @@ import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import history from "./utils/history";
|
||||
import { getConfig } from "./config";
|
||||
import {OidcProvider, useOidc} from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "./config";
|
||||
import {OidcProvider} from '@axa-fr/react-oidc';
|
||||
import {BrowserRouter} from "react-router-dom";
|
||||
import Loading from "./components/Loading";
|
||||
import LoginError from "./components/LoginError";
|
||||
import {AuthorityConfiguration} from "@axa-fr/react-oidc/dist/vanilla/oidc";
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
const authority = 'https://' + config.domain
|
||||
// Unfortunately Auth0 https://<DOMAIN>/.well-known/openid-configuration doesn't contain end_session_endpoint that
|
||||
// is required for doing logout. Therefore, we need to hardcode the config for auth
|
||||
const auth0AuthorityConfig: AuthorityConfiguration = {
|
||||
authorization_endpoint: new URL("authorize", config.authority).href,
|
||||
token_endpoint: new URL("oauth/token", config.authority).href,
|
||||
revocation_endpoint: new URL("oauth/revoke", config.authority).href,
|
||||
end_session_endpoint: new URL("v2/logout", config.authority).href,
|
||||
userinfo_endpoint: new URL("userinfo", config.authority).href,
|
||||
} as AuthorityConfiguration
|
||||
|
||||
const providerConfig = {
|
||||
authority: authority,
|
||||
authority: config.authority,
|
||||
client_id: config.clientId,
|
||||
redirect_uri: window.location.origin+'#callback',
|
||||
redirect_uri: window.location.origin + config.redirectURI,
|
||||
refresh_time_before_tokens_expiration_in_second: 30,
|
||||
silent_redirect_uri: window.location.origin + '#silent-callback',
|
||||
scope: 'openid profile email api offline_access email_verified',
|
||||
silent_redirect_uri: window.location.origin + config.silentRedirectURI,
|
||||
scope: config.scopesSupported,
|
||||
// disabling service worker
|
||||
// service_worker_relative_url:'/OidcServiceWorker.js',
|
||||
service_worker_only: false,
|
||||
authority_configuration: {
|
||||
authorization_endpoint: authority + "/authorize",
|
||||
token_endpoint: authority + "/oauth/token",
|
||||
revocation_endpoint: authority + "/oauth/revoke",
|
||||
end_session_endpoint: authority + "/v2/logout",
|
||||
userinfo_endpoint: authority + "/userinfo"
|
||||
},
|
||||
...(config.audience ? {extras:{ audience: config.audience}} : null)
|
||||
authority_configuration: config.auth0Auth ? auth0AuthorityConfig : undefined,
|
||||
...(config.audience ? {extras: {audience: config.audience}} : null)
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
const loadingComponent = () => <Loading padding="3em" width="50px" height="50px"/>
|
||||
const loadingComponent = () => <Loading padding="3em" width={50} height={50}/>
|
||||
|
||||
root.render(
|
||||
|
||||
<OidcProvider
|
||||
configuration={providerConfig}
|
||||
callbackSuccessComponent={loadingComponent}
|
||||
authenticatingErrorComponent={LoginError}
|
||||
authenticatingComponent={loadingComponent}
|
||||
sessionLostComponent={loadingComponent}
|
||||
loadingComponent={loadingComponent}
|
||||
onSessionLost={()=>{
|
||||
history.push("/peers")
|
||||
}}
|
||||
>
|
||||
<BrowserRouter>
|
||||
<App/>
|
||||
</BrowserRouter>
|
||||
</OidcProvider>
|
||||
|
||||
<OidcProvider
|
||||
configuration={providerConfig}
|
||||
callbackSuccessComponent={loadingComponent}
|
||||
authenticatingErrorComponent={LoginError}
|
||||
authenticatingComponent={loadingComponent}
|
||||
sessionLostComponent={loadingComponent}
|
||||
loadingComponent={loadingComponent}
|
||||
onSessionLost={() => {
|
||||
history.push("/peers")
|
||||
}}
|
||||
>
|
||||
<BrowserRouter>
|
||||
<App/>
|
||||
</BrowserRouter>
|
||||
</OidcProvider>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import axios, {AxiosError} from 'axios';
|
||||
import axios from 'axios';
|
||||
|
||||
import {ApiRequestParams, ApiResponse, ApiError} from './types';
|
||||
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}`;
|
||||
@@ -16,10 +10,10 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
|
||||
const extraHeaders = params.extraHeaders || {};
|
||||
const headers = await headersFactory((params.data as any).getAccessTokenSilently);
|
||||
|
||||
const builtHeader: RequestHeader = { ...headers, ...extraHeaders };
|
||||
const builtHeader: RequestHeader = {...headers, ...extraHeaders};
|
||||
|
||||
let response;
|
||||
let error:ApiError = {
|
||||
let error: ApiError = {
|
||||
code: '-1',
|
||||
message: '',
|
||||
data: null,
|
||||
@@ -27,7 +21,7 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
|
||||
};
|
||||
|
||||
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});
|
||||
} catch (err: any) {
|
||||
error = <ApiError>{
|
||||
code: err ? err.code : '-1',
|
||||
@@ -39,12 +33,15 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
|
||||
let old = error.message
|
||||
error.message = old + ". Please refresh the page if the issue continues."
|
||||
error.code = 'ERR_UNAUTHORIZED'
|
||||
} else if (error.statusCode === 403) {
|
||||
error.code = 'ERR_FORBIDDEN'
|
||||
console.log(error)
|
||||
}
|
||||
console.log(error)
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { statusCode: response ? response.status : error.statusCode, body: response ? response.data : error };
|
||||
return {statusCode: response ? response.status : error.statusCode, body: response ? response.data : error};
|
||||
}
|
||||
|
||||
export { apiRequest };
|
||||
export {apiRequest};
|
||||
|
||||
@@ -9,11 +9,7 @@ const headersFactory = async (getAccessTokenSilently:any): Promise<RequestHeader
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
//const token = await getLocalItem<string>(StorageKey.token);
|
||||
//const token = ''
|
||||
// const token = await getAccessTokenSilently()
|
||||
const token = getAccessTokenSilently
|
||||
|
||||
const token = await getAccessTokenSilently() as string
|
||||
if (token) {
|
||||
headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
23
src/store/account/actions.ts
Normal file
23
src/store/account/actions.ts
Normal 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;
|
||||
7
src/store/account/index.ts
Normal file
7
src/store/account/index.ts
Normal 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 };
|
||||
54
src/store/account/reducer.ts
Normal file
54
src/store/account/reducer.ts
Normal 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
|
||||
});
|
||||
64
src/store/account/sagas.ts
Normal file
64
src/store/account/sagas.ts
Normal 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),
|
||||
]);
|
||||
}
|
||||
|
||||
19
src/store/account/service.ts
Normal file
19
src/store/account/service.ts
Normal 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
|
||||
);
|
||||
}
|
||||
};
|
||||
11
src/store/account/types.ts
Normal file
11
src/store/account/types.ts
Normal 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
|
||||
}
|
||||
26
src/store/dns-settings/actions.ts
Normal file
26
src/store/dns-settings/actions.ts
Normal 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;
|
||||
7
src/store/dns-settings/index.ts
Normal file
7
src/store/dns-settings/index.ts
Normal 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 };
|
||||
79
src/store/dns-settings/reducer.ts
Normal file
79
src/store/dns-settings/reducer.ts
Normal 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
|
||||
});
|
||||
95
src/store/dns-settings/sagas.ts
Normal file
95
src/store/dns-settings/sagas.ts
Normal 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),
|
||||
]);
|
||||
}
|
||||
|
||||
18
src/store/dns-settings/service.ts
Normal file
18
src/store/dns-settings/service.ts
Normal 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
|
||||
);
|
||||
},
|
||||
};
|
||||
8
src/store/dns-settings/types.ts
Normal file
8
src/store/dns-settings/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface DNSSettings {
|
||||
disabled_management_groups: string[]
|
||||
}
|
||||
|
||||
export interface DNSSettingsToSave extends DNSSettings
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
14
src/store/event/actions.ts
Normal file
14
src/store/event/actions.ts
Normal 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
7
src/store/event/index.ts
Normal 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 };
|
||||
38
src/store/event/reducer.ts
Normal file
38
src/store/event/reducer.ts
Normal 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
23
src/store/event/sagas.ts
Normal 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),
|
||||
]);
|
||||
}
|
||||
|
||||
12
src/store/event/service.ts
Normal file
12
src/store/event/service.ts
Normal 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
9
src/store/event/types.ts
Normal 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 }
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import { sagas as setupKeySagas } from './setup-key';
|
||||
import { sagas as userSagas } from './user';
|
||||
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 rootReducer from './root-reducer';
|
||||
import { apiClient } from '../services/api-client';
|
||||
@@ -23,5 +28,10 @@ sagaMiddleware.run(setupKeySagas);
|
||||
sagaMiddleware.run(userSagas);
|
||||
sagaMiddleware.run(ruleSagas);
|
||||
sagaMiddleware.run(groupSagas);
|
||||
sagaMiddleware.run(routeSagas);
|
||||
sagaMiddleware.run(nameserverGroupSagas);
|
||||
sagaMiddleware.run(eventSagas);
|
||||
sagaMiddleware.run(dnsSettingsSagas);
|
||||
sagaMiddleware.run(accountSagas);
|
||||
|
||||
export { apiClient, rootReducer, store };
|
||||
35
src/store/nameservers/actions.ts
Normal file
35
src/store/nameservers/actions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {NameServerGroup, NameServerGroupToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getNameServerGroups: createAsyncAction(
|
||||
'GET_NameServerGroup_REQUEST',
|
||||
'GET_NameServerGroup_SUCCESS',
|
||||
'GET_NameServerGroup_FAILURE',
|
||||
)<RequestPayload<null>, NameServerGroup[], ApiError>(),
|
||||
|
||||
saveNameServerGroup: createAsyncAction(
|
||||
'SAVE_NameServerGroup_REQUEST',
|
||||
'SAVE_NameServerGroup_SUCCESS',
|
||||
'SAVE_NameServerGroup_FAILURE',
|
||||
)<RequestPayload<NameServerGroupToSave>, CreateResponse<NameServerGroup | null>, CreateResponse<NameServerGroup | null>>(),
|
||||
setSavedNameServerGroup: createAction('SET_CREATE_NameServerGroup')<CreateResponse<NameServerGroup | null>>(),
|
||||
resetSavedNameServerGroup: createAction('RESET_CREATE_NameServerGroup')<null>(),
|
||||
|
||||
deleteNameServerGroup: createAsyncAction(
|
||||
'DELETE_NameServerGroup_REQUEST',
|
||||
'DELETE_NameServerGroup_SUCCESS',
|
||||
'DELETE_NameServerGroup_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedNameServerGroup: createAction('SET_DELETED_NameServerGroup')<DeleteResponse<string | null>>(),
|
||||
resetDeletedNameServerGroup: createAction('RESET_DELETED_NameServerGroup')<null>(),
|
||||
removeNameServerGroup: createAction('REMOVE_NameServerGroup')<string>(),
|
||||
|
||||
setNameServerGroup: createAction('SET_NameServerGroup')<NameServerGroup>(),
|
||||
setSetupNewNameServerGroupVisible: createAction('SET_SETUP_NEW_NameServerGroup_VISIBLE')<boolean>(),
|
||||
setSetupNewNameServerGroupHA: createAction('SET_SETUP_NEW_NameServerGroup_HA')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/nameservers/index.ts
Normal file
7
src/store/nameservers/index.ts
Normal 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 };
|
||||
95
src/store/nameservers/reducer.ts
Normal file
95
src/store/nameservers/reducer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { NameServerGroup } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, DeleteResponse, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: NameServerGroup[] | null;
|
||||
nameserverGroup: NameServerGroup | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deleteNameServerGroup: DeleteResponse<string | null>;
|
||||
savedNameServerGroup: CreateResponse<NameServerGroup | null>;
|
||||
setupNewNameServerGroupVisible: boolean;
|
||||
setupNewNameServerGroupHA: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
nameserverGroup: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deleteNameServerGroup: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
savedNameServerGroup: <CreateResponse<NameServerGroup | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewNameServerGroupVisible: false,
|
||||
setupNewNameServerGroupHA: false
|
||||
};
|
||||
|
||||
const data = createReducer<NameServerGroup[], ActionTypes>(initialState.data as NameServerGroup[])
|
||||
.handleAction(actions.getNameServerGroups.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => []);
|
||||
|
||||
const nameserverGroup = createReducer<NameServerGroup, ActionTypes>(initialState.nameserverGroup as NameServerGroup)
|
||||
.handleAction(actions.setNameServerGroup, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getNameServerGroups.request, () => true)
|
||||
.handleAction(actions.getNameServerGroups.success, () => false)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getNameServerGroups.request, () => null)
|
||||
.handleAction(actions.getNameServerGroups.success, () => null)
|
||||
.handleAction(actions.getNameServerGroups.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getNameServerGroups.request, () => true)
|
||||
.handleAction(actions.getNameServerGroups.success, () => false)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => false);
|
||||
|
||||
const deletedNameServerGroup = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deleteNameServerGroup)
|
||||
.handleAction(actions.deleteNameServerGroup.request, () => initialState.deleteNameServerGroup)
|
||||
.handleAction(actions.deleteNameServerGroup.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deleteNameServerGroup.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedNameServerGroup, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedNameServerGroup, () => initialState.deleteNameServerGroup)
|
||||
|
||||
const savedNameServerGroup = createReducer<CreateResponse<NameServerGroup | null>, ActionTypes>(initialState.savedNameServerGroup)
|
||||
.handleAction(actions.saveNameServerGroup.request, () => initialState.savedNameServerGroup)
|
||||
.handleAction(actions.saveNameServerGroup.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveNameServerGroup.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedNameServerGroup, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedNameServerGroup, () => initialState.savedNameServerGroup)
|
||||
|
||||
const setupNewNameServerGroupVisible = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupVisible)
|
||||
.handleAction(actions.setSetupNewNameServerGroupVisible, (store, action) => action.payload)
|
||||
|
||||
const setupNewNameServerGroupHA = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupHA)
|
||||
.handleAction(actions.setSetupNewNameServerGroupHA, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
nameserverGroup,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedNameServerGroup,
|
||||
savedNameServerGroup,
|
||||
setupNewNameServerGroupVisible,
|
||||
setupNewNameServerGroupHA
|
||||
});
|
||||
160
src/store/nameservers/sagas.ts
Normal file
160
src/store/nameservers/sagas.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {NameServerGroup} 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* getNameServerGroups(action: ReturnType<typeof actions.getNameServerGroups.request>): Generator {
|
||||
try {
|
||||
|
||||
yield put(actions.setDeletedNameServerGroup({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const effect = yield call(service.getNameServerGroups, action.payload);
|
||||
const response = effect as ApiResponse<NameServerGroup[]>;
|
||||
|
||||
yield put(actions.getNameServerGroups.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getNameServerGroups.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedNameServerGroup(action: ReturnType<typeof actions.setSavedNameServerGroup>): Generator {
|
||||
yield put(actions.setSavedNameServerGroup(action.payload))
|
||||
}
|
||||
|
||||
export function* saveNameServerGroup(action: ReturnType<typeof actions.saveNameServerGroup.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedNameServerGroup({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<NameServerGroup | null>))
|
||||
|
||||
const nameserverGroupToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = nameserverGroupToSave.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 = [...nameserverGroupToSave.groups, ...resGroups]
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
id: nameserverGroupToSave.id,
|
||||
name: nameserverGroupToSave.name,
|
||||
description: nameserverGroupToSave.description,
|
||||
primary: nameserverGroupToSave.primary,
|
||||
domains: nameserverGroupToSave.domains,
|
||||
nameservers: nameserverGroupToSave.nameservers,
|
||||
groups: newGroups,
|
||||
enabled: nameserverGroupToSave.enabled,
|
||||
} as NameServerGroup
|
||||
}
|
||||
|
||||
let effect
|
||||
if (!nameserverGroupToSave.id) {
|
||||
effect = yield call(service.createNameServerGroup, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = nameserverGroupToSave.id
|
||||
effect = yield call(service.editNameServerGroup, payloadToSave);
|
||||
}
|
||||
|
||||
const response = effect as ApiResponse<NameServerGroup>;
|
||||
|
||||
yield put(actions.saveNameServerGroup.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<NameServerGroup | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getNameServerGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
|
||||
} catch (err) {
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
yield put(actions.saveNameServerGroup.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<NameServerGroup | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeleteNameServerGroup(action: ReturnType<typeof actions.setDeletedNameServerGroup>): Generator {
|
||||
yield put(actions.setDeletedNameServerGroup(action.payload))
|
||||
}
|
||||
|
||||
export function* deleteNameServerGroup(action: ReturnType<typeof actions.deleteNameServerGroup.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedNameServerGroup,{
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
|
||||
const effect = yield call(service.deletedNameServerGroup, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deleteNameServerGroup.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const nameserverGroup = (yield select(state => state.nameserverGroup.data)) as NameServerGroup[]
|
||||
yield put(actions.getNameServerGroups.success(nameserverGroup.filter((p:NameServerGroup) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deleteNameServerGroup.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.getNameServerGroups.request, getNameServerGroups),
|
||||
takeLatest(actions.saveNameServerGroup.request, saveNameServerGroup),
|
||||
takeLatest(actions.deleteNameServerGroup.request, deleteNameServerGroup)
|
||||
]);
|
||||
}
|
||||
|
||||
32
src/store/nameservers/service.ts
Normal file
32
src/store/nameservers/service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { NameServerGroup } from './types';
|
||||
|
||||
export default {
|
||||
async getNameServerGroups(payload:RequestPayload<null>): Promise<ApiResponse<NameServerGroup[]>> {
|
||||
return apiClient.get<NameServerGroup[]>(
|
||||
`/api/dns/nameservers`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedNameServerGroup(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/dns/nameservers/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createNameServerGroup(payload:RequestPayload<NameServerGroup>): Promise<ApiResponse<NameServerGroup>> {
|
||||
return apiClient.post<NameServerGroup>(
|
||||
`/api/dns/nameservers`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editNameServerGroup(payload:RequestPayload<NameServerGroup>): Promise<ApiResponse<NameServerGroup>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<NameServerGroup>(
|
||||
`/api/dns/nameservers/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
21
src/store/nameservers/types.ts
Normal file
21
src/store/nameservers/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface NameServerGroup {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
primary: boolean
|
||||
domains: string[]
|
||||
nameservers: NameServer[]
|
||||
groups: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface NameServer {
|
||||
ip: string
|
||||
ns_type: string
|
||||
port: number
|
||||
}
|
||||
|
||||
export interface NameServerGroupToSave extends NameServerGroup
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -10,6 +10,18 @@ export interface Peer {
|
||||
version: string,
|
||||
groups?: Group[]
|
||||
ssh_enabled: boolean,
|
||||
hostname: string,
|
||||
user_id?: string,
|
||||
ui_version?: string,
|
||||
dns_label: string,
|
||||
last_login: string,
|
||||
login_expired: boolean,
|
||||
login_expiration_enabled: boolean
|
||||
}
|
||||
|
||||
export interface FormPeer extends Peer {
|
||||
groupsNames: string[],
|
||||
userEmail?: string
|
||||
}
|
||||
|
||||
export interface PeerToSave extends Peer {
|
||||
@@ -21,4 +33,22 @@ export interface PeerGroupsToSave {
|
||||
groupsToRemove: string[];
|
||||
groupsToAdd: string[];
|
||||
groupsNoId: string[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface PeerNameToIP {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToName {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToID {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerDataTable extends Peer {
|
||||
key: string;
|
||||
groups: Group[];
|
||||
groupsCount: number;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
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 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';
|
||||
|
||||
export default {
|
||||
peer: PeerActions,
|
||||
setupKey: SetupKeyActions,
|
||||
user: UserActions,
|
||||
group: GroupActions,
|
||||
rule: RuleActions
|
||||
rule: RuleActions,
|
||||
route: RouteActions,
|
||||
nameserverGroup: NameServerGroupActions,
|
||||
event: EventActions,
|
||||
dnsSettings: DNSSettingsActions,
|
||||
account: AccountActions
|
||||
};
|
||||
|
||||
@@ -5,11 +5,21 @@ 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 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';
|
||||
|
||||
export default combineReducers({
|
||||
peer,
|
||||
setupKey,
|
||||
user,
|
||||
group,
|
||||
rule
|
||||
rule,
|
||||
route,
|
||||
nameserverGroup,
|
||||
event,
|
||||
dnsSettings,
|
||||
account
|
||||
});
|
||||
|
||||
35
src/store/route/actions.ts
Normal file
35
src/store/route/actions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {Route, RouteToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getRoutes: createAsyncAction(
|
||||
'GET_ROUTES_REQUEST',
|
||||
'GET_ROUTES_SUCCESS',
|
||||
'GET_ROUTES_FAILURE',
|
||||
)<RequestPayload<null>, Route[], ApiError>(),
|
||||
|
||||
saveRoute: createAsyncAction(
|
||||
'SAVE_ROUTE_REQUEST',
|
||||
'SAVE_ROUTE_SUCCESS',
|
||||
'SAVE_ROUTE_FAILURE',
|
||||
)<RequestPayload<RouteToSave>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
|
||||
setSavedRoute: createAction('SET_CREATE_ROUTE')<CreateResponse<Route | null>>(),
|
||||
resetSavedRoute: createAction('RESET_CREATE_ROUTE')<null>(),
|
||||
|
||||
deleteRoute: createAsyncAction(
|
||||
'DELETE_ROUTE_REQUEST',
|
||||
'DELETE_ROUTE_SUCCESS',
|
||||
'DELETE_ROUTE_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedRoute: createAction('SET_DELETED_ROUTE')<DeleteResponse<string | null>>(),
|
||||
resetDeletedRoute: createAction('RESET_DELETED_ROUTE')<null>(),
|
||||
removeRoute: createAction('REMOVE_ROUTE')<string>(),
|
||||
|
||||
setRoute: createAction('SET_ROUTE')<Route>(),
|
||||
setSetupNewRouteVisible: createAction('SET_SETUP_NEW_ROUTE_VISIBLE')<boolean>(),
|
||||
setSetupNewRouteHA: createAction('SET_SETUP_NEW_ROUTE_HA')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/route/index.ts
Normal file
7
src/store/route/index.ts
Normal 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 };
|
||||
95
src/store/route/reducer.ts
Normal file
95
src/store/route/reducer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Route } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, DeleteResponse, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Route[] | null;
|
||||
route: Route | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deleteRoute: DeleteResponse<string | null>;
|
||||
savedRoute: CreateResponse<Route | null>;
|
||||
setupNewRouteVisible: boolean;
|
||||
setupNewRouteHA: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
route: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deleteRoute: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
savedRoute: <CreateResponse<Route | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewRouteVisible: false,
|
||||
setupNewRouteHA: false
|
||||
};
|
||||
|
||||
const data = createReducer<Route[], ActionTypes>(initialState.data as Route[])
|
||||
.handleAction(actions.getRoutes.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getRoutes.failure, () => []);
|
||||
|
||||
const route = createReducer<Route, ActionTypes>(initialState.route as Route)
|
||||
.handleAction(actions.setRoute, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getRoutes.request, () => true)
|
||||
.handleAction(actions.getRoutes.success, () => false)
|
||||
.handleAction(actions.getRoutes.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getRoutes.request, () => null)
|
||||
.handleAction(actions.getRoutes.success, () => null)
|
||||
.handleAction(actions.getRoutes.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getRoutes.request, () => true)
|
||||
.handleAction(actions.getRoutes.success, () => false)
|
||||
.handleAction(actions.getRoutes.failure, () => false);
|
||||
|
||||
const deletedRoute = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deleteRoute)
|
||||
.handleAction(actions.deleteRoute.request, () => initialState.deleteRoute)
|
||||
.handleAction(actions.deleteRoute.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deleteRoute.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedRoute, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedRoute, () => initialState.deleteRoute)
|
||||
|
||||
const savedRoute = createReducer<CreateResponse<Route | null>, ActionTypes>(initialState.savedRoute)
|
||||
.handleAction(actions.saveRoute.request, () => initialState.savedRoute)
|
||||
.handleAction(actions.saveRoute.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveRoute.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedRoute, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedRoute, () => initialState.savedRoute)
|
||||
|
||||
const setupNewRouteVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRouteVisible)
|
||||
.handleAction(actions.setSetupNewRouteVisible, (store, action) => action.payload)
|
||||
|
||||
const setupNewRouteHA = createReducer<boolean, ActionTypes>(initialState.setupNewRouteHA)
|
||||
.handleAction(actions.setSetupNewRouteHA, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
route,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedRoute,
|
||||
savedRoute,
|
||||
setupNewRouteVisible,
|
||||
setupNewRouteHA
|
||||
});
|
||||
162
src/store/route/sagas.ts
Normal file
162
src/store/route/sagas.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {Route} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
|
||||
export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>): Generator {
|
||||
try {
|
||||
|
||||
yield put(actions.setDeletedRoute({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const effect = yield call(service.getRoutes, action.payload);
|
||||
const response = effect as ApiResponse<Route[]>;
|
||||
|
||||
yield put(actions.getRoutes.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getRoutes.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedRoute(action: ReturnType<typeof actions.setSavedRoute>): Generator {
|
||||
yield put(actions.setSavedRoute(action.payload))
|
||||
}
|
||||
|
||||
export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedRoute({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<Route | null>))
|
||||
|
||||
const routeToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = routeToSave.groupsToCreate
|
||||
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: {
|
||||
id: routeToSave.id,
|
||||
description: routeToSave.description,
|
||||
enabled: routeToSave.enabled,
|
||||
masquerade: routeToSave.masquerade,
|
||||
metric: routeToSave.metric,
|
||||
network: routeToSave.network,
|
||||
network_id: routeToSave.network_id,
|
||||
peer: routeToSave.peer,
|
||||
groups: newGroups
|
||||
} as Route
|
||||
}
|
||||
|
||||
let effect
|
||||
if (!routeToSave.id) {
|
||||
effect = yield call(service.createRoute, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = routeToSave.id
|
||||
effect = yield call(service.editRoute, payloadToSave);
|
||||
}
|
||||
|
||||
const response = effect as ApiResponse<Route>;
|
||||
|
||||
yield put(actions.saveRoute.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<Route | null>));
|
||||
|
||||
yield put(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,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<Route | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeleteRoute(action: ReturnType<typeof actions.setDeletedRoute>): Generator {
|
||||
yield put(actions.setDeletedRoute(action.payload))
|
||||
}
|
||||
|
||||
export function* deleteRoute(action: ReturnType<typeof actions.deleteRoute.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedRoute,{
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
|
||||
const effect = yield call(service.deletedRoute, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deleteRoute.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const routes = (yield select(state => state.route.data)) as Route[]
|
||||
yield put(actions.getRoutes.success(routes.filter((p:Route) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deleteRoute.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.getRoutes.request, getRoutes),
|
||||
takeLatest(actions.saveRoute.request, saveRoute),
|
||||
takeLatest(actions.deleteRoute.request, deleteRoute)
|
||||
]);
|
||||
}
|
||||
|
||||
32
src/store/route/service.ts
Normal file
32
src/store/route/service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { Route } from './types';
|
||||
|
||||
export default {
|
||||
async getRoutes(payload:RequestPayload<null>): Promise<ApiResponse<Route[]>> {
|
||||
return apiClient.get<Route[]>(
|
||||
`/api/routes`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedRoute(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/routes/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createRoute(payload:RequestPayload<Route>): Promise<ApiResponse<Route>> {
|
||||
return apiClient.post<Route>(
|
||||
`/api/routes`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editRoute(payload:RequestPayload<Route>): Promise<ApiResponse<Route>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Route>(
|
||||
`/api/routes/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
18
src/store/route/types.ts
Normal file
18
src/store/route/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
export interface Route {
|
||||
id?: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
peer: string
|
||||
network: string
|
||||
network_id: string
|
||||
network_type?: string
|
||||
metric?: number
|
||||
masquerade: boolean
|
||||
groups: string[]
|
||||
}
|
||||
|
||||
export interface RouteToSave extends Route
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
@@ -53,7 +53,6 @@ export function* saveRule(action: ReturnType<typeof actions.saveRule.request>):
|
||||
})
|
||||
))
|
||||
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Rule>[]).filter(r => r.statusCode === 200).map(r => (r.body as Group))
|
||||
|
||||
const currentGroups = [...(yield select(state => state.group.data)) as Rule[]]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {SetupKey, SetupKeyRevoke} from './types';
|
||||
import {SetupKey, SetupKeyToSave} from './types';
|
||||
import {
|
||||
ApiError,
|
||||
ChangeResponse,
|
||||
CreateResponse,
|
||||
DeleteResponse,
|
||||
RequestPayload
|
||||
@@ -15,12 +14,13 @@ const actions = {
|
||||
'GET_SETUP_KEYS_FAILURE',
|
||||
)<RequestPayload<null>, SetupKey[], ApiError>(),
|
||||
|
||||
createSetupKey: createAsyncAction(
|
||||
'CREATE_SETUP_KEY_REQUEST',
|
||||
'CREATE_SETUP_KEY_SUCCESS',
|
||||
'CREATE_SETUP_KEY_FAILURE',
|
||||
)<RequestPayload<SetupKey>, CreateResponse<SetupKey | null>, CreateResponse<SetupKey | null>>(),
|
||||
setCreateSetupKey: createAction('SET_CREATE_SETUP_KEY')<CreateResponse<SetupKey | null>>(),
|
||||
saveSetupKey: createAsyncAction(
|
||||
'SAVE_SETUP_KEY_REQUEST',
|
||||
'SAVE_SETUP_KEY_SUCCESS',
|
||||
'SAVE_SETUP_KEY_FAILURE',
|
||||
)<RequestPayload<SetupKeyToSave>, CreateResponse<SetupKey | null>, CreateResponse<SetupKey | null>>(),
|
||||
setSavedSetupKey: createAction('SET_SAVE_SETUP_KEY')<CreateResponse<SetupKey | null>>(),
|
||||
resetSavedSetupKey: createAction('RESET_SAVE_SETUP_KEY')<null>(),
|
||||
|
||||
deleteSetupKey: createAsyncAction(
|
||||
'DELETE_SETUP_KEY_REQUEST',
|
||||
@@ -30,15 +30,6 @@ const actions = {
|
||||
setDeleteSetupKey: createAction('SET_DELETE_SETUP_KEY')<DeleteResponse<string | null>>(),
|
||||
resetDeletedSetupKey: createAction('RESET_DELETE_SETUP_KEY')<null>(),
|
||||
|
||||
revokeSetupKey: createAsyncAction(
|
||||
'REVOKE_SETUP_KEY_REQUEST',
|
||||
'REVOKE_SETUP_KEY_SUCCESS',
|
||||
'REVOKE_SETUP_KEY_FAILURE'
|
||||
)<RequestPayload<SetupKeyRevoke>, ChangeResponse<SetupKey | null>, ChangeResponse<SetupKey | null>>(),
|
||||
setRevokeSetupKey: createAction('SET_REVOKED_SETUP_KEY')<ChangeResponse<SetupKey | null>>(),
|
||||
resetRevokedSetupKey: createAction('RESET_REVOKED_SETUP_KEY')<null>(),
|
||||
|
||||
|
||||
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
|
||||
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
|
||||
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>()
|
||||
|
||||
@@ -12,7 +12,7 @@ type StateType = Readonly<{
|
||||
saving: boolean;
|
||||
deletedSetupKey: DeleteResponse<string | null>;
|
||||
revokedSetupKey: ChangeResponse<SetupKey | null>;
|
||||
createdSetupKey: CreateResponse<SetupKey | null>;
|
||||
savedSetupKey: CreateResponse<SetupKey | null>;
|
||||
setupNewKeyVisible: boolean
|
||||
}>;
|
||||
|
||||
@@ -36,7 +36,7 @@ const initialState: StateType = {
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
createdSetupKey: <CreateResponse<SetupKey | null>>{
|
||||
savedSetupKey: <CreateResponse<SetupKey | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
@@ -75,18 +75,12 @@ const deletedSetupKey = createReducer<DeleteResponse<string | null>, ActionTypes
|
||||
.handleAction(actions.setDeleteSetupKey, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedSetupKey, (store, action) => initialState.deletedSetupKey);
|
||||
|
||||
const revokedSetupKey = createReducer<ChangeResponse<SetupKey | null>, ActionTypes>(initialState.revokedSetupKey)
|
||||
.handleAction(actions.revokeSetupKey.request, () => initialState.revokedSetupKey)
|
||||
.handleAction(actions.revokeSetupKey.success, (store, action) => action.payload)
|
||||
.handleAction(actions.revokeSetupKey.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setRevokeSetupKey, (store, action) => action.payload)
|
||||
.handleAction(actions.resetRevokedSetupKey, () => initialState.revokedSetupKey)
|
||||
|
||||
const createdSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes>(initialState.createdSetupKey)
|
||||
.handleAction(actions.createSetupKey.request, () => initialState.createdSetupKey)
|
||||
.handleAction(actions.createSetupKey.success, (store, action) => action.payload)
|
||||
.handleAction(actions.createSetupKey.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setCreateSetupKey, (store, action) => action.payload)
|
||||
const savedSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes>(initialState.savedSetupKey)
|
||||
.handleAction(actions.saveSetupKey.request, () => initialState.savedSetupKey)
|
||||
.handleAction(actions.saveSetupKey.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveSetupKey.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedSetupKey, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedSetupKey, () => initialState.savedSetupKey)
|
||||
|
||||
const setupNewKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewKeyVisible)
|
||||
.handleAction(actions.setSetupNewKeyVisible, (store, action) => action.payload)
|
||||
@@ -98,7 +92,6 @@ export default combineReducers({
|
||||
failed,
|
||||
saving,
|
||||
deletedSetupKey,
|
||||
revokedSetupKey,
|
||||
createdSetupKey,
|
||||
savedSetupKey: savedSetupKey,
|
||||
setupNewKeyVisible
|
||||
});
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, ChangeResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {SetupKey, SetupKeyRevoke} from './types'
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {SetupKey, SetupKeyToSave} 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* getSetupKeys(action: ReturnType<typeof actions.getSetupKeys.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getSetupKeys, action.payload);
|
||||
const response = effect as ApiResponse<SetupKey[]>;
|
||||
|
||||
yield put(actions.getSetupKeys.success(response.body));
|
||||
yield put(actions.getSetupKeys.success(response.body.map(k => {
|
||||
// always set auto_groups even if absent (avoid null)
|
||||
if (k.auto_groups) {
|
||||
return k
|
||||
}
|
||||
return {...k, auto_groups: []}
|
||||
})));
|
||||
} catch (err) {
|
||||
yield put(actions.getSetupKeys.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreateSetupKey(action: ReturnType<typeof actions.setCreateSetupKey>): Generator {
|
||||
yield put(actions.setCreateSetupKey(action.payload))
|
||||
export function* setCreateSetupKey(action: ReturnType<typeof actions.setSavedSetupKey>): Generator {
|
||||
yield put(actions.setSavedSetupKey(action.payload))
|
||||
}
|
||||
|
||||
export function* createSetupKey(action: ReturnType<typeof actions.createSetupKey.request>): Generator {
|
||||
export function* saveSetupKey(action: ReturnType<typeof actions.saveSetupKey.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setCreateSetupKey({
|
||||
yield put(actions.setSavedSetupKey({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
@@ -29,10 +38,49 @@ export function* createSetupKey(action: ReturnType<typeof actions.createSetupKey
|
||||
data: null
|
||||
} as CreateResponse<SetupKey | null>))
|
||||
|
||||
const effect = yield call(service.createSetupKey, action.payload);
|
||||
const keyToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = keyToSave.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 = [...keyToSave.auto_groups, ...resGroups]
|
||||
let effect
|
||||
if (!keyToSave.id) {
|
||||
effect = yield call(service.createSetupKey, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
name: keyToSave.name,
|
||||
auto_groups: newGroups,
|
||||
type: keyToSave.type,
|
||||
expires_in: keyToSave.expires_in,
|
||||
usage_limit: keyToSave.usage_limit
|
||||
} as SetupKeyToSave
|
||||
});
|
||||
} else {
|
||||
effect = yield call(service.editSetupKey, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
id: keyToSave.id,
|
||||
name: keyToSave.name,
|
||||
revoked: keyToSave.revoked,
|
||||
auto_groups: newGroups,
|
||||
usage_limit: keyToSave.usage_limit
|
||||
} as SetupKeyToSave
|
||||
});
|
||||
}
|
||||
const response = effect as ApiResponse<SetupKey>;
|
||||
|
||||
yield put(actions.createSetupKey.success({
|
||||
yield put(actions.saveSetupKey.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
@@ -40,11 +88,10 @@ export function* createSetupKey(action: ReturnType<typeof actions.createSetupKey
|
||||
data: response.body
|
||||
} as CreateResponse<SetupKey | null>));
|
||||
|
||||
const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]]
|
||||
setupKeys.unshift(response.body)
|
||||
yield put(actions.getSetupKeys.success(setupKeys));
|
||||
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
yield put(actions.getSetupKeys.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
} catch (err) {
|
||||
yield put(actions.createSetupKey.failure({
|
||||
yield put(actions.saveSetupKey.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
@@ -92,53 +139,11 @@ export function* deleteSetupKey(action: ReturnType<typeof actions.deleteSetupKey
|
||||
}
|
||||
}
|
||||
|
||||
export function* revokeSetupKey(action: ReturnType<typeof actions.revokeSetupKey.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setRevokeSetupKey({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as ChangeResponse<SetupKey | null>))
|
||||
|
||||
const effect = yield call(service.revokeSetupKey, action.payload);
|
||||
const response = effect as ApiResponse<SetupKey>;
|
||||
|
||||
yield put(actions.revokeSetupKey.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as ChangeResponse<SetupKey | null>));
|
||||
|
||||
const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]]
|
||||
let setupKey = setupKeys.find(s => s.id === response.body.id) as SetupKey
|
||||
if (setupKey) {
|
||||
setupKey.revoked = response.body.revoked
|
||||
setupKey.valid = response.body.valid
|
||||
setupKey.state = response.body.state
|
||||
setupKey.expires = response.body.expires
|
||||
}
|
||||
yield put(actions.getSetupKeys.success(setupKeys));
|
||||
} catch (err) {
|
||||
yield put(actions.createSetupKey.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<SetupKey | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getSetupKeys.request, getSetupKeys),
|
||||
takeLatest(actions.createSetupKey.request, createSetupKey),
|
||||
takeLatest(actions.deleteSetupKey.request, deleteSetupKey),
|
||||
takeLatest(actions.revokeSetupKey.request, revokeSetupKey)
|
||||
takeLatest(actions.saveSetupKey.request, saveSetupKey),
|
||||
takeLatest(actions.deleteSetupKey.request, deleteSetupKey)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import {SetupKey, SetupKeyNew, SetupKeyRevoke} from './types';
|
||||
import {SetupKey, SetupKeyToSave} from './types';
|
||||
|
||||
export default {
|
||||
async getSetupKeys(payload:RequestPayload<null>): Promise<ApiResponse<SetupKey[]>> {
|
||||
@@ -15,22 +15,19 @@ export default {
|
||||
payload
|
||||
);
|
||||
},
|
||||
async revokeSetupKey(payload:RequestPayload<SetupKeyRevoke>): Promise<ApiResponse<SetupKey>> {
|
||||
return apiClient.put<SetupKey>(
|
||||
`/api/setup-keys/` + payload.payload.id,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async renameSetupKey(payload:RequestPayload<any>): Promise<ApiResponse<SetupKey>> {
|
||||
return apiClient.put<SetupKey>(
|
||||
`/api/setup-keys/` + payload.payload.id,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createSetupKey(payload:RequestPayload<SetupKey>): Promise<ApiResponse<SetupKey>> {
|
||||
async createSetupKey(payload:RequestPayload<SetupKeyToSave>): Promise<ApiResponse<SetupKey>> {
|
||||
return apiClient.post<SetupKey>(
|
||||
`/api/setup-keys`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editSetupKey(payload:RequestPayload<SetupKeyToSave>): Promise<ApiResponse<SetupKey>> {
|
||||
const id = payload.payload.id
|
||||
// @ts-ignore
|
||||
delete payload.payload.id
|
||||
return apiClient.put<SetupKey>(
|
||||
`/api/setup-keys/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import {ExpiresInValue} from "../../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
|
||||
export interface SetupKey {
|
||||
expires: string;
|
||||
id: string;
|
||||
@@ -9,15 +12,19 @@ export interface SetupKey {
|
||||
type: string;
|
||||
used_times: number;
|
||||
valid: boolean;
|
||||
auto_groups: string[]
|
||||
expires_in: number;
|
||||
usage_limit: number;
|
||||
}
|
||||
|
||||
export interface SetupKeyNew {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
export interface FormSetupKey extends SetupKey {
|
||||
autoGroupNames: string[]
|
||||
expiresInFormatted: ExpiresInValue
|
||||
exp: moment.Moment
|
||||
last: moment.Moment
|
||||
}
|
||||
|
||||
export interface SetupKeyRevoke {
|
||||
id: string;
|
||||
revoked: boolean;
|
||||
export interface SetupKeyToSave extends SetupKey
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import { ActionType, createAsyncAction } from 'typesafe-actions';
|
||||
import { User } from './types';
|
||||
import { ApiError, RequestPayload } from '../../services/api-client/types';
|
||||
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
|
||||
import {User, UserToSave} from './types';
|
||||
import {ApiError, CreateResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getUsers: createAsyncAction(
|
||||
'GET_USERS_REQUEST',
|
||||
'GET_USERS_SUCCESS',
|
||||
'GET_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>()
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
// 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>(),
|
||||
|
||||
saveUser: createAsyncAction(
|
||||
'SAVE_USER_REQUEST',
|
||||
'SAVE_USER_SUCCESS',
|
||||
'SAVE_USER_FAILURE',
|
||||
)<RequestPayload<UserToSave>, CreateResponse<User | null>, CreateResponse<User | null>>(),
|
||||
setSavedUser: createAction('SET_SAVED_USER')<CreateResponse<User | null>>(),
|
||||
resetSavedUser: createAction('RESET_SAVED_USER')<null>(),
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
|
||||
@@ -2,18 +2,31 @@ import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { User } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import { ApiError } from "../../services/api-client/types";
|
||||
import {ApiError, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: User[] | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
user: User | null;
|
||||
savedUser: CreateResponse<User | null>;
|
||||
updateUserDrawerVisible: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
loading: false,
|
||||
failed: null,
|
||||
user: null,
|
||||
// right-sided user update drawer
|
||||
updateUserDrawerVisible: false,
|
||||
savedUser: <CreateResponse<User | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
};
|
||||
|
||||
const data = createReducer<User[], ActionTypes>(initialState.data as User[])
|
||||
@@ -30,9 +43,23 @@ const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getUsers.success, () => null)
|
||||
.handleAction(actions.getUsers.failure, (store, action) => action.payload);
|
||||
|
||||
const user = createReducer<User, ActionTypes>(initialState.user as User)
|
||||
.handleAction(actions.setUser, (store, action) => action.payload);
|
||||
const updateUserDrawerVisible = createReducer<boolean, ActionTypes>(initialState.updateUserDrawerVisible)
|
||||
.handleAction(actions.setUpdateUserDrawerVisible, (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)
|
||||
.handleAction(actions.saveUser.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedUser, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedUser, () => initialState.savedUser)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
loading,
|
||||
failed
|
||||
failed,
|
||||
user,
|
||||
savedUser,
|
||||
updateUserDrawerVisible
|
||||
});
|
||||
|
||||
@@ -1,23 +1,103 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse} from '../../services/api-client/types';
|
||||
import { User } from './types'
|
||||
import {ApiError, ApiResponse, CreateResponse} from '../../services/api-client/types';
|
||||
import {User, UserToSave} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
|
||||
export function* getPeers(action: ReturnType<typeof actions.getUsers.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
|
||||
yield put(actions.getUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getUsers.failure(err as ApiError));
|
||||
}
|
||||
yield put(actions.getUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getUsers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveUser(action: ReturnType<typeof actions.saveUser.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedUser({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<User | null>))
|
||||
|
||||
const userToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = userToSave.groupsToCreate
|
||||
if (!groupsToCreate) {
|
||||
groupsToCreate = []
|
||||
}
|
||||
|
||||
// first, create groups that were newly added by user
|
||||
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {name: g}
|
||||
})
|
||||
))
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
|
||||
const newGroups = [...userToSave.auto_groups, ...resGroups]
|
||||
let payload = {
|
||||
name: userToSave.name,
|
||||
email: userToSave.email,
|
||||
role: userToSave.role,
|
||||
auto_groups: newGroups,
|
||||
} as UserToSave
|
||||
|
||||
let effect
|
||||
if (!userToSave.id) {
|
||||
console.log("creating user:" + payload)
|
||||
effect = yield call(service.createUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload
|
||||
});
|
||||
} else {
|
||||
payload.id = userToSave.id
|
||||
effect = yield call(service.editUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
const response = effect as ApiResponse<User>;
|
||||
|
||||
yield put(actions.saveUser.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<User | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
yield put(actions.getUsers.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
} catch (err) {
|
||||
yield put(actions.saveUser.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<User | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getUsers.request, getPeers)
|
||||
]);
|
||||
yield all([
|
||||
takeLatest(actions.getUsers.request, getUsers),
|
||||
takeLatest(actions.saveUser.request, saveUser)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { User } from './types';
|
||||
import {User, UserToSave} from './types';
|
||||
|
||||
export default {
|
||||
async getUsers(payload:RequestPayload<null>): Promise<ApiResponse<User[]>> {
|
||||
@@ -8,5 +8,21 @@ export default {
|
||||
`/api/users`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
},
|
||||
async editUser(payload:RequestPayload<UserToSave>): Promise<ApiResponse<User>> {
|
||||
const id = payload.payload.id
|
||||
// @ts-ignore
|
||||
delete payload.payload.id
|
||||
return apiClient.put<User>(
|
||||
`/api/users/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createUser(payload:RequestPayload<UserToSave>): Promise<ApiResponse<User>> {
|
||||
// @ts-ignore
|
||||
return apiClient.post<User>(
|
||||
`/api/users`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,4 +3,15 @@ export interface User {
|
||||
email?: string;
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
status: string;
|
||||
auto_groups: string[];
|
||||
is_current?: boolean;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
autoGroupsNames: string[]
|
||||
}
|
||||
|
||||
export interface UserToSave extends User {
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
export const formatOS = (os) => {
|
||||
if (os.startsWith("windows 10")) {
|
||||
return "Windows 10";
|
||||
@@ -6,14 +8,39 @@ export const formatOS = (os) => {
|
||||
if (os.startsWith("Darwin")) {
|
||||
return os.replace("Darwin", "MacOS");
|
||||
}
|
||||
|
||||
// capitalize first letter
|
||||
os = os.charAt(0).toUpperCase() + os.slice(1);
|
||||
return os;
|
||||
};
|
||||
|
||||
export const formatDate = date => {
|
||||
if (new Date(date).getTime() > new Date("2099-12-31").getTime()) {
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
export const capitalize = text => {
|
||||
if (!text) {
|
||||
return text
|
||||
}
|
||||
return text.charAt(0).toUpperCase() + text.slice(1)
|
||||
}
|
||||
|
||||
export const checkExpiresIn = (_, value) => {
|
||||
if (value.number > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("Expiration must be greater than zero"));
|
||||
};
|
||||
|
||||
export const formatDateTime = date => {
|
||||
if (new Date(date).getTime() > new Date("2099-12-31").getTime()) {
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
|
||||
}
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
|
||||
}
|
||||
|
||||
export const classNames = (...classes) => {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
@@ -87,4 +114,12 @@ export const timeAgo = (dateParam) => {
|
||||
|
||||
export const copyToClipboard = (copyText) => {
|
||||
navigator.clipboard.writeText(copyText);
|
||||
}
|
||||
|
||||
export const isNetBirdHosted = () => {
|
||||
return window.location.hostname.endsWith(".netbird.io") || window.location.hostname.endsWith(".wiretrustee.com")
|
||||
}
|
||||
|
||||
export const isLocalDev = () => {
|
||||
return window.location.hostname.includes("localhost")
|
||||
}
|
||||
141
src/utils/groups.tsx
Normal file
141
src/utils/groups.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Col, Divider, Row, Tag} from "antd";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
|
||||
export const useGetGroupTagHelpers = () => {
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [groupTagFilterAll, setGroupTagFilterAll] = useState(false)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getExistingAndToCreateGroupsLists = (groupNameList: string[]): [string[], string[]] => {
|
||||
const groupIDList = groups?.filter(g => groupNameList.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const existingGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupNameListToCreate = groupNameList.filter(s => !existingGroupsNames.includes(s))
|
||||
return [groupIDList, groupNameListToCreate]
|
||||
}
|
||||
|
||||
const getGroupNamesFromIDs = (groupIDList: string[]): string[] => {
|
||||
if (!groupIDList) {
|
||||
return []
|
||||
}
|
||||
|
||||
return groups?.filter(g => groupIDList.includes(g.id!)).map(g => g.name || '') || []
|
||||
}
|
||||
|
||||
const selectValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
return selectValidatorEmptyStrings(obj,value)
|
||||
}
|
||||
|
||||
const selectValidatorEmptyStrings = (_: 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()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (groupTagFilterAll) {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
} else {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}
|
||||
}, [groups])
|
||||
|
||||
return {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
selectedTagGroups,
|
||||
setGroupTagFilterAll,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
selectValidatorEmptyStrings
|
||||
}
|
||||
}
|
||||
24
src/utils/pageSize.tsx
Normal file
24
src/utils/pageSize.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import {useState} from "react";
|
||||
|
||||
export const usePageSizeHelpers = () => {
|
||||
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "10", value: "10"},
|
||||
{label: "25", value: "25"},
|
||||
{label: "50", value: "50"},
|
||||
{label: "100", value: "100"},
|
||||
{label: "1000", value: "1000"}
|
||||
]
|
||||
|
||||
return {
|
||||
onChangePageSize,
|
||||
pageSize,
|
||||
pageSizeOptions
|
||||
}
|
||||
}
|
||||
95
src/utils/routes.ts
Normal file
95
src/utils/routes.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {Peer, PeerIPToID, PeerIPToName, PeerNameToIP} from "../store/peer/types";
|
||||
import {Route} from "../store/route/types";
|
||||
|
||||
export const routePeerSeparator = " - "
|
||||
|
||||
export const masqueradeDisabledMSG = "Enabling this option hides other NetBird network IPs behind the routing peer local address when accessing the target Network CIDR. This option allows access to your private networks without configuring routes on your local routers or other devices."
|
||||
|
||||
export const masqueradeEnabledMSG = "Disabling this option stops hiding all traffic coming from other NetBird peers behind the routing peer local address when accessing the target Network CIDR. You will need to configure routes for your NetBird network pointing to your routing peer on your local routers or other devices."
|
||||
|
||||
export const peerToPeerIP = (name: string, ip: string): string => {
|
||||
return name + routePeerSeparator + ip
|
||||
}
|
||||
|
||||
export const initPeerMaps = (peers: Peer[]): [PeerNameToIP, PeerIPToName, PeerIPToID] => {
|
||||
let peerNameToIP = {} as PeerNameToIP
|
||||
let peerIPToName = {} as PeerIPToName
|
||||
let peerIPToID = {} as PeerIPToID
|
||||
peers.forEach((p) => {
|
||||
peerNameToIP[p.name] = p.ip
|
||||
peerIPToName[p.ip] = p.name
|
||||
peerIPToID[p.ip] = p.id ? p.id : ""
|
||||
})
|
||||
return [peerNameToIP, peerIPToName, peerIPToID]
|
||||
}
|
||||
|
||||
export interface RouteDataTable extends Route {
|
||||
key: string;
|
||||
peer_ip: string;
|
||||
peer_name: string;
|
||||
}
|
||||
|
||||
export interface GroupedDataTable {
|
||||
key: string
|
||||
network_id: string
|
||||
network: string
|
||||
enabled: boolean
|
||||
masquerade: boolean
|
||||
description: string
|
||||
routesCount: number
|
||||
groupedRoutes: RouteDataTable[]
|
||||
routesGroups: string[]
|
||||
}
|
||||
|
||||
export const transformDataTable = (routes: Route[], peers: Peer[]): RouteDataTable[] => {
|
||||
|
||||
let peerMap = Object.fromEntries(peers.map(p => [p.id, p]));
|
||||
return routes.map(route => {
|
||||
return {
|
||||
key: route.id,
|
||||
...route,
|
||||
peer: route.peer,
|
||||
peer_ip: peerMap[route.peer] ? peerMap[route.peer].ip : route.peer,
|
||||
peer_name: peerMap[route.peer] ? peerMap[route.peer].name : route.peer,
|
||||
} as RouteDataTable
|
||||
})
|
||||
}
|
||||
|
||||
export const transformGroupedDataTable = (routes: Route[], peers: Peer[]): GroupedDataTable[] => {
|
||||
let keySet = new Set(routes.map(r => {
|
||||
return r.network_id + r.network
|
||||
}))
|
||||
|
||||
let groupedRoutes: GroupedDataTable[] = []
|
||||
|
||||
keySet.forEach((p) => {
|
||||
let hasEnabled = false
|
||||
let lastRoute: Route
|
||||
let listedRoutes: Route[] = []
|
||||
let groupList: string[] = []
|
||||
routes.forEach((r) => {
|
||||
if (p === r.network_id + r.network) {
|
||||
lastRoute = r
|
||||
if (r.enabled) {
|
||||
hasEnabled = true
|
||||
}
|
||||
listedRoutes.push(r)
|
||||
groupList = groupList.concat(r.groups)
|
||||
}
|
||||
})
|
||||
groupList = groupList.filter((value, index, arrary) => arrary.indexOf(value) === index)
|
||||
let groupDataTableRoutes = transformDataTable(listedRoutes, peers)
|
||||
groupedRoutes.push({
|
||||
key: p.toString(),
|
||||
network_id: lastRoute!.network_id,
|
||||
network: lastRoute!.network,
|
||||
masquerade: lastRoute!.masquerade,
|
||||
description: lastRoute!.description,
|
||||
enabled: hasEnabled,
|
||||
routesCount: groupDataTableRoutes.length,
|
||||
groupedRoutes: groupDataTableRoutes,
|
||||
routesGroups: groupList,
|
||||
})
|
||||
})
|
||||
return groupedRoutes
|
||||
}
|
||||
53
src/utils/token.ts
Normal file
53
src/utils/token.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
import {useEffect} from "react";
|
||||
|
||||
function sleep(ms : number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function b64DecodeUnicode(str: string): string {
|
||||
// See https://www.rfc-editor.org/rfc/rfc7515.txt, Appendix C
|
||||
str = str.replace('-', '+');
|
||||
str = str.replace('_', '/');
|
||||
switch (str.length % 4) {
|
||||
case 0: break;
|
||||
case 2: str += '=='; break;
|
||||
case 3: str += '='; break;
|
||||
}
|
||||
return decodeURIComponent(Array.prototype.map.call(atob(str), (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
|
||||
}
|
||||
|
||||
function parseJwt(token:string) {
|
||||
return JSON.parse(b64DecodeUnicode(token.split('.')[1].replace('-', '+').replace('_', '/')))
|
||||
}
|
||||
|
||||
function isTokenValid(token:string) {
|
||||
let tokenPayload = parseJwt(token)
|
||||
return tokenPayload && (tokenPayload.exp * 1000) > (Date.now() - 5000)
|
||||
}
|
||||
|
||||
// latestToken as global allows for use of the latest state value across calls
|
||||
let latestToken:string
|
||||
|
||||
|
||||
// hook that returns a getAccessTokenSilently function that returns an access token promise,
|
||||
// waiting for renewal if it was expired
|
||||
export const useGetAccessTokenSilently = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
latestToken = accessToken
|
||||
const getAccessTokenSilently = async (): Promise<string> => {
|
||||
let attempt = 0
|
||||
while (!isTokenValid(latestToken) && attempt < 15){
|
||||
attempt++
|
||||
await sleep(500)
|
||||
}
|
||||
|
||||
return latestToken
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
latestToken = accessToken
|
||||
}, [accessToken])
|
||||
|
||||
return {getAccessTokenSilently}
|
||||
}
|
||||
@@ -1,9 +1,23 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button, Card,
|
||||
Col, Dropdown, Input, Menu, message, Modal, Popover, Radio, RadioChangeEvent,
|
||||
Row, Select, Space, Table, Tag, Tooltip,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {Container} from "../components/Container";
|
||||
@@ -21,10 +35,12 @@ import AccessControlNew from "../components/AccessControlNew";
|
||||
import {Group} from "../store/group/types";
|
||||
import AccessControlModalGroups from "../components/AccessControlModalGroups";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useOidcAccessToken} from '@axa-fr/react-oidc';
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Column } = Table;
|
||||
const { confirm } = Modal;
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
interface RuleDataTable extends Rule {
|
||||
key: string;
|
||||
@@ -41,7 +57,8 @@ interface GroupsToShow {
|
||||
}
|
||||
|
||||
export const AccessControl = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const rules = useSelector((state: RootState) => state.rule.data);
|
||||
@@ -53,19 +70,15 @@ export const AccessControl = () => {
|
||||
const [showTutorial, setShowTutorial] = useState(true)
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [dataTable, setDataTable] = useState([] as RuleDataTable[]);
|
||||
const [ruleToAction, setRuleToAction] = useState(null as RuleDataTable | null);
|
||||
const [groupsToShow, setGroupsToShow] = useState({} as GroupsToShow)
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible);
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'},{label: 'All', value: 'all'}]
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
@@ -81,17 +94,17 @@ export const AccessControl = () => {
|
||||
label: (<Button type="text" block onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
}
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const getSourceDestinationLabel = (data:Group[]):string => {
|
||||
const getSourceDestinationLabel = (data: Group[]): string => {
|
||||
return (!data) ? "No group" : (data.length > 1) ? `${data.length} Groups` : (data.length === 1) ? data[0].name : "No group"
|
||||
}
|
||||
|
||||
const isShowTutorial = (rules:Rule[]):boolean => {
|
||||
const isShowTutorial = (rules: Rule[]): boolean => {
|
||||
return (!rules.length || (rules.length === 1 && rules[0].name === "Default"))
|
||||
}
|
||||
|
||||
const transformDataTable = (d:Rule[]):RuleDataTable[] => {
|
||||
const transformDataTable = (d: Rule[]): RuleDataTable[] => {
|
||||
return d.map(p => {
|
||||
const sourceLabel = getSourceDestinationLabel(p.sources as Group[])
|
||||
const destinationLabel = getSourceDestinationLabel(p.destinations as Group[])
|
||||
@@ -106,47 +119,66 @@ export const AccessControl = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(ruleActions.getRules.request({getAccessTokenSilently:accessToken, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently:accessToken, payload: null}));
|
||||
dispatch(ruleActions.getRules.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setShowTutorial(isShowTutorial(rules))
|
||||
setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
|
||||
if (failed) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(isShowTutorial(rules))
|
||||
setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
|
||||
}
|
||||
}, [rules])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const styleNotification = { marginTop: 85 }
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
const saveKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedRule.loading) {
|
||||
message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification })
|
||||
message.loading({content: 'Saving...', key: saveKey, duration: 0, style: styleNotification})
|
||||
} else if (savedRule.success) {
|
||||
message.success({ content: 'Rule has been successfully updated.', key: saveKey, duration: 2, style: styleNotification });
|
||||
message.success({
|
||||
content: 'Rule has been successfully saved.',
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(false))
|
||||
dispatch(ruleActions.setSavedRule({ ...savedRule, success: false }))
|
||||
dispatch(ruleActions.setSavedRule({...savedRule, success: false}))
|
||||
dispatch(ruleActions.resetSavedRule(null))
|
||||
} else if (savedRule.error) {
|
||||
message.error({ content: 'Failed to update rule. You might not have enough permissions.', key: saveKey, duration: 2, style: styleNotification });
|
||||
dispatch(ruleActions.setSavedRule({ ...savedRule, error: null }))
|
||||
message.error({
|
||||
content: 'Failed to update rule. You might not have enough permissions.',
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(ruleActions.setSavedRule({...savedRule, error: null}))
|
||||
dispatch(ruleActions.resetSavedRule(null))
|
||||
}
|
||||
}, [savedRule])
|
||||
|
||||
const deleteKey = 'deleting';
|
||||
useEffect(() => {
|
||||
const style = { marginTop: 85 }
|
||||
const style = {marginTop: 85}
|
||||
if (deletedRule.loading) {
|
||||
message.loading({ content: 'Deleting...', key: deleteKey, style })
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style})
|
||||
} else if (deletedRule.success) {
|
||||
message.success({ content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style })
|
||||
message.success({content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style})
|
||||
dispatch(ruleActions.resetDeletedRule(null))
|
||||
} else if (deletedRule.error) {
|
||||
message.error({ content: 'Failed to remove rule. You might not have enough permissions.', key: deleteKey, duration: 2, style })
|
||||
message.error({
|
||||
content: 'Failed to remove rule. You might not have enough permissions.',
|
||||
key: deleteKey,
|
||||
duration: 2,
|
||||
style
|
||||
})
|
||||
dispatch(ruleActions.resetDeletedRule(null))
|
||||
}
|
||||
}, [deletedRule])
|
||||
@@ -160,29 +192,28 @@ export const AccessControl = () => {
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => {
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{ruleToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete rule "{ruleToAction ? ruleToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
|
||||
<Paragraph>Are you sure you want to delete this rule from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(ruleActions.deleteRule.request({getAccessTokenSilently:accessToken, payload: ruleToAction?.id || ''}));
|
||||
dispatch(ruleActions.deleteRule.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: ruleToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setRuleToAction(null);
|
||||
@@ -192,7 +223,7 @@ export const AccessControl = () => {
|
||||
|
||||
const showConfirmDeactivate = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{ruleToAction &&
|
||||
@@ -212,13 +243,13 @@ export const AccessControl = () => {
|
||||
});
|
||||
}
|
||||
|
||||
const filterDataTable = ():Rule[] => {
|
||||
const filterDataTable = (): Rule[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f:Rule[] = filter(rules, (f:Rule) =>
|
||||
let f: Rule[] = filter(rules, (f: Rule) =>
|
||||
(f.name.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "")
|
||||
) as Rule[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f:Rule) => !f.disabled)
|
||||
f = filter(f, (f: Rule) => !f.disabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
@@ -261,7 +292,7 @@ export const AccessControl = () => {
|
||||
} as Rule))
|
||||
}
|
||||
|
||||
const toggleModalGroups = (title:string, groups:Group[] | string[] | null, modalVisible:boolean) => {
|
||||
const toggleModalGroups = (title: string, groups: Group[] | string[] | null, modalVisible: boolean) => {
|
||||
setGroupsToShow({
|
||||
title,
|
||||
groups,
|
||||
@@ -269,40 +300,60 @@ export const AccessControl = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, groups:Group[] | string[] | null, rule: RuleDataTable) => {
|
||||
useEffect(() => {
|
||||
if (setupNewRuleVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
}
|
||||
}, [setupNewRuleVisible])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean) => {
|
||||
if (setupNewRuleVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, rule: RuleDataTable) => {
|
||||
const content = groups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
return (
|
||||
<Popover content={<Space direction="vertical">{content}</Space>} title={null}>
|
||||
<Popover
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
return(
|
||||
return (
|
||||
<>
|
||||
<Container className="container-main">
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Access Control</Title>
|
||||
<Paragraph>Access rules help you manage access permissions in your organisation.</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
@@ -313,7 +364,8 @@ export const AccessControl = () => {
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
@@ -324,13 +376,15 @@ export const AccessControl = () => {
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" disabled={savedRule.loading} onClick={onClickAddNewRule}>Add Rule</Button>
|
||||
<Button type="primary" disabled={savedRule.loading}
|
||||
onClick={onClickAddNewRule}>Add Rule</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
@@ -353,24 +407,27 @@ export const AccessControl = () => {
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
const desc = (record as RuleDataTable).description.trim()
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"} arrowPointAtCenter>
|
||||
<span onClick={() => setRuleAndView(record as RuleDataTable)} className="tooltip-label">{text}</span>
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"}
|
||||
arrowPointAtCenter>
|
||||
<span onClick={() => setRuleAndView(record as RuleDataTable)}
|
||||
className="tooltip-label"><Text strong>{text}</Text></span>
|
||||
</Tooltip>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="disabled"
|
||||
render={(text:Boolean, record:RuleDataTable, index) => {
|
||||
return text ? <Tag color="red">disabled</Tag> : <Tag color="green">enabled</Tag>
|
||||
render={(text: Boolean, record: RuleDataTable, index) => {
|
||||
return text ? <Tag color="red">disabled</Tag> :
|
||||
<Tag color="green">enabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Sources" dataIndex="sourceLabel"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.sources,record as RuleDataTable)
|
||||
return renderPopoverGroups(text, record.sources, record as RuleDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Direction" dataIndex="flow"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties
|
||||
if (text === "bidirect")
|
||||
return <Tag color="processing" style={s}><img src={bidirect}/></Tag>
|
||||
@@ -379,22 +436,23 @@ export const AccessControl = () => {
|
||||
} else if (text === "destToSrc") {
|
||||
return <Tag color="green" style={s}><img src={inbound}/></Tag>
|
||||
}
|
||||
return <Tag color="red" style={s}><CloseOutlined /></Tag>
|
||||
return <Tag color="red" style={s}><CloseOutlined/></Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Destinations" dataIndex="destinationLabel"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.destinations,record as RuleDataTable)
|
||||
return renderPopoverGroups(text, record.destinations, record as RuleDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
if (deletedRule.loading || savedRule.loading) return <></>
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setRuleToAction(record as RuleDataTable)
|
||||
}}></Dropdown.Button>
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setRuleToAction(record as RuleDataTable)
|
||||
}}></Dropdown.Button>
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
@@ -409,7 +467,9 @@ export const AccessControl = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<AccessControlModalGroups data={groupsToShow.groups} title={groupsToShow.title} visible={groupsToShow.modalVisible} onCancel={() => toggleModalGroups("", [], false)}/>
|
||||
<AccessControlModalGroups data={groupsToShow.groups} title={groupsToShow.title}
|
||||
visible={groupsToShow.modalVisible}
|
||||
onCancel={() => toggleModalGroups("", [], false)}/>
|
||||
<AccessControlNew/>
|
||||
</>
|
||||
)
|
||||
|
||||
293
src/views/Activity.tsx
Normal file
293
src/views/Activity.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as eventActions} from '../store/event';
|
||||
import {Container} from "../components/Container";
|
||||
import {Alert, Button, Card, Col, Input, Row, Select, Space, Table, Typography,} from "antd";
|
||||
import {Event} from "../store/event/types";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import UserUpdate from "../components/UserUpdate";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {capitalize, formatDateTime} from "../utils/common";
|
||||
import {User} from "../store/user/types";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import {QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
|
||||
interface EventDataTable extends Event {
|
||||
}
|
||||
|
||||
export const Activity = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {oidcUser} = useOidcUser();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const events = useSelector((state: RootState) => state.event.data);
|
||||
const failed = useSelector((state: RootState) => state.event.failed);
|
||||
const loading = useSelector((state: RootState) => state.event.loading);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
|
||||
|
||||
|
||||
const transformDataTable = (d: Event[]): EventDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as EventDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(eventActions.getEvents.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(events))
|
||||
}, [events])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): Event[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let usrsMatch: User[] = filter(users, (u: User) => (u.name)?.toLowerCase().includes(t) || (u.email)?.toLowerCase().includes(t)) as User[]
|
||||
let f: Event[] = filter(events, (f: Event) =>
|
||||
((f.activity || f.id).toLowerCase().includes(t) || t === "" || usrsMatch.find(u => u.id === f.initiator_id))
|
||||
) as Event[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const getActivityRow = (group:string,text:string) => {
|
||||
return <Row> <Text>Group <Text type="secondary">{group}</Text> {text}</Text> </Row>
|
||||
}
|
||||
|
||||
const renderActivity = (event: EventDataTable) => {
|
||||
let body = <Text>{event.activity}</Text>
|
||||
switch (event.activity_code) {
|
||||
case "peer.group.add":
|
||||
return getActivityRow(event.meta.group,"added to peer")
|
||||
case "peer.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed from peer")
|
||||
case "user.group.add":
|
||||
return getActivityRow(event.meta.group,"added to user")
|
||||
case "user.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed from user")
|
||||
case "setupkey.group.add":
|
||||
return getActivityRow(event.meta.group,"added to setup key")
|
||||
case "setupkey.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed setup key")
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return getActivityRow(event.meta.group,"added to disabled management DNS setting")
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return getActivityRow(event.meta.group,"removed from disabled management DNS setting")
|
||||
}
|
||||
return body
|
||||
}
|
||||
const renderInitiator = (event: EventDataTable) => {
|
||||
let body = <></>
|
||||
const user = users?.find(u => u.id === event.initiator_id)
|
||||
switch (event.activity_code) {
|
||||
case "setupkey.peer.add":
|
||||
const key = setupKeys?.find(k => k.id === event.initiator_id)
|
||||
if (key) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{key.name}</Text> </Row>
|
||||
<Row> <Text type="secondary">Setup Key</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (user) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{user.name ? user.name : user.id}</Text> </Row>
|
||||
<Row> <Text type="secondary">{user.email ? user.email : "User"}</Text> </Row>
|
||||
</span>
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
const renderMultiRowSpan = (primaryRowText:string,secondaryRowText:string) => {
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{primaryRowText}</Text> </Row>
|
||||
<Row> <Text type="secondary">{secondaryRowText}</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
|
||||
const renderTarget = (event: EventDataTable) => {
|
||||
if (event.activity_code === "account.create" || event.activity_code === "user.join") {
|
||||
return "-"
|
||||
}
|
||||
const user = users?.find(u => u.id === event.target_id)
|
||||
switch (event.activity_code) {
|
||||
case "account.create":
|
||||
case "user.join":
|
||||
return "-"
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Rule")
|
||||
case "policy.add":
|
||||
case "policy.delete":
|
||||
case "policy.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Policy")
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
let cType:string
|
||||
cType = capitalize(event.meta.type)
|
||||
return renderMultiRowSpan(event.meta.name,cType+" setup key "+event.meta.key)
|
||||
case "group.add":
|
||||
case "group.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Group")
|
||||
case "nameserver.group.add":
|
||||
case "nameserver.group.update":
|
||||
case "nameserver.group.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Nameserver group")
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
case "peer.ssh.enable":
|
||||
case "peer.ssh.disable":
|
||||
case "peer.rename":
|
||||
case "peer.login.expiration.disable":
|
||||
case "peer.login.expiration.enable":
|
||||
return renderMultiRowSpan(event.meta.fqdn,event.meta.ip)
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
case "route.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Route for range " + event.meta.network_range)
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
case "user.role.update":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
return "-"
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey,"Setup Key")
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
case "account.setting.peer.login.expiration.enable":
|
||||
case "account.setting.peer.login.expiration.disable":
|
||||
case "account.setting.peer.login.expiration.update":
|
||||
return renderMultiRowSpan("","System setting")
|
||||
case "user.invite":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
break
|
||||
default:
|
||||
console.error("unknown event - missing handling", event.activity_code)
|
||||
}
|
||||
|
||||
return event.target_id
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Activity</Title>
|
||||
<Paragraph>Here you can see all the account and network activity events</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/activity-monitoring">Learn more about activity tracking</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
|
||||
type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
size="small"
|
||||
>
|
||||
<Column title="Timestamp" dataIndex="timestamp"
|
||||
render={(text, record, index) => {
|
||||
return formatDateTime(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Activity" dataIndex="activity"
|
||||
render={(text, record, index) => {
|
||||
return renderActivity(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Initiated By" dataIndex="initiator_id"
|
||||
render={(text, record, index) => {
|
||||
return renderInitiator(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Target" dataIndex="target_id"
|
||||
render={(text, record, index) => {
|
||||
return renderTarget(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<UserUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Activity;
|
||||
78
src/views/DNS.tsx
Normal file
78
src/views/DNS.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import type { TabsProps } from 'antd';
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import Nameservers from "./Nameservers";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import DNSSettingsForm from "./DNSSettings";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
|
||||
export const DNS = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
const nsTabKey = '1'
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: nsTabKey,
|
||||
label: 'Nameservers',
|
||||
children: <Nameservers/>,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Settings',
|
||||
children: <DNSSettingsForm/>,
|
||||
},
|
||||
]
|
||||
|
||||
const onTabClick = (key:string) => {
|
||||
if (key == nsTabKey) {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
defaultActiveKey={nsTabKey}
|
||||
items={items}
|
||||
onTabClick={onTabClick}
|
||||
animated={{ inkBar: true, tabPane: false }}
|
||||
tabPosition="top"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<NameServerGroupUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DNS;
|
||||
176
src/views/DNSSettings.tsx
Normal file
176
src/views/DNSSettings.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
import {DNSSettings, DNSSettingsToSave} from "../store/dns-settings/types";
|
||||
import {actions as nsGroupActions} from "../store/nameservers";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const DNSSettingsForm = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidatorEmptyStrings
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const dnsSettings = useSelector((state: RootState) => state.dnsSettings.dnsSettings)
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
const savedDNSSettings = useSelector((state: RootState) => state.dnsSettings.savedDNSSettings)
|
||||
const loading = useSelector((state: RootState) => state.dnsSettings.loading);
|
||||
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(dnsSettingsActions.getDNSSettings.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}, [dnsSettingsData])
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(dnsSettings)
|
||||
}, [dnsSettings])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedDNSSettings.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedDNSSettings.success) {
|
||||
message.success({
|
||||
content: 'DNS settings has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, success: false}));
|
||||
dispatch(dnsSettingsActions.resetSavedDNSSettings(null))
|
||||
} else if (savedDNSSettings.error) {
|
||||
let errorMsg = "Failed to update DNS settings"
|
||||
switch (savedDNSSettings.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update DNS settings. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedDNSSettings.error.data.message ? savedDNSSettings.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedDNSSettings])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let dnsSettingsToSave = createDNSSettingsToSave(values)
|
||||
dispatch(dnsSettingsActions.saveDNSSettings.request({
|
||||
getAccessTokenSilently:getAccessTokenSilently,
|
||||
payload: dnsSettingsToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createDNSSettingsToSave = (values: DNSSettings): DNSSettingsToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(values.disabled_management_groups)
|
||||
return {
|
||||
disabled_management_groups: existingGroups,
|
||||
groupsToCreate: newGroups
|
||||
} as DNSSettingsToSave
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>Manage your account's DNS settings</Paragraph>
|
||||
<Col>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"}
|
||||
style={{ display: 'flex' }}>
|
||||
<Card
|
||||
title="DNS Management"
|
||||
loading={loading}
|
||||
>
|
||||
<Form.Item
|
||||
label="Disable DNS management for these groups"
|
||||
name="disabled_management_groups"
|
||||
tooltip="Peers in these groups will have their DNS management disabled and require manual configuration for domain name resolution"
|
||||
rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Select.Option key={m}>{optionRender(m)}</Select.Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{ textAlign:'center' }} >
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DNSSettingsForm;
|
||||
114
src/views/ExpiresInInput.tsx
Normal file
114
src/views/ExpiresInInput.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import {Input, Select, Space} from 'antd';
|
||||
import React, {useState} from 'react';
|
||||
|
||||
export interface ExpiresInValue {
|
||||
number?: number;
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
key: string,
|
||||
title: string
|
||||
}
|
||||
|
||||
interface ExpiresInInputProps {
|
||||
value?: ExpiresInValue;
|
||||
onChange?: (value: ExpiresInValue) => void;
|
||||
options: SelectOption[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const secondsToExpiresIn = (expiresIn: number, availableOptions: string[]): ExpiresInValue => {
|
||||
|
||||
if (expiresIn == 0) {
|
||||
return {interval: "day", number: 0}
|
||||
}
|
||||
|
||||
let result = {interval: "hour", number: expiresIn / 3600}
|
||||
availableOptions.forEach(opt => {
|
||||
if (opt === "year" && (expiresIn % 31104000 === 0)) {
|
||||
result = {interval: "year", number: expiresIn / 31104000}
|
||||
} else if (opt === "month" && (expiresIn % 2592000 === 0)) {
|
||||
result = {interval: "month", number: expiresIn / 2592000}
|
||||
} else if (opt === "day" && (expiresIn % 86400 === 0)) {
|
||||
result = {interval: "day", number: expiresIn / 86400}
|
||||
} else if (opt === "hour" && (expiresIn % 3600 === 0)) {
|
||||
result = {interval: "hour", number: expiresIn / 3600}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export const expiresInToSeconds = (expiresIn: ExpiresInValue): number => {
|
||||
if (!expiresIn.number || !expiresIn.interval) {
|
||||
return 0
|
||||
}
|
||||
let multiplier = 0
|
||||
switch (expiresIn.interval.toLowerCase()) {
|
||||
case "hour":
|
||||
multiplier = 3600
|
||||
break
|
||||
case "day":
|
||||
multiplier = 24 * 3600
|
||||
break
|
||||
case "week":
|
||||
multiplier = 7 * 24 * 3600
|
||||
break
|
||||
case "month":
|
||||
multiplier = 30 * 24 * 3600
|
||||
break
|
||||
case "year":
|
||||
multiplier = 365 * 24 * 3600
|
||||
break
|
||||
default:
|
||||
multiplier = 0
|
||||
}
|
||||
|
||||
return expiresIn.number * multiplier
|
||||
|
||||
}
|
||||
|
||||
const ExpiresInInput: React.FC<ExpiresInInputProps> = ({
|
||||
value = {},
|
||||
onChange,
|
||||
options,
|
||||
disabled= false,
|
||||
}) => {
|
||||
const [number, setNumber] = useState(60);
|
||||
const [interval, setInterval] = useState("day");
|
||||
|
||||
const triggerChange = (changedValue: { number?: number; interval?: string }) => {
|
||||
onChange?.({number, interval, ...value, ...changedValue});
|
||||
};
|
||||
|
||||
const onNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newNumber = parseInt(e.target.value || '0', 10);
|
||||
setNumber(newNumber);
|
||||
triggerChange({number: newNumber});
|
||||
};
|
||||
|
||||
const onIntervalChange = (newInterval: string) => {
|
||||
setInterval(newInterval);
|
||||
triggerChange({interval: newInterval});
|
||||
};
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Input
|
||||
type="number"
|
||||
value={value.number || number}
|
||||
onChange={onNumberChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Select style={{width: "100%"}}
|
||||
value={value?.interval || interval}
|
||||
disabled={disabled}
|
||||
onChange={onIntervalChange}>
|
||||
{options.map(m =>
|
||||
<Select.Option key={m.key}>{m.title}</Select.Option>)}
|
||||
</Select>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpiresInInput;
|
||||
469
src/views/Nameservers.tsx
Normal file
469
src/views/Nameservers.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as nsGroupActions} from '../store/nameservers';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {NameServer, NameServerGroup} from "../store/nameservers/types";
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
interface NameserverGroupDataTable extends NameServerGroup {
|
||||
key: string
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const Nameservers = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.data);
|
||||
const failed = useSelector((state: RootState) => state.nameserverGroup.failed);
|
||||
const loading = useSelector((state: RootState) => state.nameserverGroup.loading);
|
||||
const updateNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
const savedNSGroup = useSelector((state: RootState) => state.nameserverGroup.savedNameServerGroup)
|
||||
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
const [nsGroupToAction, setNsGroupToAction] = useState(null as NameserverGroupDataTable | null);
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [dataTable, setDataTable] = useState([] as NameserverGroupDataTable[]);
|
||||
const [showTutorial, setShowTutorial] = useState(false)
|
||||
|
||||
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (nsGroup: NameServerGroup) => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroup.id,
|
||||
name: nsGroup.name,
|
||||
primary: nsGroup.primary,
|
||||
domains: nsGroup.domains,
|
||||
description: nsGroup.description,
|
||||
nameservers: nsGroup.nameservers,
|
||||
groups: nsGroup.groups,
|
||||
enabled: nsGroup.enabled,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const transformDataTable = (d: NameServerGroup[]): NameserverGroupDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as NameserverGroupDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nsGroupActions.getNameServerGroups.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (nsGroup.length > 0) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [nsGroup])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const filterDataTable = (): NameServerGroup[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f = filter(nsGroup, (f: NameServerGroup) =>
|
||||
((f.name).toLowerCase().includes(t) ||
|
||||
f.name.includes(t) || t === "" ||
|
||||
getGroupNamesFromIDs(f.groups).find(u => u.toLowerCase().trim().includes(t)) ||
|
||||
f.domains.find(d => d.toLowerCase().trim().includes(t)) ||
|
||||
f.nameservers.find(n => n.ip.includes(t)))
|
||||
) as NameServerGroup[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f) => f.enabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroupToAction?.id,
|
||||
name: nsGroupToAction?.name,
|
||||
primary: nsGroupToAction?.primary,
|
||||
domains: nsGroupToAction?.domains,
|
||||
description: nsGroupToAction?.description,
|
||||
groups: nsGroupToAction?.groups,
|
||||
enabled: nsGroupToAction?.enabled,
|
||||
nameservers: nsGroupToAction?.nameservers,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{nsGroupToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete Nameserver group "{nsGroupToAction ? nsGroupToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete this nameserver group from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(nsGroupActions.deleteNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: nsGroupToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setNsGroupToAction(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPopoverDomains = (_: string, inputDomains: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
var domains = [] as string[]
|
||||
if (inputDomains?.length) {
|
||||
domains = inputDomains
|
||||
}
|
||||
|
||||
let btn = <Button type="link"
|
||||
onClick={() => setUserAndView(userToAction)}>{domains.length ? domains.length : 0}</Button>
|
||||
if (!domains || domains!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = domains?.map((d, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{d}</strong>
|
||||
</Tag>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
}
|
||||
}, [updateNameServerGroupVisible])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedNSGroup.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedNSGroup.success) {
|
||||
message.success({
|
||||
content: 'Nameserver has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, success: false}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
} else if (savedNSGroup.error) {
|
||||
let errorMsg = "Failed to update nameserver group"
|
||||
switch (savedNSGroup.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update nameserver group. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedNSGroup.error.data.message ? savedNSGroup.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedNSGroup])
|
||||
|
||||
const onPopoverVisibleChange = () => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
},
|
||||
]
|
||||
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const onClickAddNewNSGroup = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
enabled: true,
|
||||
primary: true,
|
||||
} as NameServerGroup))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>Add nameservers for domain name resolution in your NetBird network</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Radio.Group
|
||||
options={optionsAllEnabled}
|
||||
onChange={onChangeAllEnabled}
|
||||
value={optionAllEnable}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
{!showTutorial &&
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)
|
||||
}}
|
||||
// className="card-table"
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record) => {
|
||||
return <Button type="text"
|
||||
onClick={() => setUserAndView(record as NameserverGroupDataTable)}
|
||||
className="tooltip-label">{(text && text.trim() !== "") ? text : (record as NameServerGroup).id}</Button>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> :
|
||||
<Tag color="red">disabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Nameservers" dataIndex="nameservers" align="center"
|
||||
render={(nameservers: NameServer[]) => (
|
||||
<>
|
||||
{nameservers.map(nameserver => (
|
||||
<Tag key={nameserver.ip}>
|
||||
{nameserver.ip}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Column title="All domains" dataIndex="primary" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="blue">yes</Tag> :
|
||||
<Tag>no</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Match domains" dataIndex="domains" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverDomains(text, record.domains, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center" width="30px"
|
||||
render={(text, record) => {
|
||||
return (
|
||||
<Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setNsGroupToAction(record as NameserverGroupDataTable)
|
||||
}}></Dropdown.Button>)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px', justifyContent: 'center'}}>
|
||||
<Paragraph type="secondary"
|
||||
style={{textAlign: "center", whiteSpace: "pre-line"}}>
|
||||
It looks like you don't have any nameservers. {"\n"}
|
||||
Get started by adding one to your network!
|
||||
</Paragraph>
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
</Space>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nameservers;
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {capitalize, formatOS, timeAgo} from "../utils/common";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as peerActions} from '../store/peer';
|
||||
import {actions as groupActions} from '../store/group';
|
||||
import {actions as routeActions} from '../store/route';
|
||||
import {Container} from "../components/Container";
|
||||
import { useOidcAccessToken } from '@axa-fr/react-oidc';
|
||||
import {ExclamationCircleOutlined, ReloadOutlined,} from '@ant-design/icons';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
List,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
@@ -25,35 +27,35 @@ import {
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {Peer} from "../store/peer/types";
|
||||
import {Peer, PeerDataTable} from "../store/peer/types";
|
||||
import {filter} from "lodash"
|
||||
import {formatOS, timeAgo} from "../utils/common";
|
||||
import Icon, {ExclamationCircleOutlined, QuestionCircleOutlined, WarningOutlined} from "@ant-design/icons";
|
||||
import ButtonCopyMessage from "../components/ButtonCopyMessage";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import PeerUpdate from "../components/PeerUpdate";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import ButtonCopyMessage from "../components/ButtonCopyMessage";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import AddPeerPopup from "../components/addpeer/AddPeerPopup";
|
||||
import {getLocalItem, setLocalItem, StorageKey} from "../services/local";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Column } = Table;
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface PeerDataTable extends Peer {
|
||||
key: string;
|
||||
groups: Group[];
|
||||
groupsCount: number;
|
||||
}
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
export const Peers = () => {
|
||||
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const {onChangePageSize, pageSizeOptions, pageSize} = usePageSizeHelpers()
|
||||
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const peers = useSelector((state: RootState) => state.peer.data);
|
||||
const routes = useSelector((state: RootState) => state.route.data);
|
||||
const failed = useSelector((state: RootState) => state.peer.failed);
|
||||
const loading = useSelector((state: RootState) => state.peer.loading);
|
||||
const deletedPeer = useSelector((state: RootState) => state.peer.deletedPeer);
|
||||
@@ -61,38 +63,37 @@ export const Peers = () => {
|
||||
const loadingGroups = useSelector((state: RootState) => state.group.loading);
|
||||
const savedGroups = useSelector((state: RootState) => state.peer.savedGroups);
|
||||
const updatedPeer = useSelector((state: RootState) => state.peer.updatedPeer);
|
||||
const updateGroupsVisible = useSelector((state: RootState) => state.peer.updateGroupsVisible)
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const [addPeerModalOpen, setAddPeerModalOpen] = useState(false);
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionOnOff, setOptionOnOff] = useState('all');
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [dataTable, setDataTable] = useState([] as PeerDataTable[]);
|
||||
const [peerToAction, setPeerToAction] = useState(null as PeerDataTable | null);
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
const [showTutorial, setShowTutorial] = useState(false)
|
||||
const [hadFirstRun, setHadFirstRun] = useState(true)
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
|
||||
const optionsOnOff = [{label: 'Online', value: 'on'},{label: 'All', value: 'all'}]
|
||||
const optionsOnOff = [{label: 'Online', value: 'on'}, {label: 'All', value: 'all'}]
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "view",
|
||||
label: (<Button type="text" block onClick={() => onClickViewRule()}>View</Button>)
|
||||
label: (<Button type="text" block onClick={() => onClickViewPeer()}>View</Button>)
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
}
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const transformDataTable = (d:Peer[]):PeerDataTable[] => {
|
||||
const peer_ids = d.map(_p => _p.id)
|
||||
const transformDataTable = (d: Peer[]): PeerDataTable[] => {
|
||||
return d.map((p) => {
|
||||
const gs = groups
|
||||
.filter(g => g.peers?.find((_p:GroupPeer) => _p.id === p.id))
|
||||
.filter(g => g.peers?.find((_p: GroupPeer) => _p.id === p.id))
|
||||
.map(g => ({id: g.id, name: g.name, peers_count: g.peers?.length, peers: g.peers || []}))
|
||||
return {
|
||||
key: p.id,
|
||||
@@ -103,12 +104,36 @@ export const Peers = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently:accessToken, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently:accessToken, payload: null}));
|
||||
getLocalItem<boolean>(StorageKey.hadFirstRun).then(f => setHadFirstRun(f === null? false : f))
|
||||
refresh()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hadFirstRun) {
|
||||
setLocalItem(StorageKey.hadFirstRun, true).then()
|
||||
setAddPeerModalOpen(true)
|
||||
} else {
|
||||
setAddPeerModalOpen(false)
|
||||
}
|
||||
}, [hadFirstRun])
|
||||
|
||||
useEffect(() => {
|
||||
if (peers.length) {
|
||||
setShowTutorial(false)
|
||||
if (!hadFirstRun) {
|
||||
setHadFirstRun(true)
|
||||
}
|
||||
} else {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
setDataTable(transformDataTable(peers))
|
||||
}, [peers, groups])
|
||||
|
||||
@@ -118,60 +143,95 @@ export const Peers = () => {
|
||||
|
||||
const deleteKey = 'deleting';
|
||||
useEffect(() => {
|
||||
const style = { marginTop: 85 }
|
||||
const style = {marginTop: 85}
|
||||
if (deletedPeer.loading) {
|
||||
message.loading({ content: 'Deleting...', key: deleteKey, style });
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style});
|
||||
} else if (deletedPeer.success) {
|
||||
message.success({ content: 'Peer has been successfully removed.', key: deleteKey, duration: 2, style });
|
||||
message.success({content: 'Peer has been successfully removed.', key: deleteKey, duration: 2, style});
|
||||
dispatch(peerActions.resetDeletedPeer(null))
|
||||
} else if (deletedPeer.error) {
|
||||
message.error({ content: 'Failed to delete peer. You might not have enough permissions.', key: deleteKey, duration: 2, style });
|
||||
message.error({
|
||||
content: 'Failed to delete peer. You might not have enough permissions.',
|
||||
key: deleteKey,
|
||||
duration: 2,
|
||||
style
|
||||
});
|
||||
dispatch(peerActions.resetDeletedPeer(null))
|
||||
}
|
||||
}, [deletedPeer])
|
||||
|
||||
const saveGroupsKey = 'saving_groups';
|
||||
useEffect(() => {
|
||||
const style = { marginTop: 85 }
|
||||
const style = {marginTop: 85}
|
||||
if (savedGroups.loading) {
|
||||
message.loading({ content: 'Updating peer groups...', key: saveGroupsKey, style });
|
||||
message.loading({content: 'Updating peer groups...', key: saveGroupsKey, style});
|
||||
} else if (savedGroups.success) {
|
||||
message.success({ content: 'Peer groups have been successfully updated.', key: saveGroupsKey, duration: 2, style });
|
||||
message.success({
|
||||
content: 'Peer groups have been successfully updated.',
|
||||
key: saveGroupsKey,
|
||||
duration: 2,
|
||||
style
|
||||
});
|
||||
// setUpdateGroupsVisible({} as Peer, false)
|
||||
dispatch(peerActions.resetSavedGroups(null))
|
||||
} else if (savedGroups.error) {
|
||||
message.error({ content: 'Failed to update peer groups. You might not have enough permissions.', key: saveGroupsKey, duration: 2, style });
|
||||
message.error({
|
||||
content: 'Failed to update peer groups. You might not have enough permissions.',
|
||||
key: saveGroupsKey,
|
||||
duration: 2,
|
||||
style
|
||||
});
|
||||
dispatch(peerActions.resetSavedGroups(null))
|
||||
}
|
||||
}, [savedGroups])
|
||||
|
||||
const updatePeerKey = 'updating_peer';
|
||||
useEffect(() => {
|
||||
const style = { marginTop: 85 }
|
||||
const style = {marginTop: 85}
|
||||
if (updatedPeer.loading) {
|
||||
message.loading({ content: 'Updating peer...', key: updatePeerKey, duration: 0, style })
|
||||
message.loading({content: 'Updating peer...', key: updatePeerKey, duration: 0, style})
|
||||
} else if (updatedPeer.success) {
|
||||
message.success({ content: 'Peer has been successfully updated.', key: updatePeerKey, duration: 2, style });
|
||||
dispatch(peerActions.setUpdatedPeer({ ...updatedPeer, success: false }))
|
||||
message.success({content: 'Peer has been successfully updated.', key: updatePeerKey, duration: 2, style});
|
||||
dispatch(peerActions.setUpdatedPeer({...updatedPeer, success: false}))
|
||||
dispatch(peerActions.resetUpdatedPeer(null))
|
||||
} else if (updatedPeer.error) {
|
||||
message.error({ content: 'Failed to update peer. You might not have enough permissions.', key: updatePeerKey, duration: 2, style });
|
||||
dispatch(peerActions.setUpdatedPeer({ ...updatedPeer, error: null }))
|
||||
let msg = updatedPeer.error.data ? capitalize(updatedPeer.error.data.message) : updatedPeer.error.message
|
||||
message.error({
|
||||
content: msg,
|
||||
key: updatePeerKey,
|
||||
duration: 3,
|
||||
style
|
||||
});
|
||||
dispatch(peerActions.setUpdatedPeer({...updatedPeer, error: null}))
|
||||
dispatch(peerActions.resetUpdatedPeer(null))
|
||||
}
|
||||
}, [updatedPeer])
|
||||
|
||||
const filterDataTable = ():Peer[] => {
|
||||
const filterDataTable = (): Peer[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f:Peer[] = filter(peers, (f:Peer) =>
|
||||
(f.name.toLowerCase().includes(t) || f.ip.includes(t) || f.os.includes(t) || t === "")
|
||||
) as Peer[]
|
||||
let f: Peer[] = filter(peers, (f: Peer) => {
|
||||
let userEmail: string | null
|
||||
const u = users?.find(u => u.id === f.user_id)?.email
|
||||
userEmail = u ? u : ""
|
||||
return (f.name.toLowerCase().includes(t) || f.ip.includes(t) || f.os.includes(t) || t === "" ||
|
||||
f.groups?.find(u => u.name.toLowerCase().trim().includes(t)) ||
|
||||
(userEmail && userEmail.toLowerCase().includes(t)))
|
||||
}
|
||||
) as Peer[]
|
||||
if (optionOnOff === "on") {
|
||||
f = filter(peers, (f:Peer) => f.connected)
|
||||
f = filter(peers, (f: Peer) => f.connected)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
const getGroupNamesFromIDs = (groupIDList: string[] | undefined): string[] => {
|
||||
if (!groupIDList) {
|
||||
return []
|
||||
}
|
||||
|
||||
return groups?.filter(g => groupIDList.includes(g.id!)).map(g => g.name || '') || []
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
@@ -181,24 +241,65 @@ export const Peers = () => {
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onChangeOnOff = ({ target: { value } }: RadioChangeEvent) => {
|
||||
const onChangeOnOff = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionOnOff(value)
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
let peerRoutes: string[] = []
|
||||
routes.forEach((r) => {
|
||||
if (r.peer == peerToAction?.id) {
|
||||
peerRoutes.push(r.network_id)
|
||||
}
|
||||
})
|
||||
|
||||
let content = <Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
|
||||
let contentModule = <div>{content}</div>
|
||||
if (peerRoutes.length) {
|
||||
let contentWithRoutes =
|
||||
"Removing this peer will disable the following routes: " + peerRoutes
|
||||
let B = <Alert
|
||||
message={contentWithRoutes}
|
||||
type="warning"
|
||||
showIcon
|
||||
closable={false}
|
||||
/>
|
||||
|
||||
contentModule = <div>
|
||||
{content}
|
||||
<Paragraph>
|
||||
<Alert
|
||||
message={
|
||||
<div>
|
||||
<>This peer is part of one or more network routes. Removing this peer will disable the
|
||||
following routes:
|
||||
</>
|
||||
<List
|
||||
dataSource={peerRoutes}
|
||||
renderItem={item => <List.Item><Text strong>- {item}</Text></List.Item>}
|
||||
bordered={false}
|
||||
split={false}
|
||||
itemLayout={"vertical"}
|
||||
/>
|
||||
</div>}
|
||||
type="warning"
|
||||
showIcon={false}
|
||||
closable={false}
|
||||
/>
|
||||
</Paragraph>
|
||||
</div>
|
||||
}
|
||||
let name = peerToAction ? peerToAction.name : ''
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete peer \"" + name + "\"",
|
||||
width: 600,
|
||||
content: "Are you sure you want to delete peer from your account?",
|
||||
okType: 'danger',
|
||||
content: contentModule,
|
||||
onOk() {
|
||||
dispatch(peerActions.deletedPeer.request({getAccessTokenSilently:accessToken, payload: peerToAction ? peerToAction.ip : ''}));
|
||||
dispatch(peerActions.deletedPeer.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: (peerToAction && peerToAction.id) ? peerToAction.id! : ""
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setPeerToAction(null);
|
||||
@@ -207,12 +308,11 @@ export const Peers = () => {
|
||||
}
|
||||
|
||||
const showConfirmEnableSSH = (record: PeerDataTable) => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Enable SSH Server for \"" + record.name + "\"?",
|
||||
width: 600,
|
||||
content: "Experimental feature. Enabling this option allows remote SSH access to this machine from other connected network participants.",
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
handleSwitchSSH(record, true)
|
||||
},
|
||||
@@ -220,22 +320,37 @@ export const Peers = () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleSwitchSSH(record: PeerDataTable, checked: boolean) {
|
||||
const peer = {
|
||||
id: record.id,
|
||||
ssh_enabled: checked,
|
||||
name: record.name
|
||||
} as Peer
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently:accessToken, payload: peer}));
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently: getAccessTokenSilently, payload: peer}));
|
||||
|
||||
}
|
||||
|
||||
const onClickViewRule = () => {
|
||||
const onClickViewPeer = () => {
|
||||
dispatch(peerActions.setUpdateGroupsVisible(true))
|
||||
dispatch(peerActions.setPeer(peerToAction as Peer))
|
||||
}
|
||||
|
||||
const setUpdateGroupsVisible = (peerToAction:Peer, status:boolean) => {
|
||||
useEffect(() => {
|
||||
if (updateGroupsVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
}
|
||||
}, [updateGroupsVisible])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean) => {
|
||||
if (updateGroupsVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const setUpdateGroupsVisible = (peerToAction: Peer, status: boolean) => {
|
||||
if (status) {
|
||||
dispatch(peerActions.setPeer({...peerToAction}))
|
||||
dispatch(peerActions.setUpdateGroupsVisible(true))
|
||||
@@ -245,15 +360,15 @@ export const Peers = () => {
|
||||
dispatch(peerActions.setUpdateGroupsVisible(false))
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, groups:Group[] | string[] | null, peerToAction:PeerDataTable) => {
|
||||
const content = groups?.map((g,i) => {
|
||||
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, peerToAction: PeerDataTable) => {
|
||||
const content = groups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
@@ -268,24 +383,84 @@ export const Peers = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement} key={peerToAction.key} content={mainContent} title={null}>
|
||||
<Popover placement={popoverPlacement as TooltipPlacement} key={peerToAction.key} content={mainContent}
|
||||
onOpenChange={onPopoverVisibleChange} open={groupPopupVisible}
|
||||
title={null}>
|
||||
<Button type="link" onClick={() => setUpdateGroupsVisible(peerToAction, true)}>{label}</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const renderAddress = (peer: PeerDataTable) => {
|
||||
if (!peer.dns_label) {
|
||||
return <ButtonCopyMessage keyMessage={peer.key}
|
||||
toCopy={peer.ip}
|
||||
body={peer.ip}
|
||||
messageText={'IP copied'}
|
||||
styleNotification={{}}/>
|
||||
}
|
||||
|
||||
const body = <span style={{textAlign: "left"}}>
|
||||
<Row>
|
||||
<ButtonCopyMessage keyMessage={peer.dns_label}
|
||||
toCopy={peer.dns_label}
|
||||
body={peer.dns_label}
|
||||
messageText={'Peer domain copied'}
|
||||
styleNotification={{}}/>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<ButtonCopyMessage keyMessage={peer.ip}
|
||||
toCopy={peer.ip}
|
||||
body={<Text type="secondary">{peer.ip}</Text>}
|
||||
messageText={'Peer IP copied'}
|
||||
style={{marginTop: '-10px'}}
|
||||
styleNotification={{}}/>
|
||||
</Row>
|
||||
</span>
|
||||
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
const renderName = (peer: PeerDataTable) => {
|
||||
const userEmail = users?.find(u => u.id === peer.user_id)?.email
|
||||
let expiry =!peer.login_expiration_enabled ? <div><Tag><Text type="secondary" style={{fontSize: 10}}>expiration disabled</Text></Tag></div> : null
|
||||
if (!userEmail) {
|
||||
return <Button type="text" style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}
|
||||
onClick={() => setUpdateGroupsVisible(peer, true)}>
|
||||
<span style={{textAlign: "left"}}>
|
||||
<Row><Text strong>{peer.name}</Text></Row>
|
||||
</span>
|
||||
</Button>
|
||||
}
|
||||
return <div>
|
||||
<Button type="text"
|
||||
onClick={() => setUpdateGroupsVisible(peer, true)}>
|
||||
<span style={{textAlign: "left"}}>
|
||||
<Row> <Text strong>{peer.name}</Text></Row>
|
||||
<Row><Text type="secondary">{userEmail}</Text></Row>
|
||||
<Row> {expiry}</Row>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Peers</Title>
|
||||
<Paragraph>A list of all the machines in your account including their name, IP and status.</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
|
||||
{showTutorial && <Paragraph type={"secondary"}>A list of all the machines in your account including their name, IP and
|
||||
status.</Paragraph>}
|
||||
{!showTutorial && <Paragraph>A list of all the machines in your account including their name, IP and
|
||||
status.</Paragraph>}
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
{/*<Input.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
@@ -295,8 +470,11 @@ export const Peers = () => {
|
||||
value={optionOnOff}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
disabled={showTutorial}
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
disabled={showTutorial}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
@@ -307,19 +485,24 @@ export const Peers = () => {
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add Peer</Link>
|
||||
{!showTutorial && <Button type="primary" onClick={() => setAddPeerModalOpen(true)}>Add peer</Button>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
|
||||
type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
{/*{loading && <Loading/>}*/}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)}}
|
||||
className="card-table"
|
||||
{!showTutorial && (<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)
|
||||
}}
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
@@ -327,62 +510,79 @@ export const Peers = () => {
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
defaultSortOrder='ascend'
|
||||
align="left"
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
render={(text:string, record:PeerDataTable,) => {
|
||||
return <Button type="text" onClick={() => setUpdateGroupsVisible(record, true)}>{text}</Button>
|
||||
render={(text: string, record: PeerDataTable,) => {
|
||||
return renderName(record)
|
||||
}}
|
||||
/>
|
||||
<Column title="IP" dataIndex="ip"
|
||||
<Column title="Address" dataIndex="ip"
|
||||
sorter={(a, b) => {
|
||||
const _a = (a as any).ip.split('.')
|
||||
const _b = (b as any).ip.split('.')
|
||||
const a_s = _a.map((i:any) => i.padStart(3, '0')).join()
|
||||
const b_s = _b.map((i:any) => i.padStart(3, '0')).join()
|
||||
const a_s = _a.map((i: any) => i.padStart(3, '0')).join()
|
||||
const b_s = _b.map((i: any) => i.padStart(3, '0')).join()
|
||||
return a_s.localeCompare(b_s)
|
||||
}}
|
||||
render={(text, record, index) => {
|
||||
return <ButtonCopyMessage keyMessage={(record as PeerDataTable).key} text={text} messageText={'IP copied!'} styleNotification={{}}/>
|
||||
render={(text: string, record: PeerDataTable, index: number) => {
|
||||
return renderAddress(record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="connected" align="center"
|
||||
render={(text, record, index) => {
|
||||
return text ? <Tag color="green">online</Tag> : <Tag color="red">offline</Tag>
|
||||
render={(text, record: PeerDataTable, index) => {
|
||||
|
||||
let status = text ? <Tag color="green">online</Tag> :
|
||||
<Tag color="red">offline</Tag>
|
||||
|
||||
if (record.login_expired) {
|
||||
return <Tooltip
|
||||
title="The peer is offline and needs to be re-authenticated because its login has expired ">
|
||||
<Tag color="orange">needs login</Tag>
|
||||
</Tooltip>
|
||||
|
||||
}
|
||||
|
||||
return status
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record:PeerDataTable, index) => {
|
||||
render={(text, record: PeerDataTable, index) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="SSH Server" dataIndex="ssh_enabled" align="center"
|
||||
render={(e, record:PeerDataTable, index) => {
|
||||
let isWindows = record.os.toLocaleLowerCase().startsWith("windows")
|
||||
let toggle = <Switch size={"small"} checked={e}
|
||||
disabled={isWindows}
|
||||
onClick={(checked: boolean) => {
|
||||
if (checked) {
|
||||
showConfirmEnableSSH(record)
|
||||
} else {
|
||||
handleSwitchSSH(record, checked)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
render={(e, record: PeerDataTable, index) => {
|
||||
let isWindows = record.os.toLocaleLowerCase().startsWith("windows")
|
||||
let toggle = <Switch size={"small"} checked={e}
|
||||
disabled={isWindows}
|
||||
onClick={(checked: boolean) => {
|
||||
if (checked) {
|
||||
showConfirmEnableSSH(record)
|
||||
} else {
|
||||
handleSwitchSSH(record, checked)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
if (isWindows) {
|
||||
return <Tooltip title="SSH server feature is not yet supported on Windows">
|
||||
{toggle}
|
||||
</Tooltip>
|
||||
} else {
|
||||
return toggle
|
||||
}
|
||||
if (isWindows) {
|
||||
return <Tooltip
|
||||
title="SSH server feature is not yet supported on Windows">
|
||||
{toggle}
|
||||
</Tooltip>
|
||||
} else {
|
||||
return toggle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
<Column title="LastSeen" dataIndex="last_seen"
|
||||
render={(text, record, index) => {
|
||||
return (record as PeerDataTable).connected ? 'just now' : timeAgo(text)
|
||||
let dt = new Date(text)
|
||||
return <Popover content={dt.toLocaleString()}>
|
||||
{(record as PeerDataTable).connected ? 'just now' : timeAgo(text)}
|
||||
</Popover>
|
||||
}}
|
||||
/>
|
||||
<Column title="OS" dataIndex="os"
|
||||
@@ -390,22 +590,54 @@ export const Peers = () => {
|
||||
return formatOS(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Version" dataIndex="version" />
|
||||
<Column title="Version" dataIndex="version"/>
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setPeerToAction(record as PeerDataTable)
|
||||
}}></Dropdown.Button>
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Table>)}
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px', justifyContent: 'center'}}>
|
||||
<Title level={4}
|
||||
style={{textAlign: "center"}}>
|
||||
Get Started
|
||||
</Title>
|
||||
<Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line"}}>
|
||||
It looks like you don't have any connected machines. {"\n"}
|
||||
Get started by adding one to your network.
|
||||
</Paragraph>
|
||||
<Button size={"middle"} type="primary" onClick={() => setAddPeerModalOpen(true)}>
|
||||
Add new peer
|
||||
</Button>
|
||||
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<PeerUpdate/>
|
||||
<Modal
|
||||
open={addPeerModalOpen}
|
||||
onOk={() => setAddPeerModalOpen(false)}
|
||||
onCancel={() => {
|
||||
setAddPeerModalOpen(false)
|
||||
setHadFirstRun(true)
|
||||
}}
|
||||
footer={[]}
|
||||
width={780}
|
||||
>
|
||||
<AddPeerPopup greeting={!hadFirstRun ? "Hi there!" : ""} headline={!hadFirstRun ? "It's time to add your first device." : "Add new peer"}/>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
561
src/views/Routes.tsx
Normal file
561
src/views/Routes.tsx
Normal file
@@ -0,0 +1,561 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal, Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {Container} from "../components/Container";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Route, RouteToSave} from "../store/route/types";
|
||||
import {actions as routeActions} from "../store/route";
|
||||
import {actions as peerActions} from "../store/peer";
|
||||
import {filter, sortBy} from "lodash";
|
||||
import {ExclamationCircleOutlined, QuestionCircleOutlined} from "@ant-design/icons";
|
||||
import RouteUpdate from "../components/RouteUpdate";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {
|
||||
GroupedDataTable,
|
||||
initPeerMaps,
|
||||
masqueradeDisabledMSG,
|
||||
masqueradeEnabledMSG,
|
||||
peerToPeerIP,
|
||||
RouteDataTable,
|
||||
transformDataTable,
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
export const Routes = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const routes = useSelector((state: RootState) => state.route.data);
|
||||
const failed = useSelector((state: RootState) => state.route.failed);
|
||||
const loading = useSelector((state: RootState) => state.route.loading);
|
||||
const deletedRoute = useSelector((state: RootState) => state.route.deletedRoute);
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute);
|
||||
const peers = useSelector((state: RootState) => state.peer.data)
|
||||
const loadingPeer = useSelector((state: RootState) => state.peer.loading);
|
||||
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
|
||||
const [showTutorial, setShowTutorial] = useState(true)
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [dataTable, setDataTable] = useState([] as RouteDataTable[]);
|
||||
const [routeToAction, setRouteToAction] = useState(null as RouteDataTable | null);
|
||||
const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
|
||||
const [expandRowsOnClick, setExpandRowsOnClick] = useState(true)
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
|
||||
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "view",
|
||||
label: (<Button type="text" block onClick={() => onClickViewRoute()}>View</Button>)
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" block onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
}
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const isShowTutorial = (routes: Route[]): boolean => {
|
||||
return (!routes.length || (routes.length === 1 && routes[0].network === "Default"))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [peers])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
const filterGroupedDataTable = (routes: GroupedDataTable[]): GroupedDataTable[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: GroupedDataTable[] = filter(routes, (f) =>
|
||||
(f.network_id.toLowerCase().includes(t) || f.network.toLowerCase().includes(t) ||
|
||||
f.description.toLowerCase().includes(t) || t === "" ||
|
||||
getGroupNamesFromIDs(f.routesGroups).find(u => u.toLowerCase().trim().includes(t)) )
|
||||
) as GroupedDataTable[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f) => f.enabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}, [dataTable])
|
||||
|
||||
useEffect(() => {
|
||||
if (failed) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(isShowTutorial(routes))
|
||||
setDataTable(sortBy(transformDataTable(routes, peers), "network_id"))
|
||||
}
|
||||
}, [routes])
|
||||
|
||||
useEffect(() => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
const saveKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedRoute.loading) {
|
||||
message.loading({content: 'Saving...', key: saveKey, duration: 0, style: styleNotification})
|
||||
} else if (savedRoute.success) {
|
||||
message.success({
|
||||
content: 'Route has been successfully updated.',
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(routeActions.setSetupNewRouteVisible(false))
|
||||
dispatch(routeActions.setSavedRoute({...savedRoute, success: false}))
|
||||
dispatch(routeActions.resetSavedRoute(null))
|
||||
} else if (savedRoute.error) {
|
||||
let errorMsg = "Failed to update network route"
|
||||
switch (savedRoute.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update network route. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedRoute.error.data.message ? savedRoute.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: saveKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(routeActions.setSavedRoute({...savedRoute, error: null}))
|
||||
dispatch(routeActions.resetSavedRoute(null))
|
||||
}
|
||||
}, [savedRoute])
|
||||
|
||||
const deleteKey = 'deleting';
|
||||
useEffect(() => {
|
||||
const style = {marginTop: 85}
|
||||
if (deletedRoute.loading) {
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style})
|
||||
} else if (deletedRoute.success) {
|
||||
message.success({content: 'Route has been successfully deleted.', key: deleteKey, duration: 2, style})
|
||||
dispatch(routeActions.resetDeletedRoute(null))
|
||||
} else if (deletedRoute.error) {
|
||||
message.error({
|
||||
content: 'Failed to remove route. You might not have enough permissions.',
|
||||
key: deleteKey,
|
||||
duration: 2,
|
||||
style
|
||||
})
|
||||
dispatch(routeActions.resetDeletedRoute(null))
|
||||
}
|
||||
}, [deletedRoute])
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{routeToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete netowork route "{routeToAction ? routeToAction.network_id : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete this route from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(routeActions.deleteRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: routeToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setRouteToAction(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const onClickAddNewRoute = () => {
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
dispatch(routeActions.setRoute({
|
||||
network: '',
|
||||
network_id: '',
|
||||
description: '',
|
||||
peer: '',
|
||||
masquerade: true,
|
||||
metric: 9999,
|
||||
enabled: true
|
||||
} as Route))
|
||||
}
|
||||
|
||||
const onClickViewRoute = () => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(false));
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
dispatch(routeActions.setRoute({
|
||||
id: routeToAction?.id || null,
|
||||
network: routeToAction?.network,
|
||||
network_id: routeToAction?.network_id,
|
||||
description: routeToAction?.description,
|
||||
peer: peerToPeerIP(routeToAction!.peer, peerNameToIP[routeToAction!.peer]),
|
||||
metric: routeToAction?.metric,
|
||||
masquerade: routeToAction?.masquerade,
|
||||
enabled: routeToAction?.enabled
|
||||
} as Route))
|
||||
}
|
||||
|
||||
const setRouteAndView = (route: RouteDataTable) => {
|
||||
if (!route.id) {
|
||||
dispatch(routeActions.setSetupNewRouteHA(true));
|
||||
}
|
||||
dispatch(routeActions.setRoute({
|
||||
id: route.id || null,
|
||||
network: route.network,
|
||||
network_id: route.network_id,
|
||||
description: route.description,
|
||||
peer: route.peer ? peerToPeerIP(route.peer_name, route.peer_ip) : '',
|
||||
metric: route.metric ? route.metric : 9999,
|
||||
masquerade: route.masquerade,
|
||||
enabled: route.enabled,
|
||||
groups: route.groups
|
||||
} as Route))
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
}
|
||||
|
||||
const showConfirmEnableMasquerade = (record: GroupedDataTable, checked: boolean) => {
|
||||
let label = record.network_id ? record.network_id : record.network
|
||||
let tittle = "Enable Masquerade for \"" + label + "\"?"
|
||||
let content = masqueradeDisabledMSG
|
||||
|
||||
if (!checked) {
|
||||
tittle = "Disable Masquerade for \"" + label + "\"?"
|
||||
content = masqueradeEnabledMSG
|
||||
}
|
||||
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: tittle,
|
||||
width: 600,
|
||||
content: content,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
handleSwitchMasquerade(record, checked)
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const onPopoverVisibleChange = () => {
|
||||
if (setupNewRouteVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwitchMasquerade(routeGroup: GroupedDataTable, checked: boolean) {
|
||||
routeGroup.groupedRoutes.forEach((record) => {
|
||||
const route = {
|
||||
...record,
|
||||
peer: peerNameToIP[record.peer],
|
||||
masquerade: checked,
|
||||
groupsToCreate: []
|
||||
} as RouteToSave
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently: getAccessTokenSilently, payload: route}));
|
||||
})
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: RouteDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setRouteAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const expandedRowRender = (record: GroupedDataTable) => {
|
||||
|
||||
return <Table
|
||||
dataSource={record.groupedRoutes}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
showHeader={true}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
bordered={true}
|
||||
>
|
||||
<Column title="Routing Peer" dataIndex="peer_name" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).peer.includes(value)}
|
||||
sorter={(a, b) => ((a as any).peer.localeCompare((b as any).peer))}
|
||||
render={(text, record) => {
|
||||
return <span onClick={() => setRouteAndView(record as RouteDataTable)}
|
||||
className="tooltip-label">{text}</span>
|
||||
}}
|
||||
/>
|
||||
<Column title="Metric" dataIndex="metric" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).metric.includes(value)}
|
||||
sorter={(a, b) => ((a as any).metric - ((b as any).metric))}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: RouteDataTable) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Routing peer status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center"
|
||||
render={(text, record) => {
|
||||
if (deletedRoute.loading || savedRoute.loading) return <></>
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setRouteToAction(record as RouteDataTable)
|
||||
}}></Dropdown.Button>
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className="container-main">
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Network Routes</Title>
|
||||
<Paragraph>Network routes allow you to create routes to access other networks without installing
|
||||
NetBird on every resource.</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Radio.Group
|
||||
options={optionsAllEnabled}
|
||||
onChange={onChangeAllEnabled}
|
||||
value={optionAllEnable}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" disabled={savedRoute.loading}
|
||||
onClick={onClickAddNewRoute}>Add Route</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
current: currentPage, hideOnSinglePage: showTutorial, disabled: showTutorial,
|
||||
pageSize, responsive: true, showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} routes`),
|
||||
onChange: (page) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
}}
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading || loadingPeer)}
|
||||
dataSource={groupedDataTable}
|
||||
expandable={{
|
||||
expandedRowRender,
|
||||
expandRowByClick: expandRowsOnClick,
|
||||
onExpandedRowsChange: (r) => {
|
||||
setExpandRowsOnClick((!r.length))
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Column title={() =>
|
||||
<span>
|
||||
Network Identifier
|
||||
<Tooltip
|
||||
title="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes">
|
||||
<QuestionCircleOutlined style={{marginLeft: '0.25em', color: "gray"}}/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
dataIndex="network_id"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
defaultSortOrder='ascend' align="center"
|
||||
sorter={(a, b) => ((a as any).network_id.localeCompare((b as any).network_id))}
|
||||
render={(text, record) => {
|
||||
const desc = (record as RouteDataTable).description.trim()
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"}
|
||||
arrowPointAtCenter>
|
||||
<Text strong>{text}</Text>
|
||||
</Tooltip>
|
||||
}}
|
||||
/>
|
||||
<Column title="Network Range" dataIndex="network" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).network.includes(value)}
|
||||
sorter={(a, b) => ((a as any).network.localeCompare((b as any).network))}
|
||||
// defaultSortOrder='ascend'
|
||||
/>
|
||||
<Column title="Route status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> :
|
||||
<Tag color="red">disabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Masquerade Traffic" dataIndex="masquerade" align="center"
|
||||
render={(e, record: GroupedDataTable) => {
|
||||
let toggle = <Switch size={"small"} checked={e}
|
||||
onClick={(checked: boolean) => {
|
||||
showConfirmEnableMasquerade(record, checked)
|
||||
}}
|
||||
/>
|
||||
return <Tooltip
|
||||
title="Hides the traffic with the routing peer address">
|
||||
{toggle}
|
||||
</Tooltip>
|
||||
}}
|
||||
/>
|
||||
<Column title="High Availability" align="center" dataIndex="routesCount"
|
||||
render={(count, record: RouteDataTable) => {
|
||||
let tag = <Tag color="red">off</Tag>
|
||||
if (count > 1) {
|
||||
tag = <Tag color="green">on</Tag>
|
||||
}
|
||||
return <div>{tag}<Divider type="vertical"/>
|
||||
<Button type="link" onClick={() => setRouteAndView(record)}>Configure</Button>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px'}}>
|
||||
<Button type="link" onClick={onClickAddNewRoute}>Add new route</Button>
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<RouteUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Routes;
|
||||
229
src/views/Settings.tsx
Normal file
229
src/views/Settings.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Button, Card, Col, Form, List, message, Modal, Radio, Row, Space, Typography,} from "antd";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {Container} from "../components/Container";
|
||||
import UserUpdate from "../components/UserUpdate";
|
||||
import ExpiresInInput, {expiresInToSeconds, secondsToExpiresIn} from "./ExpiresInInput";
|
||||
import {checkExpiresIn} from "../utils/common";
|
||||
import {actions as accountActions} from "../store/account";
|
||||
import {Account, FormAccount} from "../store/account/types";
|
||||
import {ExclamationCircleOutlined, QuestionCircleFilled} from "@ant-design/icons";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const Settings = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const accounts = useSelector((state: RootState) => state.account.data);
|
||||
const failed = useSelector((state: RootState) => state.account.failed);
|
||||
const loading = useSelector((state: RootState) => state.account.loading);
|
||||
const updatedAccount = useSelector((state: RootState) => state.account.updatedAccount);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const [formAccount, setFormAccount] = useState({} as FormAccount);
|
||||
const [accountToAction, setAccountToAction] = useState({} as FormAccount);
|
||||
const [formPeerExpirationEnabled, setFormPeerExpirationEnabled] = useState(true);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(accountActions.getAccounts.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (accounts.length < 1) {
|
||||
console.error("invalid account data returned from the Management API", accounts)
|
||||
return
|
||||
}
|
||||
let account = accounts[0]
|
||||
|
||||
let fAccount = {
|
||||
id: account.id,
|
||||
settings: account.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(account.settings.peer_login_expiration, ["hour", "day"]),
|
||||
peer_login_expiration_enabled: account.settings.peer_login_expiration_enabled
|
||||
} as FormAccount
|
||||
setFormAccount(fAccount)
|
||||
setFormPeerExpirationEnabled(fAccount.peer_login_expiration_enabled)
|
||||
form.setFieldsValue(fAccount)
|
||||
}, [accounts])
|
||||
|
||||
const updatingSettings = 'updating_settings';
|
||||
useEffect(() => {
|
||||
if (updatedAccount.loading) {
|
||||
message.loading({content: 'Saving...', key: updatingSettings, duration: 0, style: styleNotification});
|
||||
} else if (updatedAccount.success) {
|
||||
message.success({
|
||||
content: 'Account settings have been successfully saved.',
|
||||
key: updatingSettings,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(accountActions.setUpdateAccount({...updatedAccount, success: false}));
|
||||
dispatch(accountActions.resetUpdateAccount(null))
|
||||
let fAccount = {
|
||||
id: updatedAccount.data.id,
|
||||
settings: updatedAccount.data.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(updatedAccount.data.settings.peer_login_expiration, ["hour", "day"]),
|
||||
peer_login_expiration_enabled: updatedAccount.data.settings.peer_login_expiration_enabled
|
||||
} as FormAccount
|
||||
setFormAccount(fAccount)
|
||||
} else if (updatedAccount.error) {
|
||||
let errorMsg = "Failed to update account settings"
|
||||
switch (updatedAccount.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update account settings. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = updatedAccount.error.data.message ? updatedAccount.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: updatingSettings,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
}
|
||||
}, [updatedAccount])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
confirmSave(values)
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createAccountToSave = (values: FormAccount): Account => {
|
||||
return {
|
||||
id: formAccount.id,
|
||||
settings: {
|
||||
peer_login_expiration: expiresInToSeconds(values.peer_login_expiration_formatted),
|
||||
peer_login_expiration_enabled: values.peer_login_expiration_enabled
|
||||
}
|
||||
} as Account
|
||||
}
|
||||
|
||||
const confirmSave = (newValues: FormAccount) => {
|
||||
if (newValues.peer_login_expiration_enabled != formAccount.peer_login_expiration_enabled) {
|
||||
let content = newValues.peer_login_expiration_enabled ? "Enabling peer expiration will cause some peers added with the SSO login to disconnect, and re-authentication will be required. Do you want to enable peer login expiration?" : "Disabling peer expiration will cause peers added with the SSO login never to expire. For security reasons, keeping peers expiring periodically is usually better. Do you want to disable peer login expiration?"
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Before you update your account settings.",
|
||||
width: 600,
|
||||
content: content,
|
||||
onOk() {
|
||||
saveAccount(newValues)
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
saveAccount(newValues)
|
||||
}
|
||||
}
|
||||
|
||||
const saveAccount = (newValues: FormAccount) => {
|
||||
let accountToSave = createAccountToSave(newValues)
|
||||
dispatch(accountActions.updateAccount.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: accountToSave
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Settings</Title>
|
||||
<Paragraph>Manage your account's settings</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"}
|
||||
style={{display: 'flex'}}>
|
||||
<Card
|
||||
title="Authentication"
|
||||
loading={loading}
|
||||
defaultValue={"Enabled"}
|
||||
>
|
||||
<Form.Item
|
||||
label="Peer login expiration"
|
||||
name="peer_login_expiration_enabled"
|
||||
tooltip="Peer login expiration allows to periodically request re-authentication of peers that were added with the SSO login. You can disable the expiration per peer in the peers tab."
|
||||
//rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Radio.Group
|
||||
options={[{label: 'Enabled', value: true}, {
|
||||
label: 'Disabled',
|
||||
value: false
|
||||
}]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={function (e) {
|
||||
setFormPeerExpirationEnabled(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="peer_login_expiration_formatted"
|
||||
label="Peer login expires in"
|
||||
tooltip="Time after which every peer added with SSO login will require re-authentication."
|
||||
rules={[{validator: checkExpiresIn}]}>
|
||||
<ExpiresInInput
|
||||
disabled={!formPeerExpirationEnabled}
|
||||
options={Array.of({key: "hour", title: "Hours"}, {
|
||||
key: "day",
|
||||
title: "Days"
|
||||
})
|
||||
}/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/periodic-authentication">Learn more about login expiration</Button>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{textAlign: 'center'}}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<UserUpdate/>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user