Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
439e803ef2 | ||
|
|
440c51a35c | ||
|
|
800e94de85 | ||
|
|
ba7d138156 | ||
|
|
a66fb3bf8f | ||
|
|
fcc51243e0 | ||
|
|
312c60dd45 | ||
|
|
09e6de74ee | ||
|
|
addd348456 | ||
|
|
a8190bfe5b | ||
|
|
9e3d9f245d | ||
|
|
e7a7a75906 | ||
|
|
67efd47f22 | ||
|
|
813cd851ca | ||
|
|
f44ccf3ef7 | ||
|
|
899f56acdc | ||
|
|
dc2760d5ff | ||
|
|
5ae4fd31f1 | ||
|
|
54d9d7c768 | ||
|
|
3a73781fca | ||
|
|
f3c5d4df6a | ||
|
|
b4c9135c58 | ||
|
|
a280d9c67c | ||
|
|
5caf06b086 | ||
|
|
95af1c4e32 | ||
|
|
d247e7f3ed | ||
|
|
391e534253 | ||
|
|
a7f64d4a15 | ||
|
|
53ed514803 | ||
|
|
6cadce1598 | ||
|
|
c03d4b8a4b | ||
|
|
bc1053fbb4 | ||
|
|
d154bfb799 | ||
|
|
67fd2fcb2e | ||
|
|
f4933e45ee | ||
|
|
331dd2b429 | ||
|
|
3a8106c1e7 | ||
|
|
b595e0d6a8 | ||
|
|
33621cae5d | ||
|
|
360d807008 | ||
|
|
6f8897ffa5 | ||
|
|
75d5f804c5 | ||
|
|
f7cac02a2d | ||
|
|
ec40730cb2 | ||
|
|
7f3648861b | ||
|
|
b50464db43 | ||
|
|
1eb5ccc131 | ||
|
|
ac42a17b11 | ||
|
|
77ca3c6fde | ||
|
|
20e24b4ede | ||
|
|
7a29dac01c | ||
|
|
444e9ec44a | ||
|
|
dff0313f82 | ||
|
|
11fbfb336a | ||
|
|
4a0ae8f27d | ||
|
|
9a72d8b0c4 | ||
|
|
8e038cf242 | ||
|
|
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 |
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`
|
||||
|
||||
@@ -43,6 +43,7 @@ fi
|
||||
|
||||
export AUTH_AUTHORITY=${AUTH_AUTHORITY:-https://$AUTH0_DOMAIN}
|
||||
export AUTH_CLIENT_ID=${AUTH_CLIENT_ID:-$AUTH0_CLIENT_ID}
|
||||
export AUTH_CLIENT_SECRET=${AUTH_CLIENT_SECRET}
|
||||
export AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH0_AUDIENCE}
|
||||
export AUTH_REDIRECT_URI=${AUTH_REDIRECT_URI}
|
||||
export AUTH_SILENT_REDIRECT_URI=${AUTH_SILENT_REDIRECT_URI}
|
||||
@@ -52,14 +53,12 @@ export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api o
|
||||
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
|
||||
REPO="https://github.com/netbirdio/netbird/"
|
||||
# this command will fetch the latest release e.g. v0.6.3
|
||||
export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest))
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE"
|
||||
|
||||
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
|
||||
1580
package-lock.json
generated
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@axa-fr/react-oidc": "^5.14.0",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"antd": "^4.20.6",
|
||||
"antd": "^5.3.1",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"axios": "^0.27.2",
|
||||
"cidr-regex": "^3.1.1",
|
||||
@@ -27,16 +27,19 @@
|
||||
"highlight.js": "^11.2.0",
|
||||
"history": "^5.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"postcss": "^8.4.12",
|
||||
"prop-types": "^15.7.2",
|
||||
"punycode": "^2.1.1",
|
||||
"rc-overflow": "^1.2.8",
|
||||
"react": "^18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-hotjar": "^5.1.0",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-select": "^5.7.3",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-table": "^7.7.0",
|
||||
"redux": "^4.2.0",
|
||||
|
||||
@@ -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: /
|
||||
|
||||
107
src/App.tsx
@@ -4,7 +4,7 @@ import {apiClient, store} from "./store";
|
||||
import {hotjar} from 'react-hotjar';
|
||||
import {getConfig} from "./config";
|
||||
import Banner from "./components/Banner";
|
||||
import {Col, Layout, Row} from "antd";
|
||||
import {Col, ConfigProvider, Layout, Row} from "antd";
|
||||
import {Container} from "./components/Container";
|
||||
import Navbar from "./components/Navbar";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
@@ -16,11 +16,12 @@ import SetupKeys from "./views/SetupKeys";
|
||||
import AccessControl from "./views/AccessControl";
|
||||
import Users from "./views/Users";
|
||||
import FooterComponent from "./components/FooterComponent";
|
||||
import {useGetAccessTokenSilently} from "./utils/token";
|
||||
import {useGetTokenSilently, useTokenSource} from "./utils/token";
|
||||
import {User} from "./store/user/types";
|
||||
import {SecureLoading} from "./components/Loading";
|
||||
import DNS from "./views/DNS";
|
||||
|
||||
import Activity from "./views/Activity";
|
||||
import Settings from "./views/Settings";
|
||||
|
||||
|
||||
const {Header, Content} = Layout;
|
||||
@@ -28,8 +29,9 @@ const {Header, Content} = Layout;
|
||||
function App() {
|
||||
const run = useRef(false)
|
||||
const [show, setShow] = useState(false)
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently();
|
||||
const {hotjarTrackID} = getConfig();
|
||||
const {hotjarTrackID,tokenSource} = getConfig();
|
||||
useTokenSource(tokenSource)
|
||||
const {getTokenSilently} = useGetTokenSilently();
|
||||
// @ts-ignore
|
||||
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
|
||||
hotjar.initialize(hotjarTrackID, 6);
|
||||
@@ -54,9 +56,9 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (!run.current) {
|
||||
run.current = true
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getTokenSilently})
|
||||
.then(() => {
|
||||
setShow(true)
|
||||
setShow(true)
|
||||
})
|
||||
.catch(e => {
|
||||
setShow(true)
|
||||
@@ -64,53 +66,66 @@ function App() {
|
||||
})
|
||||
}
|
||||
|
||||
}, [getAccessTokenSilently])
|
||||
}, [getTokenSilently])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
colorPrimary: "#1890ff",
|
||||
fontFamily: "Arial"
|
||||
},
|
||||
components: {Badge: {fontSizeSM: 20}},
|
||||
}}
|
||||
>
|
||||
<Provider store={store}>
|
||||
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
|
||||
{show &&
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
<Route path="/activity" component={withOidcSecure(Activity)}/>
|
||||
<Route path="/settings" component={withOidcSecure(Settings)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
}
|
||||
</Provider>
|
||||
|
||||
</ConfigProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="29.435" height="7.636" viewBox="0 0 29.435 7.636">
|
||||
<path id="direct_bi" d="M3.64,8.158-.178,4.34,3.64.522,4.3,1.17l-2.7,2.7h25.89l-2.7-2.7.656-.648L29.257,4.34,27.213,6.384,25.439,8.158,24.783,7.5l2.7-2.693H1.595L4.3,7.5Z" transform="translate(0.178 -0.522)" fill="#1e429f"/>
|
||||
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.451731 2.66267C0.315048 2.79935 0.315048 3.02096 0.451731 3.15764L2.67912 5.38503C2.8158 5.52171 3.03741 5.52171 3.17409 5.38503C3.31078 5.24835 3.31078 5.02674 3.17409 4.89006L1.19419 2.91016L3.17409 0.930257C3.31078 0.793574 3.31078 0.571966 3.17409 0.435282C3.03741 0.298599 2.8158 0.298599 2.67912 0.435282L0.451731 2.66267ZM38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#03543F"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 837 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="29.497" height="7.636" viewBox="0 0 29.497 7.636">
|
||||
<path id="direct_out" d="M26.589,8l-.656-.656,2.7-2.693H.91V3.713H28.635l-2.7-2.7.656-.648,3.818,3.818Z" transform="translate(-0.91 -0.364)" fill="#03543f"/>
|
||||
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#1E429F"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 506 B |
3
src/assets/forward_default.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3846 3.68889C38.5213 3.55221 38.5213 3.3306 38.3846 3.19392L36.1572 0.966532C36.0205 0.829849 35.7989 0.829849 35.6622 0.966532C35.5255 1.10322 35.5255 1.32482 35.6622 1.46151L37.6421 3.44141L35.6622 5.42131C35.5255 5.55799 35.5255 5.7796 35.6622 5.91628C35.7989 6.05296 36.0205 6.05296 36.1572 5.91628L38.3846 3.68889ZM0.703125 3.79141H38.1371V3.09141H0.703125V3.79141Z" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 503 B |
BIN
src/assets/google-play-badge.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
3
src/assets/in_bound.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#2B4DA5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
3
src/assets/out_bound_blue.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3807 3.68889C38.5174 3.55221 38.5174 3.3306 38.3807 3.19392L36.1533 0.966532C36.0166 0.829849 35.795 0.829849 35.6583 0.966532C35.5216 1.10322 35.5216 1.32482 35.6583 1.46151L37.6382 3.44141L35.6583 5.42131C35.5216 5.55799 35.5216 5.7796 35.6583 5.91628C35.795 6.05296 36.0166 6.05296 36.1533 5.91628L38.3807 3.68889ZM0.699219 3.79141L38.1332 3.79141V3.09141L0.699219 3.09141V3.79141Z" fill="#2B4DA5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
3
src/assets/out_bound_green.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3846 3.68889C38.5213 3.55221 38.5213 3.3306 38.3846 3.19392L36.1572 0.966532C36.0205 0.829849 35.7989 0.829849 35.6622 0.966532C35.5255 1.10322 35.5255 1.32482 35.6622 1.46151L37.6421 3.44141L35.6622 5.42131C35.5255 5.55799 35.5255 5.7796 35.6622 5.91628C35.7989 6.05296 36.0205 6.05296 36.1572 5.91628L38.3846 3.68889ZM0.703125 3.79141H38.1371V3.09141H0.703125V3.79141Z" fill="#03543F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 503 B |
3
src/assets/reverse_default.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
3
src/assets/reverse_green.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#03543F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
891
src/components/AccessControlEdit.tsx
Normal file
@@ -0,0 +1,891 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as policyActions} from "../store/policy";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Card,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
Breadcrumb,
|
||||
} from "antd";
|
||||
import inbound from "../assets/in_bound.svg";
|
||||
import outBoundGreen from "../assets/out_bound_green.svg";
|
||||
import outBoundblue from "../assets/out_bound_blue.svg";
|
||||
import reverseDefault from "../assets/reverse_default.svg";
|
||||
import forwardDefault from "../assets/forward_default.svg";
|
||||
import reverseGreen from "../assets/reverse_green.svg";
|
||||
import type {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {Policy, PolicyToSave} from "../store/policy/types";
|
||||
import {uniq} from "lodash";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {Container} from "./Container";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const {Text} = Typography;
|
||||
|
||||
interface FormPolicy {
|
||||
id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
query: string;
|
||||
bidirectional: boolean;
|
||||
protocol: string;
|
||||
ports: string[];
|
||||
action: string;
|
||||
tagSourceGroups: string[];
|
||||
tagDestinationGroups: string[];
|
||||
}
|
||||
|
||||
const AccessControlEdit = () => {
|
||||
const {
|
||||
optionRender,
|
||||
blueTagRender,
|
||||
grayTagRender
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const {getTokenSilently} = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
const setupEditPolicyVisible = useSelector(
|
||||
(state: RootState) => state.policy.setupEditPolicyVisible
|
||||
);
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const actions: SelectProps["options"] = [
|
||||
{label: "Accept", value: "accept"},
|
||||
{label: "Drop", value: "drop"},
|
||||
];
|
||||
const protocols: SelectProps["options"] = [
|
||||
{label: "All", value: "all"},
|
||||
{label: "TCP", value: "tcp"},
|
||||
{label: "UDP", value: "udp"},
|
||||
{label: "ICMP", value: "icmp"},
|
||||
];
|
||||
const policy = useSelector((state: RootState) => state.policy.policy);
|
||||
const savedPolicy = useSelector(
|
||||
(state: RootState) => state.policy.savedPolicy
|
||||
);
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const [direction, setDirection] = useState<any>({
|
||||
biDirectional: false,
|
||||
reverseDirectional: false,
|
||||
});
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [formPolicy, setFormPolicy] = useState({} as FormPolicy);
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const inputDescriptionRef = useRef<any>(null);
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
return () => {
|
||||
onCancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({cursor: "end"});
|
||||
}, [editName]);
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({cursor: "end"});
|
||||
}, [editDescription]);
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.map((g) => g.name) || []);
|
||||
}, [groups]);
|
||||
useEffect(() => {
|
||||
if (!policy) return;
|
||||
const fPolicy = {
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
enabled: policy.enabled,
|
||||
query: "",
|
||||
bidirectional: policy.rules[0].bidirectional,
|
||||
protocol: policy.rules[0].protocol,
|
||||
ports: policy.rules[0].ports,
|
||||
action: policy.rules[0].action,
|
||||
tagSourceGroups: policy.rules[0].sources
|
||||
? policy.rules[0].sources?.map((t) => t.name)
|
||||
: [],
|
||||
tagDestinationGroups: policy.rules[0].destinations
|
||||
? policy.rules[0].destinations?.map((t) => t.name)
|
||||
: [],
|
||||
} as FormPolicy;
|
||||
setFormPolicy(fPolicy);
|
||||
form.setFieldsValue(fPolicy);
|
||||
if (fPolicy.bidirectional) {
|
||||
setDirection({
|
||||
biDirectional: true,
|
||||
reverseDirectional: true,
|
||||
});
|
||||
} else {
|
||||
setDirection({
|
||||
biDirectional: false,
|
||||
reverseDirectional: false,
|
||||
});
|
||||
}
|
||||
}, [policy, form]);
|
||||
|
||||
const createPolicyToSave = (): PolicyToSave => {
|
||||
const sources =
|
||||
groups
|
||||
?.filter((g) => formPolicy.tagSourceGroups.includes(g.name))
|
||||
.map((g) => g.id || "") || [];
|
||||
const destinations =
|
||||
groups
|
||||
?.filter((g) => formPolicy.tagDestinationGroups.includes(g.name))
|
||||
.map((g) => g.id || "") || [];
|
||||
const sourcesNoId = formPolicy.tagSourceGroups.filter(
|
||||
(s) => !tagGroups.includes(s)
|
||||
);
|
||||
const destinationsNoId = formPolicy.tagDestinationGroups.filter(
|
||||
(s) => !tagGroups.includes(s)
|
||||
);
|
||||
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId]);
|
||||
return {
|
||||
id: formPolicy.id,
|
||||
name: formPolicy.name,
|
||||
description: formPolicy.description,
|
||||
enabled: formPolicy.enabled,
|
||||
sourcesNoId,
|
||||
destinationsNoId,
|
||||
groupsToSave,
|
||||
rules: [
|
||||
{
|
||||
id: formPolicy.id,
|
||||
name: formPolicy.name,
|
||||
description: formPolicy.description,
|
||||
enabled: formPolicy.enabled,
|
||||
sources:
|
||||
direction.reverseDirectional && !direction.biDirectional
|
||||
? destinations
|
||||
: sources,
|
||||
destinations:
|
||||
direction.reverseDirectional && !direction.biDirectional
|
||||
? sources
|
||||
: destinations,
|
||||
bidirectional: formPolicy.bidirectional,
|
||||
protocol: formPolicy.protocol,
|
||||
ports: formPolicy.ports,
|
||||
action: "accept",
|
||||
},
|
||||
],
|
||||
} as PolicyToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((_) => {
|
||||
const policyToSave = createPolicyToSave();
|
||||
dispatch(
|
||||
policyActions.savePolicy.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: policyToSave,
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRule = (status: boolean) => {
|
||||
dispatch(policyActions.setSetupEditPolicyVisible(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedPolicy.loading) return;
|
||||
setEditName(false);
|
||||
dispatch(
|
||||
policyActions.setPolicy({
|
||||
name: "",
|
||||
description: "",
|
||||
enabled: true,
|
||||
query: "",
|
||||
rules: [
|
||||
{
|
||||
name: "",
|
||||
description: "",
|
||||
enabled: true,
|
||||
sources: [],
|
||||
destinations: [],
|
||||
bidirectional: true,
|
||||
protocol: "all",
|
||||
ports: [],
|
||||
action: "accept",
|
||||
},
|
||||
],
|
||||
} as Policy)
|
||||
);
|
||||
setVisibleNewRule(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormPolicy({...formPolicy, ...data});
|
||||
};
|
||||
|
||||
const handleChangeSource = (value: string[]) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
tagSourceGroups: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeDestination = (value: string[]) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
tagDestinationGroups: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeProtocol = (value: string) => {
|
||||
if (value === "all" || value === "icmp") {
|
||||
setDirection({
|
||||
biDirectional: true,
|
||||
reverseDirectional: true,
|
||||
});
|
||||
}
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
ports: value === "all" || value === "icmp" ? [] : formPolicy.ports,
|
||||
protocol: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangePorts = (value: string[]) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
ports: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeDisabled = (checked: boolean) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
enabled: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeBidirect = (checked: boolean) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
bidirectional: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const dropDownRenderGroups = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: "8px 0"}}/>
|
||||
<Row style={{padding: "0 8px 4px"}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>
|
||||
Add new group by pressing "Enter"
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
const dropDownRenderPorts = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: "8px 0"}}/>
|
||||
<Row style={{padding: "0 8px 4px"}}>
|
||||
<Col flex="auto">
|
||||
<Text type={"secondary"}>
|
||||
Add new ports by pressing "Enter"
|
||||
</Text>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"));
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const selectPortRangeValidator = (_: RuleObject, value: string[]) => {
|
||||
if (value) {
|
||||
var failed = false;
|
||||
value.forEach(function (v: string) {
|
||||
let p = Number(v);
|
||||
if (Number.isNaN(p) || p < 1 || p > 65535 || !Number.isInteger(p)) {
|
||||
failed = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (failed) {
|
||||
return Promise.reject(
|
||||
new Error("Port value must be in 1..65535 range")
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const selectPortProtocolValidator = (_: RuleObject, value: string[]) => {
|
||||
if (!formPolicy.bidirectional && value.length === 0) {
|
||||
return Promise.reject(new Error("Directional traffic require ports"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const handleDirection = (directionValue: string) => {
|
||||
if (
|
||||
directionValue === "forwardDirectional" &&
|
||||
!direction.reverseDirectional
|
||||
) {
|
||||
setDirection({
|
||||
biDirectional: false,
|
||||
reverseDirectional: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
directionValue === "forwardDirectional" &&
|
||||
direction.reverseDirectional
|
||||
) {
|
||||
setDirection({
|
||||
...direction,
|
||||
biDirectional: !direction.biDirectional,
|
||||
});
|
||||
}
|
||||
|
||||
if (directionValue === "reverseDirectional" && direction.biDirectional) {
|
||||
setDirection({
|
||||
biDirectional: false,
|
||||
reverseDirectional: !direction.reverseDirectional,
|
||||
});
|
||||
}
|
||||
|
||||
if (directionValue === "reverseDirectional" && !direction.biDirectional) {
|
||||
setDirection({
|
||||
biDirectional: true,
|
||||
reverseDirectional: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onBreadcrumbUsersClick = () => {
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(formPolicy).length > 0) {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
bidirectional: direction.biDirectional,
|
||||
});
|
||||
}
|
||||
}, [direction]);
|
||||
return (
|
||||
<>
|
||||
{policy && (
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Breadcrumb
|
||||
style={{marginBottom: "25px"}}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Access Control</a>,
|
||||
},
|
||||
{
|
||||
title: policy.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card
|
||||
bordered={true}
|
||||
// title={setupKey.name}
|
||||
style={{marginBottom: "7px"}}
|
||||
>
|
||||
<div style={{maxWidth: "550px"}}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
form={form}
|
||||
onValuesChange={onChange}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
{!editName && (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "22px",
|
||||
margin: "0px",
|
||||
marginBottom: "10px",
|
||||
cursor: "pointer",
|
||||
fontWeight:"500"
|
||||
}}
|
||||
onDoubleClick={() => toggleEditName(true)}
|
||||
>
|
||||
{formPolicy.name}
|
||||
</Paragraph>
|
||||
)}
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
{editName && (
|
||||
<>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Rule name
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label=""
|
||||
style={{margin: "0"}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add a name for this access rule",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "UserAccessRule"'}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!editDescription ? (
|
||||
<div
|
||||
style={{
|
||||
margin: "12px 0 30px",
|
||||
lineHeight: "22px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => toggleEditDescription(true)}
|
||||
>
|
||||
{formPolicy.description &&
|
||||
formPolicy.description.trim() !== "" ? (
|
||||
formPolicy.description
|
||||
) : (
|
||||
<span style={{textDecoration: "underline"}}>
|
||||
Add description
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24, fontWeight: "500"}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
onPressEnter={() =>
|
||||
toggleEditDescription(false)
|
||||
}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
onChange={handleChangeDisabled}
|
||||
defaultChecked={policy.enabled}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
{formPolicy.enabled
|
||||
? "Disable this rule to apply it later"
|
||||
: "Enable this rule to apply it immediately"}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row gutter={15}>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="tagSourceGroups"
|
||||
label="Source groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
style={{fontWeight: "500"}}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{width: "100%", fontWeight: "500"}}
|
||||
placeholder="Select groups"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeSource}
|
||||
dropdownRender={dropDownRenderGroups}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col
|
||||
span={4}
|
||||
style={{padding: "0 2.5px", lineHeight: "16px"}}
|
||||
>
|
||||
<Button
|
||||
type={"ghost"}
|
||||
disabled={
|
||||
formPolicy.protocol === "all" ||
|
||||
formPolicy.protocol === "icmp"
|
||||
}
|
||||
onClick={() => handleDirection("forwardDirectional")}
|
||||
style={{
|
||||
padding: "0",
|
||||
width: "100%",
|
||||
marginTop: "30px",
|
||||
height: "13px",
|
||||
}}
|
||||
>
|
||||
<Tag
|
||||
style={{
|
||||
marginInlineEnd: "0",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
height: "13px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
color={
|
||||
!direction.biDirectional &&
|
||||
!direction.reverseDirectional
|
||||
? "processing"
|
||||
: direction.biDirectional
|
||||
? "green"
|
||||
: "default"
|
||||
}
|
||||
>
|
||||
{!direction.biDirectional &&
|
||||
!direction.reverseDirectional ? (
|
||||
<img
|
||||
src={outBoundblue}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
) : direction.biDirectional ? (
|
||||
<img
|
||||
src={outBoundGreen}
|
||||
alt="out icon"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={forwardDefault}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
</Button>
|
||||
<Button
|
||||
type="ghost"
|
||||
disabled={
|
||||
formPolicy.protocol === "all" ||
|
||||
formPolicy.protocol === "icmp"
|
||||
}
|
||||
onClick={() => handleDirection("reverseDirectional")}
|
||||
style={{
|
||||
padding: "0",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
height: "13px",
|
||||
marginTop: "0",
|
||||
}}
|
||||
>
|
||||
<Tag
|
||||
style={{
|
||||
marginInlineEnd: "0",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
height: "13px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
color={
|
||||
direction.reverseDirectional &&
|
||||
direction.biDirectional
|
||||
? "green"
|
||||
: direction.reverseDirectional
|
||||
? "processing"
|
||||
: "default"
|
||||
}
|
||||
>
|
||||
{direction.reverseDirectional &&
|
||||
direction.biDirectional ? (
|
||||
<img
|
||||
src={reverseGreen}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
) : direction.reverseDirectional ? (
|
||||
<img
|
||||
src={inbound}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={reverseDefault}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="tagDestinationGroups"
|
||||
label="Destination groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
style={{fontWeight: "500"}}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{width: "100%", fontWeight: "500"}}
|
||||
placeholder="Select groups"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeDestination}
|
||||
dropdownRender={dropDownRenderGroups}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{marginTop: "-15px", marginBottom: "30px"}}
|
||||
>
|
||||
To change traffic direction and ports, select TCP or UDP protocol below
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="protocol"
|
||||
label="Protocol"
|
||||
style={{fontWeight: "500"}}
|
||||
className="tag-box"
|
||||
>
|
||||
<Select
|
||||
style={{width: "100%", maxWidth: "260px"}}
|
||||
options={protocols}
|
||||
onChange={handleChangeProtocol}
|
||||
className="menlo-font"
|
||||
defaultValue={"all"}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="ports"
|
||||
label="Ports"
|
||||
style={{fontWeight: "500"}}
|
||||
rules={[
|
||||
{
|
||||
message:
|
||||
"Directional traffic requires at least one port",
|
||||
validator: selectPortProtocolValidator,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
message: "Port value must be in 1..65535 range",
|
||||
validator: selectPortRangeValidator,
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "260px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
placeholder={(formPolicy.protocol === "all" || formPolicy.protocol === "icmp") ? "Change protocol to add ports" : "Add ports"}
|
||||
tagRender={grayTagRender}
|
||||
onChange={handleChangePorts}
|
||||
className="menlo-font"
|
||||
dropdownRender={dropDownRenderPorts}
|
||||
disabled={
|
||||
formPolicy.protocol === "all" ||
|
||||
formPolicy.protocol === "icmp"
|
||||
}
|
||||
>
|
||||
{formPolicy &&
|
||||
formPolicy.ports?.map((m) => (
|
||||
<Option key={m}>
|
||||
<Tag style={{marginRight: 3}}>
|
||||
{m}
|
||||
</Tag>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
<Container
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
padding: 0,
|
||||
gap: "10px",
|
||||
marginTop: "30px",
|
||||
}}
|
||||
key={0}
|
||||
>
|
||||
<Button onClick={onCancel} disabled={savedPolicy.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedPolicy.loading}
|
||||
onClick={handleFormSubmit}
|
||||
>{`${formPolicy.id ? "Save" : "Create"}`}</Button>
|
||||
</Container>
|
||||
</Card>
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessControlEdit;
|
||||
@@ -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}
|
||||
|
||||
@@ -22,7 +22,7 @@ const Banner = () => {
|
||||
const linkLearnMore = () => {
|
||||
return (
|
||||
<a
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
href="https://docs.netbird.io/how-to/manage-dns-in-your-network"
|
||||
className="font-bold underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -36,7 +36,7 @@ const Banner = () => {
|
||||
|
||||
if((!stored_banner_closed || stored_banner_closed !== 'true') ||
|
||||
(!store_banner_md5 || store_banner_md5 !== announcement_md5)) {
|
||||
setShow(true);
|
||||
//setShow(true);
|
||||
localStorage.setItem(banner_md5_key,announcement_md5);
|
||||
localStorage.setItem(banner_closed_key,'false');
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {NameServer, NameServerGroup, NameServerGroupToSave} from "../store/nameservers/types";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups"
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
@@ -41,7 +41,7 @@ interface formNSGroup extends NameServerGroup {
|
||||
|
||||
const NameServerGroupUpdate = () => {
|
||||
const {
|
||||
tagRender,
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
@@ -51,7 +51,7 @@ const NameServerGroupUpdate = () => {
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const dispatch = useDispatch()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const {Option} = Select;
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
|
||||
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
@@ -220,7 +220,7 @@ const NameServerGroupUpdate = () => {
|
||||
.then((values) => {
|
||||
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
|
||||
dispatch(nsGroupActions.saveNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: nsGroupToSave
|
||||
}))
|
||||
|
||||
@@ -572,7 +572,7 @@ const NameServerGroupUpdate = () => {
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the NS group"
|
||||
tagRender={tagRender}
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
@@ -604,9 +604,7 @@ const NameServerGroupUpdate = () => {
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about nameservers</Button>
|
||||
href="https://docs.netbird.io/how-to/manage-dns-in-your-network">Learn more about nameservers</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>) :
|
||||
|
||||
@@ -5,13 +5,14 @@ import {Avatar, Button, Col, Dropdown, Grid, Menu, Row} from 'antd'
|
||||
import {ItemType} from "antd/lib/menu/hooks/useItems";
|
||||
import {AvatarSize} from "antd/es/avatar/SizeContext";
|
||||
import {UserOutlined} from '@ant-design/icons';
|
||||
import {useOidc, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "../config";
|
||||
import {User} from "../store/user/types";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
@@ -19,10 +20,11 @@ const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const { logout } = useOidc();
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {idTokenPayload} = useOidcIdToken()
|
||||
const user = oidcUser;
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
|
||||
@@ -34,22 +36,37 @@ const Navbar = () => {
|
||||
|
||||
const items = [
|
||||
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
|
||||
{label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer'},
|
||||
{label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys'},
|
||||
{label: (<Link to="/acls">Access Control</Link>), key: '/acls'},
|
||||
{label: (<Link to="/routes">Network Routes</Link>), key: '/routes'},
|
||||
{ label: (<Link to="/dns">DNS</Link>), key: '/dns' },
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'}
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'},
|
||||
{label: (<Link to="/activity">Activity</Link>), key: '/activity'},
|
||||
{label: (<Link to="/settings">Settings</Link>), key: '/settings'}
|
||||
] as ItemType[]
|
||||
|
||||
const userEmailKey = 'user-email'
|
||||
const userLogoutKey = 'user-logout'
|
||||
const userDividerKey = 'user-divider'
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns"]
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
|
||||
const [menuItems, setMenuItems] = useState(items)
|
||||
const logoutWithRedirect = () =>
|
||||
logout("/", {client_id: config.clientId});
|
||||
|
||||
const openPersonalUserPage = () => {
|
||||
dispatch(userActions.setUser({
|
||||
id: currentUser.id,
|
||||
email: currentUser.email,
|
||||
role: currentUser.role,
|
||||
auto_groups: currentUser.auto_groups ? currentUser.auto_groups : [],
|
||||
name: currentUser.name,
|
||||
is_current: currentUser.is_current,
|
||||
is_service_user: currentUser.is_service_user,
|
||||
} as User));
|
||||
dispatch(userActions.setUserTabOpen("Users"));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fs = items.filter(m => showTab(m?.key?.toString(), currentUser) && m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
if (screens.xs === true) {
|
||||
@@ -78,15 +95,19 @@ const Navbar = () => {
|
||||
window.location.pathname !== '/peers' &&
|
||||
window.location.pathname !== '/users') {
|
||||
setIsRefreshingUserState(true)
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}))
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
return
|
||||
}
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
return
|
||||
}
|
||||
let runUser = oidcUser
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload
|
||||
}
|
||||
setIsRefreshingUserState(false)
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
if (runUser) {
|
||||
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
@@ -108,7 +129,7 @@ const Navbar = () => {
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: <>{user?.email}</>,
|
||||
label: (<Link to="/users" onClick={openPersonalUserPage}>{user?.email}</Link>),
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
@@ -142,6 +163,10 @@ const Navbar = () => {
|
||||
<Col flex="1 1 auto">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]}
|
||||
onSelect={(e) => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
}}
|
||||
defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
929
src/components/RouteAddNew.tsx
Normal file
@@ -0,0 +1,929 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as routeActions } from "../store/route";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Space,
|
||||
Switch,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
FlagFilled,
|
||||
QuestionCircleFilled,
|
||||
} from "@ant-design/icons";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import { Route, RouteToSave } from "../store/route/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import cidrRegex from "cidr-regex";
|
||||
import {
|
||||
initPeerMaps,
|
||||
masqueradeDisabledMSG,
|
||||
peerToPeerIP,
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable,
|
||||
} from "../utils/routes";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface FormRoute extends Route {}
|
||||
|
||||
const RouteAddNew = (selectedPeer: any) => {
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
// const { optionRender, blueTagRender, grayTagRender } =
|
||||
// useGetGroupTagHelpers();
|
||||
const { Option } = Select;
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
const setupNewRouteVisible = useSelector(
|
||||
(state: RootState) => state.route.setupNewRouteVisible
|
||||
);
|
||||
const setupNewRouteHA = useSelector(
|
||||
(state: RootState) => state.route.setupNewRouteHA
|
||||
);
|
||||
const peers = useSelector((state: RootState) => state.peer.data);
|
||||
const route = useSelector((state: RootState) => state.route.route);
|
||||
const routes = useSelector((state: RootState) => state.route.data);
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute);
|
||||
const [previousRouteKey, setPreviousRouteKey] = useState("");
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const options: SelectProps["options"] = [];
|
||||
const testOptions: SelectProps["options"] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute);
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const inputDescriptionRef = useRef<any>(null);
|
||||
const defaultRoutingPeerMSG = "Routing Peer";
|
||||
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG);
|
||||
const defaultMasqueradeMSG = "Masquerade";
|
||||
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG);
|
||||
const defaultStatusMSG = "Status";
|
||||
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG);
|
||||
const [enableNetwork, setEnableNetwork] = useState(false);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!newRoute) {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG);
|
||||
setMasqueradeMSG("Update Masquerade");
|
||||
setStatusMSG("Update Status");
|
||||
} else {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG);
|
||||
setMasqueradeMSG(defaultMasqueradeMSG);
|
||||
setStatusMSG(defaultStatusMSG);
|
||||
setPreviousRouteKey("");
|
||||
}
|
||||
}, [newRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editName)
|
||||
inputNameRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription)
|
||||
inputDescriptionRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!route) return;
|
||||
|
||||
if (selectedPeer && selectedPeer.selectedPeer) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(
|
||||
selectedPeer.selectedPeer.name,
|
||||
selectedPeer.selectedPeer.ip
|
||||
),
|
||||
value: peerToPeerIP(
|
||||
selectedPeer.selectedPeer.name,
|
||||
selectedPeer.selectedPeer.ip
|
||||
),
|
||||
disabled: false,
|
||||
});
|
||||
const udpateRoute = { ...route, peer: options[0].value } as FormRoute;
|
||||
setFormRoute(udpateRoute);
|
||||
form.setFieldsValue(udpateRoute);
|
||||
setPreviousRouteKey(udpateRoute.network_id + udpateRoute.network);
|
||||
} else {
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: getGroupNamesFromIDs(route.groups),
|
||||
} as FormRoute;
|
||||
setFormRoute(fRoute);
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network);
|
||||
form.setFieldsValue(fRoute);
|
||||
}
|
||||
|
||||
if (!route.network_id) {
|
||||
setNewRoute(true);
|
||||
} else {
|
||||
setNewRoute(false);
|
||||
}
|
||||
|
||||
// let options = [];
|
||||
}, [route]);
|
||||
|
||||
selectedPeer &&
|
||||
selectedPeer.notPeerRoutes &&
|
||||
selectedPeer.notPeerRoutes.forEach((element: any, index: number) => {
|
||||
testOptions?.push({
|
||||
label: element.network_id + " - " + element.network,
|
||||
value: element.network_id + "+" + index,
|
||||
network: element.network,
|
||||
disabled: false,
|
||||
key: index,
|
||||
});
|
||||
});
|
||||
if (!selectedPeer.selectedPeer) {
|
||||
peers.forEach((p) => {
|
||||
let os: string;
|
||||
os = p.os;
|
||||
if (
|
||||
!os.toLowerCase().startsWith("darwin") &&
|
||||
!os.toLowerCase().startsWith("windows") &&
|
||||
!os.toLowerCase().startsWith("android") &&
|
||||
route &&
|
||||
!routes
|
||||
.filter((r) => r.network_id === route.network_id)
|
||||
.find((r) => r.peer === p.id)
|
||||
) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator);
|
||||
let peerID: string;
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer;
|
||||
} else {
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]];
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]];
|
||||
}
|
||||
}
|
||||
|
||||
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
|
||||
inputRoute.groups
|
||||
);
|
||||
|
||||
return {
|
||||
id: inputRoute.id,
|
||||
network: inputRoute.network,
|
||||
network_id: inputRoute.network_id,
|
||||
description: inputRoute.description,
|
||||
peer: peerID,
|
||||
enabled: inputRoute.enabled,
|
||||
masquerade: inputRoute.masquerade,
|
||||
metric: inputRoute.metric,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != "") {
|
||||
const routeToSave = createRouteToSave(formRoute);
|
||||
dispatch(
|
||||
routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peers);
|
||||
groupedDataTable.forEach((group) => {
|
||||
if (group.key == previousRouteKey) {
|
||||
group.groupedRoutes.forEach((route) => {
|
||||
let updateRoute: FormRoute = {
|
||||
...formRoute,
|
||||
id: route.id,
|
||||
peer: route.peer,
|
||||
metric: route.metric,
|
||||
enabled:
|
||||
formRoute.enabled != group.enabled
|
||||
? formRoute.enabled
|
||||
: route.enabled,
|
||||
};
|
||||
const routeToSave = createRouteToSave(updateRoute);
|
||||
dispatch(
|
||||
routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRoute = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteVisible(status));
|
||||
};
|
||||
|
||||
const setSetupNewRouteHA = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedRoute.loading) return;
|
||||
setEditName(false);
|
||||
dispatch(
|
||||
routeActions.setRoute({
|
||||
network: "",
|
||||
network_id: "",
|
||||
description: "",
|
||||
peer: "",
|
||||
metric: 9999,
|
||||
masquerade: false,
|
||||
enabled: true,
|
||||
groups: [],
|
||||
} as Route)
|
||||
);
|
||||
setVisibleNewRoute(false);
|
||||
setSetupNewRouteHA(false);
|
||||
setPreviousRouteKey("");
|
||||
setNewRoute(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({ ...formRoute, ...data });
|
||||
};
|
||||
|
||||
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
};
|
||||
|
||||
const networkRangeValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value)) {
|
||||
return Promise.reject(
|
||||
new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24")
|
||||
);
|
||||
}
|
||||
|
||||
if (Number(value.split("/")[1]) < 7) {
|
||||
return Promise.reject(
|
||||
new Error("Please enter a network mask larger than /7")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const peerValidator = (_: RuleObject, value: string) => {
|
||||
if (value == "" && newRoute) {
|
||||
return Promise.reject(new Error("Please select routing one peer"));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (setupNewRouteHA && formRoute.peer == "") {
|
||||
let [, newGroups] = getExistingAndToCreateGroupsLists(value);
|
||||
if (newGroups.length > 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"You can't add new Groups from the group update view, please remove:\"" +
|
||||
newGroups +
|
||||
'"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return selectValidator(obj, value);
|
||||
};
|
||||
|
||||
const handleMasqueradeChange = (checked: boolean) => {
|
||||
setFormRoute({
|
||||
...formRoute,
|
||||
masquerade: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnableChange = (checked: boolean) => {
|
||||
setFormRoute({
|
||||
...formRoute,
|
||||
enabled: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const onNetworkChange = (selectedOption: any) => {
|
||||
if (selectedOption === null) {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: "",
|
||||
network_id: "",
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(false)
|
||||
} else if (!!selectedOption.__isNew__) {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: "",
|
||||
network_id: selectedOption.value.split("+")[0],
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(false);
|
||||
} else {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: selectedOption.network,
|
||||
network_id: selectedOption.value.split("+")[0],
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{route && (
|
||||
<Modal
|
||||
open={setupNewRouteVisible}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
Add Route
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
requiredMark={false}
|
||||
onValuesChange={onChange}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
margin: "0px",
|
||||
fontWeight: 500,
|
||||
marginBottom: "15px",
|
||||
}}
|
||||
>
|
||||
Add Route
|
||||
</Paragraph>
|
||||
|
||||
{!!selectedPeer.selectedPeer && (
|
||||
<div style={{ lineHeight: "20px" }}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Assign a peer as a routing peer for the Network CIDR
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="peer"
|
||||
rules={[{ validator: peerValidator }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
disabled={!!selectedPeer.selectedPeer}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Row align="top">
|
||||
<Col span={24} style={{ lineHeight: "20px" }}>
|
||||
{!editName && formRoute.id ? (
|
||||
<div
|
||||
className={
|
||||
"access-control input-text ant-drawer-title"
|
||||
}
|
||||
onClick={() => toggleEditName(true)}
|
||||
>
|
||||
{formRoute.id ? formRoute.network_id : "New Route"}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginBottom: "15px" }}>
|
||||
{!!selectedPeer.selectedPeer && (
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a unique cryptographic key that is assigned
|
||||
to each device
|
||||
</Paragraph>
|
||||
<CreatableSelect
|
||||
isClearable
|
||||
className="ant-select-selector-custom"
|
||||
options={testOptions}
|
||||
onChange={onNetworkChange}
|
||||
placeholder="Select an existing network or add a new one"
|
||||
classNamePrefix="react-select"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!!!selectedPeer.selectedPeer && (
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a unique cryptographic key that is assigned
|
||||
to each device
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="network_id"
|
||||
label=""
|
||||
style={{ marginBottom: "10px" }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add an identifier for this access route",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “e.g. aws-eu-central-1-vpc”"
|
||||
ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
maxLength={40}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div
|
||||
onClick={() => toggleEditDescription(true)}
|
||||
style={{
|
||||
margin: "0 0 30px",
|
||||
lineHeight: "22px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{formRoute.description &&
|
||||
formRoute.description.trim() !== "" ? (
|
||||
formRoute.description
|
||||
) : (
|
||||
<span style={{ textDecoration: "underline" }}>
|
||||
Add description
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{ marginTop: 24, fontWeight: 500 }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto"></Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
{/* {!!!selectedPeer.selectedPeer && (
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
checked={formRoute.enabled}
|
||||
onChange={handleEnableChange}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "500",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
You can enable or disable the route
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)} */}
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Range
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a private IP address range
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="network"
|
||||
label=""
|
||||
rules={[{ validator: networkRangeValidator }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “172.16.0.0/16”"
|
||||
disabled={(!setupNewRouteHA && !newRoute) || enableNetwork}
|
||||
autoComplete="off"
|
||||
minLength={9}
|
||||
maxLength={43}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{!!!selectedPeer.selectedPeer && (
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Assign a peer as a routing peer for the Network CIDR
|
||||
</Paragraph>
|
||||
<Form.Item name="peer" rules={[{ validator: peerValidator }]}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
disabled={!!selectedPeer.selectedPeer}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Distribution groups
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Advertise this route to peers that belong to the following
|
||||
groups
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label=""
|
||||
rules={[{ validator: selectPreValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the network route"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
checked={formRoute.enabled}
|
||||
onChange={handleEnableChange}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
You can enable or disable the route
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Collapse
|
||||
onChange={onChange}
|
||||
bordered={false}
|
||||
ghost={true}
|
||||
style={{ padding: "0" }}
|
||||
>
|
||||
<Panel
|
||||
key="0"
|
||||
header={
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
fontWeight: "400",
|
||||
margin: "0",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
More settings
|
||||
</Paragraph>
|
||||
}
|
||||
className="system-info-panel"
|
||||
>
|
||||
<Row gutter={16} style={{ padding: "15px 0 0" }}>
|
||||
<Col span={22}>
|
||||
<Form.Item name="masquerade" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
checked={formRoute.masquerade}
|
||||
onChange={handleMasqueradeChange}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Masquerade
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
Allow access to your private networks without
|
||||
configuring routes on your local routers or
|
||||
other devices.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Metric
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Lower metrics indicating higher priority routes
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="metric" label="">
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={9999}
|
||||
autoComplete="off"
|
||||
className="w-100"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
<Col
|
||||
span={24}
|
||||
style={{ marginTop: "20px", marginBottom: "25px" }}
|
||||
>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
>
|
||||
{" "}
|
||||
Network Routes
|
||||
</a>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouteAddNew;
|
||||
@@ -1,404 +1,560 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as routeActions} from '../store/route';
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as routeActions } from "../store/route";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Space,
|
||||
Switch,
|
||||
Typography
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Space,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {Route} from "../store/route/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {
|
||||
initPeerMaps,
|
||||
masqueradeDisabledMSG,
|
||||
peerToPeerIP,
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
CloseOutlined,
|
||||
FlagFilled,
|
||||
QuestionCircleFilled,
|
||||
} from "@ant-design/icons";
|
||||
import { Route, RouteToSave } from "../store/route/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import cidrRegex from "cidr-regex";
|
||||
import {
|
||||
initPeerMaps,
|
||||
masqueradeDisabledMSG,
|
||||
peerToPeerIP,
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable,
|
||||
} from "../utils/routes";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface FormRoute extends Route {
|
||||
}
|
||||
interface FormRoute extends Route {}
|
||||
|
||||
const RouteUpdate = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
|
||||
const setupNewRouteHA = useSelector((state: RootState) => state.route.setupNewRouteHA)
|
||||
const peers = useSelector((state: RootState) => state.peer.data)
|
||||
const route = useSelector((state: RootState) => state.route.route)
|
||||
const routes = useSelector((state: RootState) => state.route.data)
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute)
|
||||
// const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
|
||||
const [previousRouteKey, setPreviousRouteKey] = useState("")
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [editDescription, setEditDescription] = useState(false)
|
||||
const options: SelectProps['options'] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute)
|
||||
const [form] = Form.useForm()
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const inputDescriptionRef = useRef<any>(null)
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
const { Option } = Select;
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
const setupEditRouteVisible = useSelector(
|
||||
(state: RootState) => state.route.setupEditRouteVisible
|
||||
);
|
||||
const setupNewRouteHA = useSelector(
|
||||
(state: RootState) => state.route.setupNewRouteHA
|
||||
);
|
||||
const peers = useSelector((state: RootState) => state.peer.data);
|
||||
const route = useSelector((state: RootState) => state.route.route);
|
||||
const routes = useSelector((state: RootState) => state.route.data);
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute);
|
||||
const [previousRouteKey, setPreviousRouteKey] = useState("");
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const options: SelectProps["options"] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute);
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const inputDescriptionRef = useRef<any>(null);
|
||||
|
||||
const defaultRoutingPeerMSG = "Routing Peer"
|
||||
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG)
|
||||
const defaultMasqueradeMSG = "Masquerade"
|
||||
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG)
|
||||
const defaultStatusMSG = "Status"
|
||||
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG)
|
||||
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
|
||||
const defaultRoutingPeerMSG = "Routing Peer";
|
||||
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG);
|
||||
const defaultMasqueradeMSG = "Masquerade";
|
||||
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG);
|
||||
const defaultStatusMSG = "Status";
|
||||
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false);
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
const optionsDisabledEnabled = [
|
||||
{ label: "Enabled", value: true },
|
||||
{ label: "Disabled", value: false },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (setupNewRouteHA) {
|
||||
setRoutingPeerMSG("Add additional routing peer")
|
||||
setMasqueradeMSG("Update Masquerade")
|
||||
setStatusMSG("Update Status")
|
||||
} else {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG)
|
||||
setMasqueradeMSG(defaultMasqueradeMSG)
|
||||
setStatusMSG(defaultStatusMSG)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
}, [setupNewRouteHA])
|
||||
useEffect(() => {
|
||||
if (!newRoute) {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG);
|
||||
setMasqueradeMSG("Update Masquerade");
|
||||
setStatusMSG("Update Status");
|
||||
} else {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG);
|
||||
setMasqueradeMSG(defaultMasqueradeMSG);
|
||||
setStatusMSG(defaultStatusMSG);
|
||||
setPreviousRouteKey("");
|
||||
}
|
||||
}, [newRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
useEffect(() => {
|
||||
if (editName)
|
||||
inputNameRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editDescription]);
|
||||
useEffect(() => {
|
||||
if (editDescription)
|
||||
inputDescriptionRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!route) return
|
||||
useEffect(() => {
|
||||
if (!route) return;
|
||||
|
||||
const fRoute = {
|
||||
...route,
|
||||
} as FormRoute
|
||||
setFormRoute(fRoute)
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network)
|
||||
form.setFieldsValue(fRoute)
|
||||
}, [route])
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: getGroupNamesFromIDs(route.groups),
|
||||
} as FormRoute;
|
||||
setFormRoute(fRoute);
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network);
|
||||
if (!route.network_id) {
|
||||
setNewRoute(true);
|
||||
} else {
|
||||
setNewRoute(false);
|
||||
}
|
||||
form.setFieldsValue(fRoute);
|
||||
}, [route]);
|
||||
|
||||
peers.forEach((p) => {
|
||||
let os: string
|
||||
os = p.os
|
||||
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows")) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false
|
||||
peers.forEach((p) => {
|
||||
let os: string;
|
||||
os = p.os;
|
||||
if (
|
||||
!os.toLowerCase().startsWith("darwin") &&
|
||||
!os.toLowerCase().startsWith("windows") &&
|
||||
!os.toLowerCase().startsWith("android") &&
|
||||
route &&
|
||||
!routes
|
||||
.filter((r) => r.network_id === route.network_id)
|
||||
.find((r) => r.peer === p.id)
|
||||
) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator);
|
||||
let peerID: string;
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer;
|
||||
} else {
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]];
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]];
|
||||
}
|
||||
}
|
||||
|
||||
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
|
||||
inputRoute.groups
|
||||
);
|
||||
|
||||
return {
|
||||
id: inputRoute.id,
|
||||
network: inputRoute.network,
|
||||
network_id: inputRoute.network_id,
|
||||
description: inputRoute.description,
|
||||
peer: peerID,
|
||||
enabled: inputRoute.enabled,
|
||||
masquerade: inputRoute.masquerade,
|
||||
metric: inputRoute.metric,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != "") {
|
||||
const routeToSave = createRouteToSave(formRoute);
|
||||
dispatch(
|
||||
routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): Route => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator)
|
||||
let peerID: string
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIDList[1]
|
||||
);
|
||||
} else {
|
||||
peerID = peerNameToIP[inputRoute.peer]
|
||||
}
|
||||
|
||||
return {
|
||||
id: inputRoute.id,
|
||||
network: inputRoute.network,
|
||||
network_id: inputRoute.network_id,
|
||||
description: inputRoute.description,
|
||||
peer: peerID,
|
||||
enabled: inputRoute.enabled,
|
||||
masquerade: inputRoute.masquerade,
|
||||
metric: inputRoute.metric
|
||||
} as Route
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != '') {
|
||||
const routeToSave = createRouteToSave(formRoute)
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
} else {
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peerIPToName)
|
||||
groupedDataTable.forEach((group) => {
|
||||
if (group.key == previousRouteKey) {
|
||||
group.groupedRoutes.forEach((route) => {
|
||||
let updateRoute: FormRoute = {
|
||||
...formRoute,
|
||||
id: route.id,
|
||||
peer: route.peer,
|
||||
metric: route.metric,
|
||||
enabled: (formRoute.enabled != group.enabled) ? formRoute.enabled : route.enabled
|
||||
}
|
||||
const routeToSave = createRouteToSave(updateRoute)
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRoute = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteVisible(status));
|
||||
}
|
||||
|
||||
const setSetupNewRouteHA = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(status));
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedRoute.loading) return
|
||||
setEditName(false)
|
||||
dispatch(routeActions.setRoute({
|
||||
network: '',
|
||||
network_id: '',
|
||||
description: '',
|
||||
peer: "",
|
||||
metric: 9999,
|
||||
masquerade: false,
|
||||
enabled: true
|
||||
} as Route))
|
||||
setVisibleNewRoute(false)
|
||||
setSetupNewRouteHA(false)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({...formRoute, ...data})
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
</>
|
||||
)
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
}
|
||||
|
||||
const networkRangeValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value)) {
|
||||
return Promise.reject(new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24"))
|
||||
}
|
||||
|
||||
if (Number(value.split("/")[1]) < 7) {
|
||||
return Promise.reject(new Error("Please enter a network mask larger than /7"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{route &&
|
||||
<Drawer
|
||||
headerStyle={{display: "none"}}
|
||||
forceRender={true}
|
||||
visible={setupNewRouteVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}>{`${formRoute.network_id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && !editDescription && formRoute.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && formRoute.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formRoute.id ? formRoute.network_id : 'New Route'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="network_id"
|
||||
label="Network Identifier"
|
||||
tooltip="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add an identifier for this access route',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input placeholder="e.g. aws-eu-central-1-vpc" ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"
|
||||
maxLength={40}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"}
|
||||
onClick={() => toggleEditDescription(true)}>{formRoute.description && formRoute.description.trim() !== "" ? formRoute.description : 'Add description...'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef}
|
||||
disabled={!setupNewRouteHA}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off" maxLength={200}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="network"
|
||||
label="Network Range"
|
||||
tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16"
|
||||
rules={[{validator: networkRangeValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. 172.16.0.0/16" disabled={!setupNewRouteHA}
|
||||
autoComplete="off" minLength={9} maxLength={43}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label={statusMSG}
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="peer"
|
||||
label={routingPeerMSG}
|
||||
tooltip="Assign a peer as a routing peer for the Network CIDR"
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: '100%'}}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={dropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="masquerade"
|
||||
label={masqueradeMSG}
|
||||
tooltip={masqueradeDisabledMSG}
|
||||
>
|
||||
<Switch size={"small"} disabled={!setupNewRouteHA} checked={formRoute.masquerade}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="metric"
|
||||
label="Metric"
|
||||
tooltip="Choose from 1 to 9999. Lower number has higher priority"
|
||||
>
|
||||
<InputNumber min={1} max={9999} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
You can enable high-availability by assigning the same network identifier
|
||||
and network CIDR to multiple routes.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/network-routes"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about network routes</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peers);
|
||||
groupedDataTable.forEach((group) => {
|
||||
if (group.key == previousRouteKey) {
|
||||
group.groupedRoutes.forEach((route) => {
|
||||
let updateRoute: FormRoute = {
|
||||
...formRoute,
|
||||
id: route.id,
|
||||
peer: route.peer,
|
||||
metric: route.metric,
|
||||
enabled:
|
||||
formRoute.enabled != group.enabled
|
||||
? formRoute.enabled
|
||||
: route.enabled,
|
||||
};
|
||||
const routeToSave = createRouteToSave(updateRoute);
|
||||
dispatch(
|
||||
routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
export default RouteUpdate
|
||||
const setVisibleNewRoute = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupEditRouteVisible(status));
|
||||
};
|
||||
|
||||
const setSetupNewRouteHA = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedRoute.loading) return;
|
||||
setEditName(false);
|
||||
dispatch(
|
||||
routeActions.setRoute({
|
||||
network: "",
|
||||
network_id: "",
|
||||
description: "",
|
||||
peer: "",
|
||||
metric: 9999,
|
||||
masquerade: false,
|
||||
enabled: true,
|
||||
groups: [],
|
||||
} as Route)
|
||||
);
|
||||
setVisibleNewRoute(false);
|
||||
setSetupNewRouteHA(false);
|
||||
setPreviousRouteKey("");
|
||||
setNewRoute(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({ ...formRoute, ...data });
|
||||
};
|
||||
|
||||
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
};
|
||||
|
||||
const networkRangeValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value)) {
|
||||
return Promise.reject(
|
||||
new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24")
|
||||
);
|
||||
}
|
||||
|
||||
if (Number(value.split("/")[1]) < 7) {
|
||||
return Promise.reject(
|
||||
new Error("Please enter a network mask larger than /7")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const peerValidator = (_: RuleObject, value: string) => {
|
||||
if (value == "" && newRoute) {
|
||||
return Promise.reject(new Error("Please select routing one peer"));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (setupNewRouteHA && formRoute.peer == "") {
|
||||
let [, newGroups] = getExistingAndToCreateGroupsLists(value);
|
||||
if (newGroups.length > 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"You can't add new Groups from the group update view, please remove:\"" +
|
||||
newGroups +
|
||||
'"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return selectValidator(obj, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{route && (
|
||||
<Drawer
|
||||
headerStyle={{ display: "none" }}
|
||||
forceRender={true}
|
||||
open={setupEditRouteVisible}
|
||||
bodyStyle={{ paddingBottom: 80 }}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}
|
||||
>{`${newRoute ? "Create" : "Save"}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
requiredMark={false}
|
||||
onValuesChange={onChange}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
margin: "-32px -24px 20px -24px",
|
||||
padding: "24px 24px 0 24px",
|
||||
}}
|
||||
>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{ display: "flex" }}>
|
||||
{!editName && !editDescription && formRoute.id && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
className="ant-drawer-close"
|
||||
style={{ paddingTop: 3 }}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<span
|
||||
role="img"
|
||||
aria-label="close"
|
||||
className="anticon anticon-close"
|
||||
>
|
||||
<CloseOutlined size={16} />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && formRoute.id ? (
|
||||
<div
|
||||
className={
|
||||
"access-control input-text ant-drawer-title"
|
||||
}
|
||||
onClick={() => toggleEditName(true)}
|
||||
>
|
||||
{formRoute.id ? formRoute.network_id : "New Route"}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="network_id"
|
||||
label="Network Identifier"
|
||||
tooltip="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add an identifier for this access route",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="e.g. aws-eu-central-1-vpc"
|
||||
ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
maxLength={40}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div
|
||||
className={
|
||||
"access-control input-text ant-drawer-subtitle"
|
||||
}
|
||||
onClick={() => toggleEditDescription(true)}
|
||||
>
|
||||
{formRoute.description &&
|
||||
formRoute.description.trim() !== ""
|
||||
? formRoute.description
|
||||
: "Add description..."}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto"></Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}></Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="network"
|
||||
label="Network Range"
|
||||
tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16"
|
||||
rules={[{ validator: networkRangeValidator }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="e.g. 172.16.0.0/16"
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
autoComplete="off"
|
||||
minLength={9}
|
||||
maxLength={43}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label={statusMSG}>
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="peer"
|
||||
label={routingPeerMSG}
|
||||
tooltip="Assign a peer as a routing peer for the Network CIDR"
|
||||
rules={[{ validator: peerValidator }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="masquerade"
|
||||
label={masqueradeMSG}
|
||||
tooltip={masqueradeDisabledMSG}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
checked={formRoute.masquerade}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="metric"
|
||||
label="Metric"
|
||||
tooltip="Choose from 1 to 9999. Lower number has higher priority"
|
||||
>
|
||||
<InputNumber min={1} max={9999} autoComplete="off" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Distribution groups"
|
||||
tooltip="NetBird will advertise this route to peers that belong to the following groups"
|
||||
rules={[{ validator: selectPreValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the network route"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled />
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
You can enable high-availability by assigning the same
|
||||
network identifier and network CIDR to multiple routes.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button
|
||||
icon={<QuestionCircleFilled />}
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
>
|
||||
Learn more about network routes
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Drawer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouteUpdate;
|
||||
|
||||
466
src/components/SetupKeyEdit.tsx
Normal file
@@ -0,0 +1,466 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions as setupKeyActions } from "../store/setup-key";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Breadcrumb,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
Card,
|
||||
} from "antd";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import {
|
||||
FormSetupKey,
|
||||
SetupKey,
|
||||
SetupKeyToSave,
|
||||
} from "../store/setup-key/types";
|
||||
import { formatDate, timeAgo } from "../utils/common";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
import { Group } from "../store/group/types";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { expiresInToSeconds, ExpiresInValue } from "../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
import { Container } from "./Container";
|
||||
import Paragraph from "antd/es/typography/Paragraph";
|
||||
import { EditOutlined, LockOutlined } from "@ant-design/icons";
|
||||
import { actions as personalAccessTokenActions } from "../store/personal-access-token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const { Option } = Select;
|
||||
const { Text } = Typography;
|
||||
const ExpiresInDefault: ExpiresInValue = { number: 30, interval: "day" };
|
||||
|
||||
const customExpiresFormat = (value: Date): string | null => {
|
||||
return formatDate(value);
|
||||
};
|
||||
|
||||
const SetupKeyNew = () => {
|
||||
const { optionRender, blueTagRender } = useGetGroupTagHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey);
|
||||
const savedSetupKey = useSelector(
|
||||
(state: RootState) => state.setupKey.savedSetupKey
|
||||
);
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey);
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
return () => {
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editName) return;
|
||||
|
||||
inputNameRef.current!.focus({ cursor: "end" });
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(
|
||||
groups?.filter((g) => g.name !== "All").map((g) => g.name) || []
|
||||
);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!setupKey) return;
|
||||
|
||||
const allGroups = new Map<string, Group>();
|
||||
let formKeyGroups: string[] = [];
|
||||
groups.forEach((g) => allGroups.set(g.id!, g));
|
||||
|
||||
if (setupKey.auto_groups) {
|
||||
formKeyGroups = setupKey.auto_groups
|
||||
.filter((g) => allGroups.get(g))
|
||||
.map((g) => allGroups.get(g)!.name);
|
||||
}
|
||||
|
||||
const fSetupKey = {
|
||||
...setupKey,
|
||||
autoGroupNames: setupKey.auto_groups ? formKeyGroups : [],
|
||||
expiresInFormatted: ExpiresInDefault,
|
||||
exp: moment(setupKey.expires),
|
||||
last: moment(setupKey.last_used),
|
||||
} as FormSetupKey;
|
||||
|
||||
form.setFieldsValue(fSetupKey);
|
||||
setFormSetupKey(fSetupKey);
|
||||
}, [setupKey]);
|
||||
|
||||
const createSetupKeyToSave = (): SetupKeyToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter((g) => formSetupKey.autoGroupNames.includes(g.name))
|
||||
.map((g) => g.id || "") || [];
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.name);
|
||||
const groupsToCreate = formSetupKey.autoGroupNames.filter(
|
||||
(s) => !allGroupsNames.includes(s)
|
||||
);
|
||||
|
||||
const expiresIn = expiresInToSeconds(formSetupKey.expiresInFormatted);
|
||||
return {
|
||||
id: formSetupKey.id,
|
||||
name: formSetupKey.name,
|
||||
type: formSetupKey.type,
|
||||
auto_groups: autoGroups,
|
||||
revoked: formSetupKey.revoked,
|
||||
groupsToCreate: groupsToCreate,
|
||||
expires_in: expiresIn,
|
||||
usage_limit: formSetupKey.usage_limit,
|
||||
} as SetupKeyToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (e) {
|
||||
const errorFields = (e as any).errorFields;
|
||||
return console.log("errorInfo", errorFields);
|
||||
}
|
||||
|
||||
const setupKeyToSave = createSetupKeyToSave();
|
||||
dispatch(
|
||||
setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: setupKeyToSave,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const setVisibleNewSetupKey = (status: boolean) => {
|
||||
form.resetFields();
|
||||
dispatch(setupKeyActions.setSetupEditKeyVisible(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedSetupKey.loading) return;
|
||||
|
||||
dispatch(
|
||||
setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "one-off",
|
||||
key: "",
|
||||
last_used: "",
|
||||
expires: "",
|
||||
state: "valid",
|
||||
auto_groups: [] as string[],
|
||||
usage_limit: 0,
|
||||
used_times: 0,
|
||||
expires_in: 0,
|
||||
} as SetupKey)
|
||||
);
|
||||
setFormSetupKey({} as FormSetupKey);
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormSetupKey({ ...formSetupKey, ...data });
|
||||
};
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Row style={{ padding: "0 8px 4px" }}>
|
||||
<Col flex="auto">
|
||||
<span style={{ color: "#9CA3AF" }}>
|
||||
Add new group by pressing "Enter"
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
const changesDetected = (): boolean => {
|
||||
return (
|
||||
formSetupKey.name == null ||
|
||||
formSetupKey.name !== setupKey.name ||
|
||||
groupsChanged() ||
|
||||
formSetupKey.usage_limit !== setupKey.usage_limit
|
||||
);
|
||||
};
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (
|
||||
setupKey &&
|
||||
setupKey.auto_groups &&
|
||||
formSetupKey.autoGroupNames.length !== setupKey.auto_groups.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const formGroupIds =
|
||||
groups
|
||||
?.filter((g) => formSetupKey.autoGroupNames.includes(g.name))
|
||||
.map((g) => g.id || "") || [];
|
||||
|
||||
return (
|
||||
setupKey.auto_groups?.filter((g) => !formGroupIds.includes(g)).length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const getFormKey = (key: string) => {
|
||||
if (key) return key.substring(0, 4).concat("****");
|
||||
};
|
||||
|
||||
const onBreadcrumbUsersClick = () => {
|
||||
if (savedSetupKey.loading) return;
|
||||
// dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null));
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "25px" }}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Setup Keys</a>,
|
||||
},
|
||||
{
|
||||
title: setupKey.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card bordered={true} style={{ marginBottom: "7px", border: "none" }}>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<h3 style={{ fontSize: "22px", fontWeight: "500",marginBottom:"30px" }}>
|
||||
{setupKey.name}
|
||||
</h3>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
form={form}
|
||||
onValuesChange={onChange}
|
||||
initialValues={{
|
||||
expiresIn: ExpiresInDefault,
|
||||
usage_limit: 1,
|
||||
}}
|
||||
>
|
||||
<Row style={{ marginTop: "10px" }}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
style={{ paddingRight: "70px" }}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
fontWeight: "500",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Key
|
||||
<Tag
|
||||
color={`${
|
||||
formSetupKey.state === "valid" ? "green" : "red"
|
||||
}`}
|
||||
style={{
|
||||
marginLeft: "10px",
|
||||
borderRadius: "2px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{formSetupKey.state}
|
||||
</Tag>
|
||||
</Paragraph>
|
||||
<Input
|
||||
style={{ marginTop: "8px" }}
|
||||
disabled
|
||||
value={getFormKey(formSetupKey.key)}
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
></Paragraph>
|
||||
{formSetupKey.type === "one-off" ? "One-off" : "Reusable"},
|
||||
available uses
|
||||
</Paragraph>
|
||||
<Col>
|
||||
<Input
|
||||
disabled
|
||||
value={
|
||||
formSetupKey.type === "reusable" &&
|
||||
formSetupKey.usage_limit === 0
|
||||
? "unlimited"
|
||||
: formSetupKey.usage_limit - formSetupKey.used_times
|
||||
}
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
style={{ marginTop: "8px" }}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ marginTop: "30px" }}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
style={{ paddingRight: "70px" }}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Auto-assigned groups
|
||||
</Paragraph>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
style={{ marginTop: "8px", marginBottom: 0 }}
|
||||
name="autoGroupNames"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the key"
|
||||
tagRender={blueTagRender}
|
||||
dropdownRender={dropDownRender}
|
||||
// enabled only when we have a new key !setupkey.id or when the key is valid
|
||||
disabled={!(!setupKey.id || setupKey.valid)}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Paragraph style={{ margin: 0, fontWeight: "500" }}>
|
||||
Expires
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Input
|
||||
style={{ marginTop: "8px" }}
|
||||
disabled
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
value={customExpiresFormat(new Date(formSetupKey.expires))!}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ marginTop: "40px", marginBottom: "28px" }}>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
>
|
||||
{" "}
|
||||
Setup Keys
|
||||
</a>
|
||||
</Text>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
<Container
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
padding: 0,
|
||||
gap: "10px",
|
||||
}}
|
||||
key={0}
|
||||
>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedSetupKey.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
{`${formSetupKey.id ? "Save" : "Create"} key`}
|
||||
</Button>
|
||||
</Container>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupKeyNew;
|
||||
666
src/components/UserEdit.tsx
Normal file
@@ -0,0 +1,666 @@
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
Button, Card,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
List, Modal,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space, Switch, Table,
|
||||
Tag, Typography
|
||||
} from "antd";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {FormUser, User, UserToSave} from "../store/user/types";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
import {PersonalAccessToken, PersonalAccessTokenCreate, SpecificPAT} from "../store/personal-access-token/types";
|
||||
import tableSpin from "./Spin";
|
||||
import AddPATPopup from "./popups/AddPATPopup";
|
||||
import {fullDate} from "../utils/common";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import Column from "antd/lib/table/Column";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Option} = Select;
|
||||
const {Meta} = Card;
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
|
||||
interface TokenDataTable extends PersonalAccessToken {
|
||||
key: string
|
||||
status: string
|
||||
created_by_email: string
|
||||
}
|
||||
|
||||
const UserEdit = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
optionRender,
|
||||
blueTagRender
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const personalAccessTokens = useSelector((state: RootState) => state.personalAccessToken.data);
|
||||
const tab = useSelector((state: RootState) => state.user.userTabOpen)
|
||||
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
const [tokenTable, setTokenTable] = useState([] as TokenDataTable[]);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [currentGroups, setCurrentGroups] = useState([] as string[])
|
||||
|
||||
const [formUser, setFormUser] = useState({} as FormUser)
|
||||
const [form] = Form.useForm()
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
setFormUser({} as FormUser)
|
||||
dispatch(userActions.setEditUserPopupVisible(false));
|
||||
}
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => values.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = values.autoGroupsNames.filter((s: string) => !allGroupsNames.includes(s))
|
||||
let userID = user ? user.id : ''
|
||||
let isServiceUser = user ? user.is_service_user : false
|
||||
return {
|
||||
id: userID,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: isServiceUser,
|
||||
is_blocked: values.is_blocked
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(users) {
|
||||
let currentUser = users.find((user) => user.is_current)
|
||||
if(currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
}
|
||||
}
|
||||
}, [users])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
dispatch(userActions.setEditUserPopupVisible(false));
|
||||
dispatch(userActions.setUser(null as unknown as User))
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const onClickAddNewPersonalAccessToken = () => {
|
||||
dispatch(personalAccessTokenActions.setPersonalAccessToken({
|
||||
user_id: "",
|
||||
name: "",
|
||||
expires_in: 7
|
||||
} as PersonalAccessTokenCreate))
|
||||
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(true));
|
||||
}
|
||||
|
||||
const onBreadcrumbUsersClick = (key: string) => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
dispatch(userActions.setUserTabOpen(key))
|
||||
}
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const transformTokenTable = (d: PersonalAccessToken[]): TokenDataTable[] => {
|
||||
if (!d) {
|
||||
return []
|
||||
}
|
||||
return d.map(p => ({
|
||||
key: p.id,
|
||||
status: Date.parse(p.expiration_date) > Date.now() ? "valid" : "expired",
|
||||
created_by_email: getEmail(p),
|
||||
...p
|
||||
} as TokenDataTable))
|
||||
}
|
||||
|
||||
const getEmail = (token: PersonalAccessToken): string => {
|
||||
return users.find(u => u.id === token.created_by)?.email || ""
|
||||
}
|
||||
|
||||
const showConfirmDelete = (token: TokenDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: <span className="font-500">Delete token {token.name}</span>,
|
||||
width: 600,
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to delete this token?</Paragraph>
|
||||
</Space>
|
||||
),
|
||||
onOk() {
|
||||
dispatch(
|
||||
personalAccessTokenActions.deletePersonalAccessToken.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: {
|
||||
user_id: user.id,
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
} as SpecificPAT,
|
||||
})
|
||||
);
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTokenTable(transformTokenTable(personalAccessTokens))
|
||||
}, [personalAccessTokens, users])
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
// @ts-ignore
|
||||
setCurrentGroups(groups.filter(g => g.name != "All" && user.auto_groups.includes(g.id)).map(g => g.name) || [])
|
||||
}
|
||||
}, [groups, user])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
dispatch(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (user.is_current || user.is_service_user) {
|
||||
dispatch(personalAccessTokenActions.getPersonalAccessTokens.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: user.id
|
||||
}))
|
||||
}
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
if (user && currentGroups) {
|
||||
form.setFieldsValue({
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
email: user.email,
|
||||
is_blocked: user.is_blocked,
|
||||
autoGroupsNames: currentGroups,
|
||||
})
|
||||
}
|
||||
}, [form, user, currentGroups])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ paddingTop: "13px" }}>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "30px" }}
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<a onClick={() => onBreadcrumbUsersClick("Users")}>
|
||||
All Users
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <a onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
|
||||
// menu: { items: menuItems },
|
||||
},
|
||||
{
|
||||
title: user.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card
|
||||
bordered={true}
|
||||
loading={loading}
|
||||
style={{ marginBottom: "7px" }}
|
||||
>
|
||||
<h3 style={{fontSize:"22px",fontWeight:"500",marginBottom:"25px"}}>{user.name}</h3>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
name: formUser.name,
|
||||
role: formUser.role,
|
||||
email: formUser.email,
|
||||
is_blocked: formUser.is_blocked,
|
||||
autoGroupsNames: formUser.autoGroupsNames,
|
||||
}}
|
||||
>
|
||||
<Row style={{ paddingBottom: "15px" }}>
|
||||
{!user.is_service_user && (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<Text style={{}}>Email</Text>}
|
||||
style={{ marginRight: "70px", fontWeight: "500" }}
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{ color: "#8c8c8c" }}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label={<Text style={{fontWeight:"500"}}>Role</Text>}
|
||||
style={{ marginRight: "50px", fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
disabled={user.is_current}
|
||||
>
|
||||
<Option value="admin">
|
||||
<Text type={"secondary"}>admin</Text>
|
||||
</Option>
|
||||
<Option value="user">
|
||||
<Text type={"secondary"}>user</Text>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!user.is_service_user && (
|
||||
<Row style={{ paddingBottom: "15px" }}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={11}
|
||||
lg={11}
|
||||
xl={11}
|
||||
xxl={11}
|
||||
span={11}
|
||||
>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label={
|
||||
<Text style={{ fontWeight: "500" }}>
|
||||
Auto-assigned groups
|
||||
</Text>
|
||||
}
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ marginRight: "70px" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={blueTagRender}
|
||||
dropdownRender={dropDownRender}
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{!user.is_current && isAdmin && (
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5}
|
||||
span={5}
|
||||
>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
name="is_blocked"
|
||||
label="Block user"
|
||||
style={{ marginRight: "50px", fontWeight: "500" }}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Space style={{ display: "flex", justifyContent: "start" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{user && (user.is_current || user.is_service_user) && (
|
||||
<Card
|
||||
bordered={true}
|
||||
loading={loading}
|
||||
style={{ marginBottom: "7px" }}
|
||||
>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Access tokens
|
||||
</Paragraph>
|
||||
<Row
|
||||
gutter={21}
|
||||
style={{ marginTop: "-16px", marginBottom: "10px" }}
|
||||
>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={20}
|
||||
lg={20}
|
||||
xl={20}
|
||||
xxl={20}
|
||||
span={20}
|
||||
>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ textAlign: "left", whiteSpace: "pre-line" }}
|
||||
>
|
||||
Access tokens give access to NetBird API
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={1}
|
||||
lg={1}
|
||||
xl={1}
|
||||
xxl={1}
|
||||
span={1}
|
||||
style={{ marginTop: "-16px" }}
|
||||
>
|
||||
{personalAccessTokens &&
|
||||
personalAccessTokens.length > 0 && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClickAddNewPersonalAccessToken}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{personalAccessTokens && personalAccessTokens.length > 0 && (
|
||||
<Table
|
||||
size={"small"}
|
||||
style={{ marginTop: "-10px" }}
|
||||
showHeader={false}
|
||||
scroll={{ x: 800 }}
|
||||
pagination={false}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={tokenTable}
|
||||
>
|
||||
<Column
|
||||
className={"non-highlighted-table-column"}
|
||||
sorter={(a, b) =>
|
||||
(a as TokenDataTable).created_at.localeCompare(
|
||||
(b as TokenDataTable).created_at
|
||||
)
|
||||
}
|
||||
defaultSortOrder="descend"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Col>
|
||||
<Badge
|
||||
status={
|
||||
(record as TokenDataTable).status ===
|
||||
"valid"
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
style={{
|
||||
marginTop: "1px",
|
||||
marginRight: "5px",
|
||||
marginLeft: "0px",
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Paragraph
|
||||
style={{
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
}}
|
||||
>
|
||||
{(record as TokenDataTable).name}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: "400",
|
||||
margin: "0px",
|
||||
marginTop: "-2px",
|
||||
padding: "0px",
|
||||
}}
|
||||
>
|
||||
{"Created" +
|
||||
((record as TokenDataTable)
|
||||
.created_by_email && user.is_service_user
|
||||
? " by " +
|
||||
(record as TokenDataTable)
|
||||
.created_by_email
|
||||
: "") +
|
||||
" on " +
|
||||
fullDate(
|
||||
(record as TokenDataTable).created_at
|
||||
)}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ textAlign: "left", fontSize: "11px" }}
|
||||
>
|
||||
Expires on
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
marginTop: "-10px",
|
||||
marginBottom: "0",
|
||||
fontSize: "15px",
|
||||
}}
|
||||
>
|
||||
{fullDate(
|
||||
(record as TokenDataTable).expiration_date
|
||||
)}
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ textAlign: "left", fontSize: "11px" }}
|
||||
>
|
||||
Last used
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
marginTop: "-10px",
|
||||
marginBottom: "0",
|
||||
fontSize: "15px",
|
||||
}}
|
||||
>
|
||||
{(record as TokenDataTable).last_used
|
||||
? fullDate((record as TokenDataTable).last_used)
|
||||
: "Never"}
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
align="right"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button
|
||||
danger={true}
|
||||
type={"text"}
|
||||
onClick={() => {
|
||||
showConfirmDelete(record as TokenDataTable);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
)}
|
||||
<Divider style={{ marginTop: "-12px" }}></Divider>
|
||||
{(personalAccessTokens === null ||
|
||||
personalAccessTokens.length === 0) && (
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="small"
|
||||
align="start"
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: "35px 0px",
|
||||
marginTop: "-40px",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{ textAlign: "start", whiteSpace: "pre-line" }}
|
||||
>
|
||||
You don’t have any access tokens yet
|
||||
</Paragraph>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClickAddNewPersonalAccessToken}
|
||||
>
|
||||
Create token
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<AddPATPopup />
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default UserEdit;
|
||||
@@ -1,413 +0,0 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {Alert, Button, Col, Divider, Drawer, Form, Input, Modal, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {CloseOutlined, EditOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {Group} from "../store/group/types";
|
||||
import {FormUser, User, UserToSave} from "../store/user/types";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
|
||||
const {Paragraph, Text} = Typography;
|
||||
|
||||
const {confirm} = Modal;
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const UserUpdate = () => {
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [editName, setEditName] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
|
||||
const [formUser, setFormUser] = useState({} as FormUser)
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
useEffect(() => {
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
} else {
|
||||
setCurrentUser({} as User)
|
||||
}
|
||||
|
||||
}, [oidcUser, users])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
|
||||
let allGroups = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
allGroups.set(g.id!, g)
|
||||
})
|
||||
|
||||
if (!user.auto_groups) {
|
||||
user.auto_groups = []
|
||||
}
|
||||
let formKeyGroups = user.auto_groups.filter(g => allGroups.get(g)).map(g => allGroups.get(g)!.name)
|
||||
|
||||
const fUser = {
|
||||
...user,
|
||||
autoGroupsNames: user.auto_groups ? formKeyGroups : [],
|
||||
} as FormUser
|
||||
setFormUser(fUser)
|
||||
form.setFieldsValue(fUser)
|
||||
}, [user])
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const createUserToSave = (): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => formUser.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = formUser.autoGroupsNames.filter(s => !allGroupsNames.includes(s))
|
||||
return {
|
||||
id: formUser.id,
|
||||
email: formUser.email,
|
||||
role: formUser.role,
|
||||
name: formUser.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
const showConfirmChangeRole = (userToSave: UserToSave) => {
|
||||
let content = <Paragraph>With this action, you will remove the administrative privileges of your user.
|
||||
Your user will be limited to read-only operations in this account. Are you sure?</Paragraph>
|
||||
let contentModule = <div>{content}</div>
|
||||
|
||||
let name = formUser ? formUser.email : ''
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Update user \"" + name + "\"",
|
||||
width: 600,
|
||||
content: contentModule,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// check if currentUser (who is doing the modification) removes the administrative privileges from themselves
|
||||
const isShowConfirmWarning = (userToSave: UserToSave): boolean => {
|
||||
return currentUser.id == userToSave.id && currentUser.role === "admin" && userToSave.role !== "admin"
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave()
|
||||
if (isShowConfirmWarning(userToSave)) {
|
||||
showConfirmChangeRole(userToSave)
|
||||
} else {
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser({
|
||||
id: "",
|
||||
email: "",
|
||||
role: "",
|
||||
status: "",
|
||||
auto_groups: [],
|
||||
name: user.name
|
||||
} as User));
|
||||
setFormUser({} as FormUser)
|
||||
toggleEditName(false)
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormUser({...formUser, ...data})
|
||||
}
|
||||
|
||||
const changesDetected = (): boolean => {
|
||||
return emailChanged() || nameChanged() || groupsChanged() || roleChanged()
|
||||
}
|
||||
|
||||
const emailChanged = (): boolean => {
|
||||
return formUser.email !== user.email
|
||||
}
|
||||
|
||||
const roleChanged = (): boolean => {
|
||||
return formUser.role !== user.role
|
||||
}
|
||||
|
||||
const nameChanged = (): boolean => {
|
||||
return formUser.name !== user.name
|
||||
}
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (!formUser.autoGroupsNames) {
|
||||
return false
|
||||
}
|
||||
if (formUser.autoGroupsNames.length != user.auto_groups.length) {
|
||||
return true
|
||||
}
|
||||
const formGroupIds = groups?.filter(g => formUser.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
|
||||
return user.auto_groups?.filter(g => !formGroupIds.includes(g)).length > 0
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{user &&
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
open={updateUserDrawerVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={savedUser.loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedUser.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}>{`${formUser.id ? 'Save' : 'Invite'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}
|
||||
initialValues={{
|
||||
["role"]: formUser.role
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
{/*Close Icon*/}
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && user.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
{/* Name Label*/}
|
||||
<Col flex="auto">
|
||||
{!editName && user.id && formUser.name !== "" ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formUser.name ? formUser.name : formUser.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: false,
|
||||
message: 'Please add a new name for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={formUser.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="Email"
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="Role"
|
||||
>
|
||||
<Select
|
||||
style={{width: '100%'}}
|
||||
disabled={currentUser.role != null && currentUser.role !== "admin"}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label="Auto-assigned groups"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
disabled={currentUser.role != null && currentUser.role !== "admin"}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
</Col>
|
||||
{currentUser && currentUser.role !== "admin" && (
|
||||
<div>
|
||||
<Col span={24}>
|
||||
<Alert
|
||||
message={<div style={{color: "#5a5c5a"}}>
|
||||
You are not an administrator, therefore you can't update users.</div>}
|
||||
showIcon={false}
|
||||
type="warning"/>
|
||||
</Col>
|
||||
<br></br>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserUpdate
|
||||
@@ -1,55 +0,0 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
|
||||
export const LinuxTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install Brew (package manager)',
|
||||
commands: (
|
||||
<Button type="primary" href="https://brew.sh/" target="_blank">Download Brew</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`# for CLI only`,
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`# for GUI package`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: [
|
||||
`sudo netbird up`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Get your IP address:',
|
||||
commands: [
|
||||
`sudo ifconfig utun100`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinuxTab
|
||||
@@ -1,82 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import { getConfig } from "../../config";
|
||||
import Paragraph from 'antd/lib/skeleton/Paragraph';
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
|
||||
|
||||
export const UbuntuTab = () => {
|
||||
|
||||
const formatNetBirdUP = () => {
|
||||
let cmd = "sudo netbird up"
|
||||
if (grpcApiOrigin) {
|
||||
cmd = "sudo netbird up --management-url " + grpcApiOrigin
|
||||
}
|
||||
return [
|
||||
cmd
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Add repository:',
|
||||
commands: [
|
||||
`sudo apt install ca-certificates curl gnupg -y`,
|
||||
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/wiretrustee.gpg`,
|
||||
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`# for CLI only`,
|
||||
`sudo apt-get install netbird`,
|
||||
`# for GUI package`,
|
||||
`sudo apt-get install netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Get your IP address:',
|
||||
commands: [
|
||||
`ip addr show wt0`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
])
|
||||
|
||||
/*const clickTest = () => {
|
||||
steps.push({
|
||||
key: steps.length+1,
|
||||
title: `Test ${steps.length+1}`,
|
||||
commands: [`hi lorena!`].join('\n'),
|
||||
copied: false
|
||||
})
|
||||
console.log(steps)
|
||||
setSteps([...steps])
|
||||
}*/
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default UbuntuTab
|
||||
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
@@ -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 |
204
src/components/popups/AddPATPopup.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import {useGetTokenSilently} from "../../utils/token";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Button, Col, Divider, Form, Input, InputNumber, message, Modal, Row, Space, Typography} from "antd";
|
||||
import {Container} from "../Container";
|
||||
import {CheckOutlined, CopyOutlined, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {actions as personalAccessTokenActions} from "../../store/personal-access-token";
|
||||
import {PersonalAccessTokenCreate} from "../../store/personal-access-token/types";
|
||||
import {copyToClipboard} from "../../utils/common";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
|
||||
const ExpiresInDefault = 30
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
const AddPATPopup = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
|
||||
const addTokenModalOpen = useSelector((state: RootState) => state.personalAccessToken.newPersonalAccessTokenPopupVisible)
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [showPlainToken, setShowPlainToken] = useState(false);
|
||||
const [tokenCopied, setTokenCopied] = useState(false);
|
||||
const [plainToken, setPlainToken] = useState("")
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const savedPersonalAccessToken = useSelector((state: RootState) => state.personalAccessToken.savedPersonalAccessToken);
|
||||
|
||||
const onCopyClick = (text: string, copied: boolean) => {
|
||||
copyToClipboard(text)
|
||||
setTokenCopied(true)
|
||||
if (copied) {
|
||||
setTimeout(() => {
|
||||
onCopyClick(text, false)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setShowPlainToken(false)
|
||||
setTokenCopied(false)
|
||||
if (savedPersonalAccessToken.loading) return
|
||||
dispatch(personalAccessTokenActions.setPersonalAccessToken({
|
||||
user_id: "",
|
||||
name: "",
|
||||
expires_in: 7
|
||||
} as PersonalAccessTokenCreate))
|
||||
form.resetFields()
|
||||
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(false));
|
||||
dispatch(personalAccessTokenActions.setSavedPersonalAccessToken({...savedPersonalAccessToken, success: false}));
|
||||
dispatch(personalAccessTokenActions.resetSavedPersonalAccessToken(null))
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let personalAccessTokenToSave = {
|
||||
user_id: user.id,
|
||||
name: values.name,
|
||||
expires_in: values.expires_in,
|
||||
} as PersonalAccessTokenCreate
|
||||
dispatch(personalAccessTokenActions.savePersonalAccessToken.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: personalAccessTokenToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedPersonalAccessToken.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedPersonalAccessToken.success) {
|
||||
message.destroy(createKey)
|
||||
setPlainToken(savedPersonalAccessToken.data.plain_token)
|
||||
setShowPlainToken(true)
|
||||
form.resetFields()
|
||||
} else if (savedPersonalAccessToken.error) {
|
||||
message.error({
|
||||
content: 'Failed to create personal access token. You might not have enough permissions.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(false));
|
||||
setShowPlainToken(false)
|
||||
setTokenCopied(false)
|
||||
dispatch(personalAccessTokenActions.setSavedPersonalAccessToken({...savedPersonalAccessToken, error: null}));
|
||||
dispatch(personalAccessTokenActions.resetSavedPersonalAccessToken(null))
|
||||
}
|
||||
}, [savedPersonalAccessToken])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={addTokenModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
{!showPlainToken && <Button disabled={savedPersonalAccessToken.loading} onClick={onCancel}>{"Cancel"}</Button>}
|
||||
{!showPlainToken && <Button type="primary" disabled={showPlainToken}
|
||||
onClick={handleFormSubmit}>{"Create token"}</Button>}
|
||||
{showPlainToken && <Button type="primary" disabled={!showPlainToken} onClick={onCancel}>Done</Button>}
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "18px",fontWeight:"500"}}>
|
||||
{showPlainToken ? "Token created successfully!" : "Create token"}
|
||||
</Paragraph>
|
||||
{!showPlainToken && <Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}>
|
||||
{"Use this token to access NetBird's public API"}
|
||||
</Paragraph>}
|
||||
{showPlainToken && <Paragraph type={"secondary"} style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "25px",
|
||||
}}>{"This token won't be shown again, so be sure to copy and store it in a secure location"}</Paragraph>}
|
||||
{!showPlainToken && <Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
expires_in: ExpiresInDefault,
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set an easily identifiable name for your token</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
style={{marginTop: "-10px"}}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a name for this token',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={"for example \"Infra token\""}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24} style={{textAlign: "left", marginTop: "10px"}}>
|
||||
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Expires in</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Number of days this token will be valid for</Paragraph>
|
||||
<Form.Item
|
||||
name="expires_in"
|
||||
style={{marginTop: "-10px"}}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 365,
|
||||
message: 'The expiration should be set between 1 and 365 days'
|
||||
}]}>
|
||||
<InputNumber addonAfter=" Days" style={{maxWidth: "150px"}}/>
|
||||
</Form.Item>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Should be between 1 and 365 days</Paragraph>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}
|
||||
href="https://docs.netbird.io/how-to/access-netbird-public-api">Learn more about personal access tokens</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>}
|
||||
{showPlainToken &&
|
||||
<Input style={{marginTop: "-15px", marginBottom: "25px"}} suffix={
|
||||
!tokenCopied ? <Button type="text" size="middle" className="btn-copy-code" icon={<CopyOutlined/>}
|
||||
style={{color: "rgb(107, 114, 128)", marginTop: "-1px"}}
|
||||
onClick={() => onCopyClick(plainToken, true)}/>
|
||||
: <Button type="text" size="middle" className="btn-copy-code" icon={<CheckOutlined/>}
|
||||
style={{color: "green", marginTop: "-1px"}}/>
|
||||
}
|
||||
defaultValue={plainToken}
|
||||
readOnly={true}
|
||||
></Input>}
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPATPopup
|
||||
362
src/components/popups/AddServiceUserPopup.tsx
Normal file
@@ -0,0 +1,362 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { Container } from "../Container";
|
||||
import { CloseOutlined } from "@ant-design/icons";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { useGetTokenSilently } from "../../utils/token";
|
||||
import { actions as userActions } from "../../store/user";
|
||||
import { actions as groupActions } from "../../store/group";
|
||||
import { User, UserToSave } from "../../store/user/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const AddServiceUserPopup = () => {
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const addServiceUserModalOpen = useSelector(
|
||||
(state: RootState) => state.user.addServiceUserPopupVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter(
|
||||
(g) =>
|
||||
values.autoGroupsNames && values.autoGroupsNames.includes(g.name)
|
||||
)
|
||||
.map((g) => g.id || "") || [];
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.name);
|
||||
const groupsToCreate =
|
||||
values.autoGroupsNames?.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
) || [];
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: true,
|
||||
} as UserToSave;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return;
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
form.resetFields();
|
||||
dispatch(
|
||||
userActions.getServiceUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const { label, value, closable, onClose } = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = [];
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v);
|
||||
}
|
||||
});
|
||||
setSelectedTagGroups(validatedValues);
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Row style={{ padding: "0 8px 4px" }}>
|
||||
<Col flex="auto">
|
||||
<span style={{ color: "#9CA3AF" }}>
|
||||
Add new group by pressing "Enter"
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = "";
|
||||
const g = groups.find((_g) => _g.name === label);
|
||||
if (g)
|
||||
peersCount = ` - ${g.peers_count || 0} ${
|
||||
!g.peers_count || parseInt(g.peers_count) !== 1 ? "peers" : "peer"
|
||||
} `;
|
||||
return (
|
||||
<>
|
||||
<Tag color="blue" style={{ marginRight: 3 }}>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(
|
||||
groups?.filter((g) => g.name != "All").map((g) => g.name) || []
|
||||
);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={addServiceUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Create user
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
textAlign: "start",
|
||||
marginLeft: "-15px",
|
||||
marginRight: "-15px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{"Add service user"}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}
|
||||
>
|
||||
{
|
||||
"Service users are non-login users that are not associated with any specific person."
|
||||
}
|
||||
</Paragraph>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
["role"]: "user",
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a name to easily identify the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a new name for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Ansible user"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Role
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ fontSize: "14px", marginTop: "-15px" }}
|
||||
>
|
||||
Set a role for the user to assign access permissions
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select a role for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select style={{ width: "120px" }}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
|
||||
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
|
||||
{/* <Form.Item*/}
|
||||
{/* name="autoGroupsNames"*/}
|
||||
{/* label="Auto-assigned groups"*/}
|
||||
{/* tooltip="Every peer enrolled with this user will be automatically added to these groups"*/}
|
||||
{/* rules={[{validator: selectValidator}]}*/}
|
||||
{/* >*/}
|
||||
{/* <Select mode="tags"*/}
|
||||
{/* style={{width: '100%'}}*/}
|
||||
{/* placeholder="Associate groups with the user"*/}
|
||||
{/* tagRender={tagRender}*/}
|
||||
{/* onChange={handleChangeTags}*/}
|
||||
{/* dropdownRender={dropDownRender}*/}
|
||||
{/* >*/}
|
||||
{/* {*/}
|
||||
{/* tagGroups.map(m =>*/}
|
||||
{/* <Option key={m}>{optionRender(m)}</Option>*/}
|
||||
{/* )*/}
|
||||
{/* }*/}
|
||||
{/* </Select>*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddServiceUserPopup;
|
||||
366
src/components/popups/InviteUserPopup.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { Container } from "../Container";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { useGetTokenSilently } from "../../utils/token";
|
||||
import { actions as userActions } from "../../store/user";
|
||||
import { actions as groupActions } from "../../store/group";
|
||||
import { User, UserToSave } from "../../store/user/types";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
import { QuestionCircleFilled } from "@ant-design/icons";
|
||||
import { useGetGroupTagHelpers } from "../../utils/groups";
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const InviteUserPopup = () => {
|
||||
const { optionRender, blueTagRender } = useGetGroupTagHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const inviteUserModalOpen = useSelector(
|
||||
(state: RootState) => state.user.inviteUserPopupVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter(
|
||||
(g) =>
|
||||
values.autoGroupsNames && values.autoGroupsNames.includes(g.name)
|
||||
)
|
||||
.map((g) => g.id || "") || [];
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.name);
|
||||
const groupsToCreate =
|
||||
values.autoGroupsNames?.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
) || [];
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
email: values.email,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: false,
|
||||
} as UserToSave;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return;
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
form.resetFields();
|
||||
dispatch(
|
||||
userActions.getRegularUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = [];
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v);
|
||||
}
|
||||
});
|
||||
setSelectedTagGroups(validatedValues);
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Row style={{ padding: "0 8px 4px" }}>
|
||||
<Col flex="auto">
|
||||
<span style={{ color: "#9CA3AF" }}>
|
||||
Add new group by pressing "Enter"
|
||||
</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg
|
||||
width="14"
|
||||
height="12"
|
||||
viewBox="0 0 14 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"
|
||||
/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(
|
||||
groups?.filter((g) => g.name != "All").map((g) => g.name) || []
|
||||
);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={inviteUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Invite
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
textAlign: "start",
|
||||
marginLeft: "-15px",
|
||||
marginRight: "-15px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{"Invite user"}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}
|
||||
>
|
||||
{"Invite a user to your network and set their permissions."}
|
||||
</Paragraph>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
["role"]: "user",
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a name to easily identify the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a name for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Max Schmidt"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Email
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Provide the email address of the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a valid email address for this user",
|
||||
whitespace: false,
|
||||
pattern: new RegExp(
|
||||
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
|
||||
),
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "max.schmidt@gmail.com"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Role
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a role for the user to assign access permissions
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select a role for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select style={{ width: "120px" }}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Auto-assigned groups
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Add groups, that will be assigned to peers added by this user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{tagGroups.map((m) => (
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Button
|
||||
icon={<QuestionCircleFilled />}
|
||||
type="link"
|
||||
target="_blank"
|
||||
disabled={true}
|
||||
style={{ marginTop: "20px", marginBottom: "20px" }}
|
||||
href="https://docs.netbird.io/how-to/access-netbird-public-api"
|
||||
>
|
||||
Learn more about user
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteUserPopup;
|
||||
101
src/components/popups/addpeer/addpeer/AddPeerPopup.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Tabs, TabsProps} from "antd";
|
||||
import Icon, {AndroidFilled, AppleFilled, WindowsFilled} from "@ant-design/icons";
|
||||
import {ReactComponent as LinuxSVG} from "../../../icons/terminal_icon.svg";
|
||||
import UbuntuTab from "./UbuntuTab";
|
||||
import {ReactComponent as DockerSVG} from "../../../icons/docker_icon.svg";
|
||||
import Paragraph from "antd/lib/typography/Paragraph";
|
||||
import WindowsTab from "./WindowsTab";
|
||||
import MacTab from "./MacTab";
|
||||
import Link from "antd/lib/typography/Link";
|
||||
import DockerTab from "./DockerTab";
|
||||
import AndroidTab from "./AndroidTab";
|
||||
|
||||
type Props = {
|
||||
greeting?: string;
|
||||
headline: string;
|
||||
};
|
||||
|
||||
const detectOSTab = () => {
|
||||
let os = 1;
|
||||
if (navigator.userAgent.indexOf("Win") !== -1) os = 2;
|
||||
if (navigator.userAgent.indexOf("Mac") !== -1) os = 3;
|
||||
if (navigator.userAgent.indexOf("X11") !== -1) os = 1;
|
||||
if (navigator.userAgent.indexOf("Linux") !== -1) os = 1
|
||||
return os
|
||||
}
|
||||
|
||||
export const AddPeerPopup: React.FC<Props> = ({
|
||||
greeting,
|
||||
headline,
|
||||
}) => {
|
||||
|
||||
const [openTab, setOpenTab] = useState(detectOSTab);
|
||||
|
||||
const [width, setWidth] = useState<number>(window.innerWidth);
|
||||
const isMobile = width <= 768;
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: "1",
|
||||
label: <span><Icon component={LinuxSVG}/>Linux</span>,
|
||||
children: <UbuntuTab/>,
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: <span><WindowsFilled/>Windows</span>,
|
||||
children: <WindowsTab/>,
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: <span><AppleFilled/>MacOS</span>,
|
||||
children: <MacTab/>,
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
label: <span><AndroidFilled/>Android</span>,
|
||||
children: <AndroidTab/>,
|
||||
},
|
||||
{
|
||||
key: "5",
|
||||
label: <span><Icon component={DockerSVG}/>Docker</span>,
|
||||
children: <DockerTab/>,
|
||||
}
|
||||
];
|
||||
|
||||
return <>
|
||||
{greeting && <Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em", marginBottom: -10}}>
|
||||
{greeting}
|
||||
</Paragraph>}
|
||||
<Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em"}}>
|
||||
{headline}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-15px",
|
||||
textAlign: "center",
|
||||
whiteSpace: "pre-line",
|
||||
}}>
|
||||
To get started, install NetBird and log in using your {"\n"} email account.
|
||||
</Paragraph>
|
||||
|
||||
<Tabs centered={!isMobile}
|
||||
defaultActiveKey={openTab.toString()} tabPosition="top" animated={{inkBar: true, tabPane: false}}
|
||||
items={items}/>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
marginTop: "15px",
|
||||
}}>
|
||||
After that you should be connected. Add more devices to your network or manage your existing devices in the
|
||||
admin panel.
|
||||
If you have further questions check out our {<Link target="_blank"
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}>installation
|
||||
guide</Link>}
|
||||
</Paragraph>
|
||||
</>
|
||||
}
|
||||
|
||||
export default AddPeerPopup
|
||||
51
src/components/popups/addpeer/addpeer/AndroidTab.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Button, Image, Typography} from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import googleplay from '../../../../assets/google-play-badge.png';
|
||||
const {Text} = Typography;
|
||||
|
||||
export const AndroidTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install the application from Google Play Store:',
|
||||
commands: (
|
||||
<a href="https://play.google.com/store/apps/details?id=io.netbird.client" target="_blank">
|
||||
<Image width="12em" preview={false} style={{marginTop: "5px"}} src={googleplay}/>
|
||||
</a>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Run the application and click on the "Connect" button in the middle of the screen',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on Android
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AndroidTab
|
||||
63
src/components/popups/addpeer/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://docs.netbird.io/how-to/getting-started#running-net-bird-in-docker" target="_blank">Running NetBird in Docker</Link>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
{/*<Text style={{fontWeight: "bold"}}>
|
||||
Run in Docker
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
{formatDockerCommand()}
|
||||
</SyntaxHighlighter>
|
||||
</div>*/}
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on Ubuntu
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default DockerTab
|
||||
@@ -12,7 +12,7 @@ export const OtherTab = () => {
|
||||
key: 1,
|
||||
title: 'For other installation options check our documentation.',
|
||||
commands: (
|
||||
<Button type="primary" href={`https://netbird.io/docs/getting-started/installation#binary-install`} target="_blank">
|
||||
<Button type="primary" href={`https://docs.netbird.io/how-to/getting-started#binary-install`} target="_blank">
|
||||
Documentation
|
||||
</Button>
|
||||
),
|
||||
136
src/components/popups/addpeer/addpeer/MacTab.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Button, Divider, Row, Tooltip, Typography} from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import {StepCommand} from "./types"
|
||||
import {formatNetBirdUP} from "./common"
|
||||
import {Collapse} from "antd";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import {QuestionCircleOutlined} from "@ant-design/icons";
|
||||
|
||||
const {Panel} = Collapse;
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
export const LinuxTab = () => {
|
||||
|
||||
const [quickSteps, setQuickSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: (
|
||||
<Row>
|
||||
<Text>Download and run MacOS installer: </Text>
|
||||
<Tooltip title={
|
||||
<text>If you don't know what chip your Mac has, you can find out by clicking on the Apple logo
|
||||
in the top left corner of your screen and selecting 'About This Mac'. For more information
|
||||
click <a href="https://support.apple.com/en-us/HT211814" target="_blank">here</a></text>
|
||||
}
|
||||
className={"ant-form-item-tooltip"}>
|
||||
<QuestionCircleOutlined
|
||||
style={{color: "rgba(0, 0, 0, 0.45)", cursor: "help", marginLeft: "3px"}}/>
|
||||
</Tooltip>
|
||||
</Row>
|
||||
|
||||
),
|
||||
commands: (
|
||||
<Row style={{paddingTop: "5px"}}>
|
||||
<Button style={{marginRight: "10px"}} type="primary" href="https://pkgs.netbird.io/macos/amd64">Download
|
||||
for Intel</Button>
|
||||
<Button style={{marginRight: "10px"}} type="default" href="https://pkgs.netbird.io/macos/arm64">Download
|
||||
for M1 & M2</Button>
|
||||
</Row>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Click on "Connect" from the NetBird icon in your system tray',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}
|
||||
])
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install Homebrew',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://brew.sh/" target="_blank">Download
|
||||
Brew</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`# for CLI only`,
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`# for GUI package`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Start NetBird daemon:',
|
||||
commands: [
|
||||
`sudo netbird service install`,
|
||||
`sudo netbird service start`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand
|
||||
])
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on MacOS
|
||||
</Text>
|
||||
<div style={{marginTop: 5, marginBottom: 5}}>
|
||||
<TabSteps stepsItems={quickSteps}/>
|
||||
</div>
|
||||
<div style={{marginTop: 0}}/>
|
||||
{/*<Divider style={{marginTop: "5px"}} />*/}
|
||||
<Collapse bordered={false} style={{backgroundColor: "unset"}}>
|
||||
<Panel className="CustomPopupCollapse" header={<Text strong={true}>Or install via command line</Text>}
|
||||
key="1">
|
||||
<div style={{marginLeft: "25px"}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install with one command
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Or install manually with HomeBrew
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinuxTab
|
||||
@@ -2,16 +2,16 @@ import "highlight.js/styles/mono-blue.css";
|
||||
import "highlight.js/lib/languages/bash";
|
||||
import { StepCommand } from './types'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { monoBlue } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import {
|
||||
Typography,
|
||||
Space,
|
||||
Steps, Button
|
||||
Steps, Button, Popover, StepsProps
|
||||
} from "antd";
|
||||
import {copyToClipboard} from "../../utils/common";
|
||||
import {copyToClipboard} from "../../../../utils/common";
|
||||
import {CheckOutlined, CopyOutlined} from "@ant-design/icons";
|
||||
import React, {useEffect, useState} from "react";
|
||||
const { Step } = Steps;
|
||||
const {Text} = Typography;
|
||||
|
||||
type Props = {
|
||||
stepsItems: Array<StepCommand>
|
||||
@@ -36,35 +36,22 @@ const TabSteps:React.FC<Props> = ({stepsItems}) => {
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Steps direction="vertical" current={0}>
|
||||
<Steps direction="vertical" size={"small"}>
|
||||
{steps.map(c =>
|
||||
<Step
|
||||
status={"process"}
|
||||
key={c.key}
|
||||
title={c.title}
|
||||
title={<Text>{c.title}</Text>}
|
||||
description={
|
||||
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex"}}>
|
||||
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex", fontSize: ".85em"}}>
|
||||
{ (c.commands && (typeof c.commands === 'string')) ? (
|
||||
<SyntaxHighlighter language="bash" style={monoBlue}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
{c.commands}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
c.commands
|
||||
)}
|
||||
{ c.showCopyButton &&
|
||||
<>
|
||||
{ !c.copied ? (
|
||||
<Button type="text" size="large" className="btn-copy-code" icon={<CopyOutlined/>}
|
||||
style={{color: "rgb(107, 114, 128)"}}
|
||||
onClick={() => onCopyClick(c.key, c.commands, true)}/>
|
||||
): (
|
||||
<Button type="text" size="large" className="btn-copy-code" icon={<CheckOutlined/>}
|
||||
style={{color: "green"}}/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
68
src/components/popups/addpeer/addpeer/UbuntuTab.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, {useState} from 'react';
|
||||
import {StepCommand} from "./types"
|
||||
import {formatNetBirdUP} from "./common"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import TabSteps from "./TabSteps";
|
||||
import {Typography} from "antd";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
|
||||
export const UbuntuTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Add repository',
|
||||
commands: [
|
||||
`sudo apt install ca-certificates curl gnupg -y`,
|
||||
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/wiretrustee.gpg`,
|
||||
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird',
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`# for CLI only`,
|
||||
`sudo apt-get install netbird`,
|
||||
`# for GUI package`,
|
||||
`sudo apt-get install netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install with one command
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Or install manually on Ubuntu
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default UbuntuTab
|
||||
@@ -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/popups/addpeer/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
|
||||
@@ -2,13 +2,14 @@
|
||||
"auth0Auth": "$USE_AUTH0",
|
||||
"authAuthority": "$AUTH_AUTHORITY",
|
||||
"authClientId": "$AUTH_CLIENT_ID",
|
||||
"authClientSecret": "$AUTH_CLIENT_SECRET",
|
||||
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
|
||||
"authAudience": "$AUTH_AUDIENCE",
|
||||
|
||||
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
|
||||
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
|
||||
"latestVersion": "$NETBIRD_LATEST_VERSION",
|
||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
||||
"redirectURI": "$AUTH_REDIRECT_URI",
|
||||
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI"
|
||||
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
|
||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE"
|
||||
}
|
||||
@@ -9,7 +9,7 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
|
||||
const defaultRedirectURI = '/#callback';
|
||||
const defaultSilentRedirectURI = '/#silent-callback'
|
||||
|
||||
const defaultTokenSource = "accessToken"
|
||||
export function getConfig() {
|
||||
let redirectURI = defaultRedirectURI
|
||||
if (configJson.redirectURI) {
|
||||
@@ -21,17 +21,23 @@ export function getConfig() {
|
||||
silentRedirectURI = configJson.silentRedirectURI
|
||||
}
|
||||
|
||||
let tokenSource = defaultTokenSource
|
||||
if (configJson.tokenSource) {
|
||||
tokenSource = configJson.tokenSource
|
||||
}
|
||||
|
||||
return {
|
||||
auth0Auth: configJson.auth0Auth == "true", //due to substitution we can't use boolean in the config
|
||||
authority: configJson.authAuthority,
|
||||
authority: configJson.authAuthority.replace(/\/+$/, ''),
|
||||
clientId: configJson.authClientId,
|
||||
clientSecret: configJson.authClientSecret,
|
||||
scopesSupported: configJson.authScopesSupported,
|
||||
apiOrigin: configJson.apiOrigin,
|
||||
grpcApiOrigin: configJson.grpcApiOrigin,
|
||||
latestVersion: configJson.latestVersion,
|
||||
audience: configJson.authAudience,
|
||||
hotjarTrackID: configJson.hotjarTrackID,
|
||||
redirectURI: redirectURI,
|
||||
silentRedirectURI: silentRedirectURI,
|
||||
tokenSource: tokenSource,
|
||||
};
|
||||
}
|
||||
|
||||
184
src/index.css
@@ -1,14 +1,19 @@
|
||||
@import '~antd/dist/antd.css';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;500&display=swap');
|
||||
@import 'antd/dist/reset.css';
|
||||
|
||||
/*@tailwind base;*/
|
||||
/*@tailwind components;*/
|
||||
/*@tailwind utilities;*/
|
||||
html,
|
||||
body,
|
||||
* {
|
||||
font-family: 'Arimo', sans-serif !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
|
||||
.ant-layout-header {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
@@ -27,13 +32,14 @@ body {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-menu-horizontal > .ant-menu-item a {
|
||||
.ant-menu-horizontal>.ant-menu-item a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ant-menu-horizontal > .ant-menu-item-selected a {
|
||||
.ant-menu-horizontal>.ant-menu-item-selected a {
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,23 +57,25 @@ body {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.space-align-block {
|
||||
flex: none;
|
||||
margin: 8px 4px;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.space-align-block .mock-block {
|
||||
display: inline-block;
|
||||
padding: 32px 8px 16px;
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
}
|
||||
|
||||
.bg-indigo-600{
|
||||
.bg-indigo-600 {
|
||||
background-color: rgb(79, 70, 229);
|
||||
}
|
||||
|
||||
.card-table-no-placeholder .ant-table-placeholder{
|
||||
.card-table-no-placeholder .ant-table-placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -79,9 +87,10 @@ body {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.card-table .ant-table-ping-left:not(.ant-table-has-fix-left) .ant-table-container::before {
|
||||
.card-table .ant-table-ping-left:not(.ant-table-has-fix-left) .ant-table-container::before {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card-table .ant-table-ping-right:not(.ant-table-has-fix-right) .ant-table-container::after {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -92,20 +101,6 @@ body {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.ant-steps-item-tail::after {
|
||||
background-color: #1890ff !important;
|
||||
}
|
||||
|
||||
.ant-steps-item-icon {
|
||||
background: #1890ff !important;
|
||||
color: #ffffff !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-steps-icon {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.nb-code {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
@@ -135,4 +130,145 @@ body {
|
||||
.access-control.ant-drawer-subtitle {
|
||||
line-height: 22px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.ant-steps-item-icon {
|
||||
background: #EBEBEB !important;
|
||||
border-color: #EBEBEB !important;
|
||||
}
|
||||
|
||||
.ant-steps-icon-dot {
|
||||
background: #EBEBEB !important;
|
||||
border-color: #EBEBEB !important;
|
||||
}
|
||||
|
||||
.ant-steps-icon {
|
||||
background: #EBEBEB !important;
|
||||
border: none;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.ant-steps-item-tail::after {
|
||||
background: #EBEBEB !important;
|
||||
}
|
||||
|
||||
.ant-steps-item-tail {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td.non-highlighted-table-column {
|
||||
background-color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.ant-table-tbody>tr.ant-table-row:hover>td {
|
||||
background: #FAFAFA !important;
|
||||
}
|
||||
|
||||
.ant-table-thead .ant-table-cell {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
.CustomPopupCollapse .ant-collapse-content-box,
|
||||
.CustomPopupCollapse .ant-collapse-header {
|
||||
padding-left: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.system-info-panel .ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.system-info-panel .ant-collapse-content-box {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.peers-form .ant-layout-header {
|
||||
line-height: 25px;
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
padding-bottom: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
.ant-form-item-explain-error {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.menlo-font,
|
||||
.menlo-font * {
|
||||
font-family: 'Menlo', monospace !important;
|
||||
}
|
||||
|
||||
.tag-box .ant-select-selector {
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
|
||||
.tag-box .ant-select-selection-item {
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
justify-content: center;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: all 0.2s;
|
||||
text-align: start;
|
||||
max-width: 40px;
|
||||
height: 25px;
|
||||
padding: 0 4px !important;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.font-500 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
font-weight: 500 !important;
|
||||
font-size: 22px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.react-select__indicator-separator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.react-select__control,
|
||||
.react-select__value-container,
|
||||
.react-select__input-container {
|
||||
min-height: 32px !important;
|
||||
padding: 0 5px !important;
|
||||
max-height: 32px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.react-select__value-container {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.react-select__indicator {
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
|
||||
.ant-badge-status-dot {
|
||||
width: 8px!important;
|
||||
height: 8px!important;
|
||||
}
|
||||
@@ -34,7 +34,8 @@ const providerConfig = {
|
||||
// service_worker_relative_url:'/OidcServiceWorker.js',
|
||||
service_worker_only: false,
|
||||
authority_configuration: config.auth0Auth ? auth0AuthorityConfig : undefined,
|
||||
...(config.audience ? {extras: {audience: config.audience}} : null)
|
||||
...(config.audience ? {extras: {audience: config.audience}} : null),
|
||||
...(config.clientSecret ? {token_request_extras: {client_secret: config.clientSecret}} : null)
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
|
||||
@@ -3,12 +3,6 @@ import axios from 'axios';
|
||||
import {ApiError, ApiRequestParams, ApiResponse} from './types';
|
||||
import {headersFactory, RequestHeader} from './header-factory';
|
||||
|
||||
/*axios.interceptors.response.use(undefined, err => {
|
||||
let res = err.response;
|
||||
if (res.status === 401) {
|
||||
}
|
||||
})*/
|
||||
|
||||
async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>> {
|
||||
const data = params.data ? (params.data as any).payload : undefined;
|
||||
const url = `${params.urlBase}${params.url}`;
|
||||
@@ -26,8 +20,10 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
|
||||
statusCode: -1
|
||||
};
|
||||
|
||||
let queryParams = (params.data as any).queryParams ? (params.data as any).queryParams : {};
|
||||
|
||||
try {
|
||||
response = await axios.request({url, data, method: params.method, headers: builtHeader as any});
|
||||
response = await axios.request({url, data, method: params.method, headers: builtHeader as any, params: queryParams});
|
||||
} catch (err: any) {
|
||||
error = <ApiError>{
|
||||
code: err ? err.code : '-1',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Method } from 'axios';
|
||||
|
||||
export interface RequestPayload<T> {
|
||||
getAccessTokenSilently: any | null;
|
||||
queryParams?: any | null;
|
||||
payload:T;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ export interface RequestConfig {
|
||||
export interface ApiRequestParams extends RequestConfig {
|
||||
method: Method;
|
||||
url: string;
|
||||
params?: any,
|
||||
data: unknown;
|
||||
urlBase: string;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 }
|
||||
}
|
||||
@@ -5,10 +5,15 @@ import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { sagas as peerSagas } from './peer';
|
||||
import { sagas as setupKeySagas } from './setup-key';
|
||||
import { sagas as userSagas } from './user';
|
||||
import { sagas as policySagas } from './policy';
|
||||
import { sagas as ruleSagas } from './rule';
|
||||
import { sagas as groupSagas } from './group';
|
||||
import { sagas as routeSagas } from './route';
|
||||
import { sagas as nameserverGroupSagas } from './nameservers';
|
||||
import { sagas as eventSagas } from './event';
|
||||
import { sagas as dnsSettingsSagas } from './dns-settings';
|
||||
import { sagas as accountSagas } from './account';
|
||||
import { sagas as personalAccessTokenSagas } from './personal-access-token';
|
||||
|
||||
import rootReducer from './root-reducer';
|
||||
import { apiClient } from '../services/api-client';
|
||||
@@ -24,8 +29,13 @@ sagaMiddleware.run(peerSagas);
|
||||
sagaMiddleware.run(setupKeySagas);
|
||||
sagaMiddleware.run(userSagas);
|
||||
sagaMiddleware.run(ruleSagas);
|
||||
sagaMiddleware.run(policySagas);
|
||||
sagaMiddleware.run(groupSagas);
|
||||
sagaMiddleware.run(routeSagas);
|
||||
sagaMiddleware.run(nameserverGroupSagas);
|
||||
sagaMiddleware.run(eventSagas);
|
||||
sagaMiddleware.run(dnsSettingsSagas);
|
||||
sagaMiddleware.run(accountSagas);
|
||||
sagaMiddleware.run(personalAccessTokenSagas);
|
||||
|
||||
export { apiClient, rootReducer, store };
|
||||
export { apiClient, rootReducer, store };
|
||||
|
||||
@@ -79,11 +79,14 @@ const deletedPeer = createReducer<DeleteResponse<string | null>, ActionTypes>(in
|
||||
const updateGroupsVisible = createReducer<boolean, ActionTypes>(initialState.setUpdateGroupsVisible)
|
||||
.handleAction(actions.setUpdateGroupsVisible, (store, action) => action.payload)
|
||||
|
||||
const savedGroups = createReducer<ChangeResponse<Group[] | null>, ActionTypes>(initialState.savedGroups)
|
||||
.handleAction(actions.saveGroups.request, () => initialState.savedGroups)
|
||||
.handleAction(actions.saveGroups.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveGroups.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedGroups, () => initialState.savedGroups)
|
||||
const savedGroups = createReducer<ChangeResponse<Group[] | null>, ActionTypes>(
|
||||
initialState.savedGroups
|
||||
)
|
||||
.handleAction(actions.saveGroups.request, () => initialState.savedGroups)
|
||||
.handleAction(actions.saveGroups.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveGroups.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedGroups, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedGroups, () => initialState.savedGroups);
|
||||
|
||||
const updatedPeer = createReducer<CreateResponse<Peer | null>, ActionTypes>(initialState.updatedPeer)
|
||||
.handleAction(actions.updatePeer.request, () => initialState.updatedPeer)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,6 +14,9 @@ export interface Peer {
|
||||
user_id?: string,
|
||||
ui_version?: string,
|
||||
dns_label: string,
|
||||
last_login: string,
|
||||
login_expired: boolean,
|
||||
login_expiration_enabled: boolean
|
||||
}
|
||||
|
||||
export interface FormPeer extends Peer {
|
||||
@@ -40,6 +43,10 @@ export interface PeerIPToName {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToID {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerDataTable extends Peer {
|
||||
key: string;
|
||||
groups: Group[];
|
||||
|
||||
41
src/store/personal-access-token/actions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated, SpecificPAT} from './types';
|
||||
import {
|
||||
ApiError,
|
||||
CreateResponse,
|
||||
DeleteResponse,
|
||||
RequestPayload
|
||||
} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getPersonalAccessTokens: createAsyncAction(
|
||||
'GET_PERSONAL_ACCESS_TOKEN_REQUEST',
|
||||
'GET_PERSONAL_ACCESS_TOKEN_SUCCESS',
|
||||
'GET_PERSONAL_ACCESS_TOKEN_FAILURE',
|
||||
)<RequestPayload<string>, PersonalAccessToken[], ApiError>(),
|
||||
resetPersonalAccessTokens: createAction('RESET_PERSONAL_ACCESS_TOKENS')<null>(),
|
||||
|
||||
savePersonalAccessToken: createAsyncAction(
|
||||
'SAVE_PERSONAL_ACCESS_TOKEN_REQUEST',
|
||||
'SAVE_PERSONAL_ACCESS_TOKEN_SUCCESS',
|
||||
'SAVE_PERSONAL_ACCESS_TOKEN_FAILURE',
|
||||
)<RequestPayload<PersonalAccessTokenCreate>, CreateResponse<PersonalAccessTokenGenerated | null>, CreateResponse<PersonalAccessTokenGenerated | null>>(),
|
||||
setSavedPersonalAccessToken: createAction('SET_PERSONAL_ACCESS_TOKEN_KEY')<CreateResponse<PersonalAccessTokenGenerated | null>>(),
|
||||
resetSavedPersonalAccessToken: createAction('RESET_PERSONAL_ACCESS_TOKEN_KEY')<null>(),
|
||||
|
||||
deletePersonalAccessToken: createAsyncAction(
|
||||
'DELETE_PERSONAL_ACCESS_TOKEN_REQUEST',
|
||||
'DELETE_PERSONAL_ACCESS_TOKEN_SUCCESS',
|
||||
'DELETE_PERSONAL_ACCESS_TOKEN_FAILURE'
|
||||
)<RequestPayload<SpecificPAT>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletePersonalAccessToken: createAction('SET_DELETE_PERSONAL_ACCESS_TOKEN')<DeleteResponse<string | null>>(),
|
||||
resetDeletedPersonalAccessToken: createAction('RESET_DELETE_PERSONAL_ACCESS_TOKEN')<null>(),
|
||||
|
||||
removePersonalAccessToken: createAction('REMOVE_PERSONAL_ACCESS_TOKEN')<string>(),
|
||||
setPersonalAccessToken: createAction('SET_PERSONAL_ACCESS_TOKEN')<PersonalAccessTokenCreate>(),
|
||||
setNewPersonalAccessTokenVisible: createAction('SET_NEW_PERSONAL_ACCESS_TOKEN_VISIBLE')<boolean>(),
|
||||
setNewPersonalAccessTokenPopupVisible: createAction('SET_NEW_PERSONAL_ACCESS_TOKEN_POPUP_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/personal-access-token/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 };
|
||||
104
src/store/personal-access-token/reducer.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
|
||||
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated} from "./types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: PersonalAccessToken[] | null;
|
||||
personalAccessToken: PersonalAccessTokenCreate | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletedPersonalAccessToken: DeleteResponse<string | null>;
|
||||
revokedPersonalAccessToken: ChangeResponse<string | null>;
|
||||
savedPersonalAccessToken: CreateResponse<PersonalAccessTokenGenerated | null>;
|
||||
newPersonalAccessTokenVisible: boolean
|
||||
newPersonalAccessTokenPopupVisible: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
personalAccessToken: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deletedPersonalAccessToken: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
revokedPersonalAccessToken: <ChangeResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
savedPersonalAccessToken: <CreateResponse<PersonalAccessTokenGenerated | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
newPersonalAccessTokenVisible: false,
|
||||
newPersonalAccessTokenPopupVisible: false
|
||||
};
|
||||
|
||||
const data = createReducer<PersonalAccessToken[], ActionTypes>(initialState.data as PersonalAccessToken[])
|
||||
.handleAction(actions.getPersonalAccessTokens.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getPersonalAccessTokens.failure, () => [])
|
||||
.handleAction(actions.resetPersonalAccessTokens, () => []);
|
||||
|
||||
const personalAccessToken = createReducer<PersonalAccessTokenCreate, ActionTypes>(initialState.personalAccessToken as PersonalAccessTokenCreate)
|
||||
.handleAction(actions.setPersonalAccessToken, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getPersonalAccessTokens.request, () => true)
|
||||
.handleAction(actions.getPersonalAccessTokens.success, () => false)
|
||||
.handleAction(actions.getPersonalAccessTokens.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getPersonalAccessTokens.request, () => null)
|
||||
.handleAction(actions.getPersonalAccessTokens.success, () => null)
|
||||
.handleAction(actions.getPersonalAccessTokens.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getPersonalAccessTokens.request, () => true)
|
||||
.handleAction(actions.getPersonalAccessTokens.success, () => false)
|
||||
.handleAction(actions.getPersonalAccessTokens.failure, () => false);
|
||||
|
||||
const deletedPersonalAccessToken = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedPersonalAccessToken)
|
||||
.handleAction(actions.deletePersonalAccessToken.request, () => initialState.deletedPersonalAccessToken)
|
||||
.handleAction(actions.deletePersonalAccessToken.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deletePersonalAccessToken.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletePersonalAccessToken, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedPersonalAccessToken, (store, action) => initialState.deletedPersonalAccessToken);
|
||||
|
||||
const savedPersonalAccessToken = createReducer<CreateResponse<PersonalAccessTokenGenerated | null>, ActionTypes>(initialState.savedPersonalAccessToken)
|
||||
.handleAction(actions.savePersonalAccessToken.request, () => initialState.savedPersonalAccessToken)
|
||||
.handleAction(actions.savePersonalAccessToken.success, (store, action) => action.payload)
|
||||
.handleAction(actions.savePersonalAccessToken.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedPersonalAccessToken, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedPersonalAccessToken, () => initialState.savedPersonalAccessToken)
|
||||
|
||||
const newPersonalAccessTokenVisible = createReducer<boolean, ActionTypes>(initialState.newPersonalAccessTokenVisible)
|
||||
.handleAction(actions.setNewPersonalAccessTokenVisible, (store, action) => action.payload)
|
||||
|
||||
const newPersonalAccessTokenPopupVisible = createReducer<boolean, ActionTypes>(initialState.newPersonalAccessTokenPopupVisible)
|
||||
.handleAction(actions.setNewPersonalAccessTokenPopupVisible, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
personalAccessToken: personalAccessToken,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedPersonalAccessToken: deletedPersonalAccessToken,
|
||||
savedPersonalAccessToken: savedPersonalAccessToken,
|
||||
newPersonalAccessTokenVisible: newPersonalAccessTokenVisible,
|
||||
newPersonalAccessTokenPopupVisible: newPersonalAccessTokenPopupVisible
|
||||
});
|
||||
101
src/store/personal-access-token/sagas.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {PersonalAccessToken, PersonalAccessTokenCreate, PersonalAccessTokenGenerated} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
|
||||
export function* getPersonalAccessTokens(action: ReturnType<typeof actions.getPersonalAccessTokens.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getAllPersonalAccessTokens, action.payload);
|
||||
const response = effect as ApiResponse<PersonalAccessToken[]>;
|
||||
|
||||
yield put(actions.getPersonalAccessTokens.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getPersonalAccessTokens.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* savePersonalAccessToken(action: ReturnType<typeof actions.savePersonalAccessToken.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedPersonalAccessToken({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<PersonalAccessTokenGenerated | null>))
|
||||
|
||||
const tokenToSave = action.payload.payload
|
||||
|
||||
let effect = yield call(service.createPersonalAccessToken, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
user_id: tokenToSave.user_id,
|
||||
name: tokenToSave.name,
|
||||
expires_in: tokenToSave.expires_in,
|
||||
} as PersonalAccessTokenCreate
|
||||
});
|
||||
const response = effect as ApiResponse<PersonalAccessTokenGenerated>;
|
||||
|
||||
yield put(actions.savePersonalAccessToken.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<PersonalAccessTokenGenerated | null>));
|
||||
|
||||
yield put(actions.getPersonalAccessTokens.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: tokenToSave.user_id }));
|
||||
} catch (err) {
|
||||
yield put(actions.savePersonalAccessToken.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<PersonalAccessTokenGenerated | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export function* deletePersonalAccessToken(action: ReturnType<typeof actions.deletePersonalAccessToken.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletePersonalAccessToken,{
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
|
||||
const effect = yield call(service.deletePersonalAccessToken, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deletePersonalAccessToken.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const personalAccessTokens = (yield select(state => state.personalAccessToken.data)) as PersonalAccessToken[]
|
||||
yield put(actions.getPersonalAccessTokens.success(personalAccessTokens.filter((p:PersonalAccessToken) => p.id !== action.payload.payload.id)))
|
||||
} catch (err) {
|
||||
yield put(actions.deletePersonalAccessToken.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getPersonalAccessTokens.request, getPersonalAccessTokens),
|
||||
takeLatest(actions.savePersonalAccessToken.request, savePersonalAccessToken),
|
||||
takeLatest(actions.deletePersonalAccessToken.request, deletePersonalAccessToken)
|
||||
]);
|
||||
}
|
||||
|
||||
34
src/store/personal-access-token/service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import {
|
||||
PersonalAccessToken,
|
||||
PersonalAccessTokenCreate, PersonalAccessTokenGenerated,
|
||||
SpecificPAT
|
||||
} from './types';
|
||||
|
||||
export default {
|
||||
async getAllPersonalAccessTokens(payload:RequestPayload<string>): Promise<ApiResponse<PersonalAccessToken[]>> {
|
||||
return apiClient.get<PersonalAccessToken[]>(
|
||||
`/api/users/` + payload.payload + `/tokens`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async getPersonalAccessToken(payload:RequestPayload<SpecificPAT>): Promise<ApiResponse<PersonalAccessToken>> {
|
||||
return apiClient.get<PersonalAccessToken>(
|
||||
`/api/users/` + payload.payload.user_id + `/tokens/` + payload.payload.id,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletePersonalAccessToken(payload:RequestPayload<SpecificPAT>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/users/` + payload.payload.user_id + `/tokens/` + payload.payload.id,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createPersonalAccessToken(payload:RequestPayload<PersonalAccessTokenCreate>): Promise<ApiResponse<PersonalAccessTokenGenerated>> {
|
||||
return apiClient.post<PersonalAccessTokenGenerated>(
|
||||
`/api/users/` + payload.payload.user_id + `/tokens`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
26
src/store/personal-access-token/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
export interface PersonalAccessToken {
|
||||
id: string;
|
||||
name: string;
|
||||
expiration_date: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
last_used: string;
|
||||
}
|
||||
|
||||
export interface SpecificPAT {
|
||||
name: string,
|
||||
user_id: string,
|
||||
id: string,
|
||||
}
|
||||
|
||||
export interface PersonalAccessTokenGenerated {
|
||||
plain_token: string,
|
||||
personal_access_token: PersonalAccessToken
|
||||
}
|
||||
|
||||
export interface PersonalAccessTokenCreate {
|
||||
user_id: string,
|
||||
name: string,
|
||||
expires_in: number,
|
||||
}
|
||||
35
src/store/policy/actions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import { Policy, PolicyToSave } from './types';
|
||||
import { ApiError, CreateResponse, DeleteResponse, RequestPayload } from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getPolicies: createAsyncAction(
|
||||
'GET_POLICIES_REQUEST',
|
||||
'GET_POLICIES_SUCCESS',
|
||||
'GET_POLICIES_FAILURE',
|
||||
)<RequestPayload<null>, Policy[], ApiError>(),
|
||||
|
||||
savePolicy: createAsyncAction(
|
||||
'SAVE_POLICY_REQUEST',
|
||||
'SAVE_POLICY_SUCCESS',
|
||||
'SAVE_POLICY_FAILURE',
|
||||
)<RequestPayload<PolicyToSave>, CreateResponse<Policy | null>, CreateResponse<Policy | null>>(),
|
||||
setSavedPolicy: createAction('SET_CREATE_POLICY')<CreateResponse<Policy | null>>(),
|
||||
resetSavedPolicy: createAction('RESET_CREATE_POLICY')<null>(),
|
||||
|
||||
deletePolicy: createAsyncAction(
|
||||
'DELETE_POLICY_REQUEST',
|
||||
'DELETE_POLICY_SUCCESS',
|
||||
'DELETE_POLICY_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedPolicy: createAction('SET_DELETED_POLICY')<DeleteResponse<string | null>>(),
|
||||
resetDeletedPolicy: createAction('RESET_DELETED_POLICY')<null>(),
|
||||
removePolicy: createAction('REMOVE_POLICY')<string>(),
|
||||
|
||||
setPolicy: createAction('SET_POLICY')<Policy>(),
|
||||
setSetupNewPolicyVisible: createAction('SET_SETUP_NEW_POLICY_VISIBLE')<boolean>(),
|
||||
setSetupEditPolicyVisible: createAction('SET_SETUP_EDIT_POLICY_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/policy/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/policy/reducer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Policy } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import { ApiError, DeleteResponse, CreateResponse } from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Policy[] | null;
|
||||
policy: Policy | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletePolicy: DeleteResponse<string | null>;
|
||||
savedPolicy: CreateResponse<Policy | null>;
|
||||
setupNewPolicyVisible: boolean;
|
||||
setupEditPolicyVisible: boolean;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
policy: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deletePolicy: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
},
|
||||
savedPolicy: <CreateResponse<Policy | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
},
|
||||
setupNewPolicyVisible: false,
|
||||
setupEditPolicyVisible: false,
|
||||
};
|
||||
|
||||
const data = createReducer<Policy[], ActionTypes>(initialState.data as Policy[])
|
||||
.handleAction(actions.getPolicies.success, (_, action) => action.payload)
|
||||
.handleAction(actions.getPolicies.failure, () => []);
|
||||
|
||||
const policy = createReducer<Policy, ActionTypes>(initialState.policy as Policy)
|
||||
.handleAction(actions.setPolicy, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getPolicies.request, () => true)
|
||||
.handleAction(actions.getPolicies.success, () => false)
|
||||
.handleAction(actions.getPolicies.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getPolicies.request, () => null)
|
||||
.handleAction(actions.getPolicies.success, () => null)
|
||||
.handleAction(actions.getPolicies.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getPolicies.request, () => true)
|
||||
.handleAction(actions.getPolicies.success, () => false)
|
||||
.handleAction(actions.getPolicies.failure, () => false);
|
||||
|
||||
const deletedPolicy = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletePolicy)
|
||||
.handleAction(actions.deletePolicy.request, () => initialState.deletePolicy)
|
||||
.handleAction(actions.deletePolicy.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deletePolicy.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedPolicy, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedPolicy, () => initialState.deletePolicy)
|
||||
|
||||
const savedPolicy = createReducer<CreateResponse<Policy | null>, ActionTypes>(initialState.savedPolicy)
|
||||
.handleAction(actions.savePolicy.request, () => initialState.savedPolicy)
|
||||
.handleAction(actions.savePolicy.success, (store, action) => action.payload)
|
||||
.handleAction(actions.savePolicy.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedPolicy, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedPolicy, () => initialState.savedPolicy)
|
||||
|
||||
const setupNewPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewPolicyVisible)
|
||||
.handleAction(actions.setSetupNewPolicyVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupEditPolicyVisible)
|
||||
.handleAction(actions.setSetupEditPolicyVisible, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
policy,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedPolicy,
|
||||
savedPolicy,
|
||||
setupNewPolicyVisible,
|
||||
setupEditPolicyVisible,
|
||||
});
|
||||
223
src/store/policy/sagas.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||
import {
|
||||
ApiError,
|
||||
ApiResponse,
|
||||
CreateResponse,
|
||||
DeleteResponse,
|
||||
} from "../../services/api-client/types";
|
||||
import { Policy, PolicyRule } from "./types";
|
||||
import service from "./service";
|
||||
import serviceGroup from "../group/service";
|
||||
import actions from "./actions";
|
||||
import { actions as groupActions } from "../group";
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export function* getPolicies(
|
||||
action: ReturnType<typeof actions.getPolicies.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setDeletedPolicy({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const effect = yield call(service.getPolicies, action.payload);
|
||||
const response = effect as ApiResponse<Policy[]>;
|
||||
|
||||
yield put(actions.getPolicies.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getPolicies.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedPolicy(
|
||||
action: ReturnType<typeof actions.setSavedPolicy>
|
||||
): Generator {
|
||||
yield put(actions.setSavedPolicy(action.payload));
|
||||
}
|
||||
|
||||
function getNewGroupIds(dataString: string[], responses: Group[]): string[] {
|
||||
return responses
|
||||
.filter((r) => dataString.includes(r.name))
|
||||
.map((r) => r.id || "");
|
||||
}
|
||||
|
||||
export function* savePolicy(
|
||||
action: ReturnType<typeof actions.savePolicy.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setSavedPolicy({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as CreateResponse<Policy | null>)
|
||||
);
|
||||
|
||||
const policyToSave = action.payload.payload;
|
||||
const groupsToSave = policyToSave.groupsToSave
|
||||
? policyToSave.groupsToSave
|
||||
: [];
|
||||
const responsesGroup = yield all(
|
||||
groupsToSave.map((g) =>
|
||||
call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: { name: g },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Policy>[])
|
||||
.filter((r) => r.statusCode === 200)
|
||||
.map((r) => r.body as Group);
|
||||
|
||||
const currentGroups = [
|
||||
...((yield select((state) => state.group.data)) as Policy[]),
|
||||
];
|
||||
const newGroups = [...currentGroups, ...resGroups];
|
||||
yield put(groupActions.getGroups.success(newGroups));
|
||||
|
||||
const newSources = getNewGroupIds(
|
||||
policyToSave.sourcesNoId ? policyToSave.sourcesNoId : [],
|
||||
resGroups
|
||||
);
|
||||
const newDestinations = getNewGroupIds(
|
||||
policyToSave.destinationsNoId ? policyToSave.destinationsNoId : [],
|
||||
resGroups
|
||||
);
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
name: policyToSave.name,
|
||||
description: policyToSave.description,
|
||||
enabled: policyToSave.enabled,
|
||||
query: policyToSave.query,
|
||||
} as Policy,
|
||||
};
|
||||
if (policyToSave.rules.length > 0) {
|
||||
payloadToSave.payload.rules = [];
|
||||
}
|
||||
policyToSave.rules.forEach((r) => {
|
||||
payloadToSave.payload.rules.push({
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
enabled: r.enabled,
|
||||
sources: [...(r.sources as string[]), ...newSources],
|
||||
destinations: [...(r.destinations as string[]), ...newDestinations],
|
||||
bidirectional: r.bidirectional,
|
||||
protocol: r.protocol,
|
||||
ports: r.ports,
|
||||
action: r.action,
|
||||
} as PolicyRule);
|
||||
});
|
||||
|
||||
let effect;
|
||||
if (!policyToSave.id) {
|
||||
effect = yield call(service.createPolicy, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = policyToSave.id;
|
||||
effect = yield call(service.editPolicy, payloadToSave);
|
||||
}
|
||||
|
||||
const response = effect as ApiResponse<Policy>;
|
||||
|
||||
yield put(
|
||||
actions.savePolicy.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as CreateResponse<Policy | null>)
|
||||
);
|
||||
|
||||
yield put(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
yield put(
|
||||
actions.getPolicies.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(
|
||||
actions.savePolicy.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as CreateResponse<Policy | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeletePolicy(
|
||||
action: ReturnType<typeof actions.setDeletedPolicy>
|
||||
): Generator {
|
||||
yield put(actions.setDeletedPolicy(action.payload));
|
||||
}
|
||||
|
||||
export function* deletePolicy(
|
||||
action: ReturnType<typeof actions.deletePolicy.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedPolicy, {
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>);
|
||||
|
||||
const effect = yield call(service.deletedPolicy, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(
|
||||
actions.deletePolicy.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const policies = (yield select((state) => state.policy.data)) as Policy[];
|
||||
yield put(
|
||||
actions.getPolicies.success(
|
||||
policies.filter((p: Policy) => p.id !== action.payload.payload)
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(
|
||||
actions.deletePolicy.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getPolicies.request, getPolicies),
|
||||
takeLatest(actions.savePolicy.request, savePolicy),
|
||||
takeLatest(actions.deletePolicy.request, deletePolicy),
|
||||
]);
|
||||
}
|
||||
32
src/store/policy/service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { Policy } from './types';
|
||||
|
||||
export default {
|
||||
async getPolicies(payload: RequestPayload<null>): Promise<ApiResponse<Policy[]>> {
|
||||
return apiClient.get<Policy[]>(
|
||||
`/api/policies`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedPolicy(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/policies/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
|
||||
return apiClient.post<Policy>(
|
||||
`/api/policies`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Policy>(
|
||||
`/api/policies/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
29
src/store/policy/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export interface PolicyRule {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
sources: Group[] | string[] | null
|
||||
destinations: Group[] | string[] | null
|
||||
bidirectional: boolean
|
||||
action: string
|
||||
protocol: string
|
||||
ports: string[]
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
query: string
|
||||
rules: PolicyRule[]
|
||||
};
|
||||
|
||||
export interface PolicyToSave extends Policy {
|
||||
sourcesNoId?: string[],
|
||||
destinationsNoId?: string[],
|
||||
groupsToSave?: string[]
|
||||
};
|
||||
@@ -1,10 +1,14 @@
|
||||
import { actions as PeerActions } from './peer';
|
||||
import { actions as SetupKeyActions } from './setup-key';
|
||||
import { actions as UserActions } from './user';
|
||||
import { actions as GroupActions } from './group';
|
||||
import { actions as RuleActions } from './rule';
|
||||
import { actions as RouteActions } from './route';
|
||||
import { actions as NameServerGroupActions } from './nameservers';
|
||||
import {actions as PeerActions} from './peer';
|
||||
import {actions as SetupKeyActions} from './setup-key';
|
||||
import {actions as UserActions} from './user';
|
||||
import {actions as GroupActions} from './group';
|
||||
import {actions as RuleActions} from './rule';
|
||||
import {actions as RouteActions} from './route';
|
||||
import {actions as NameServerGroupActions} from './nameservers';
|
||||
import {actions as EventActions} from './event';
|
||||
import {actions as DNSSettingsActions} from './dns-settings';
|
||||
import {actions as AccountActions} from './account';
|
||||
import {actions as PersonalAccessTokenActions} from './personal-access-token';
|
||||
|
||||
export default {
|
||||
peer: PeerActions,
|
||||
@@ -13,5 +17,9 @@ export default {
|
||||
group: GroupActions,
|
||||
rule: RuleActions,
|
||||
route: RouteActions,
|
||||
nameserverGroup: NameServerGroupActions
|
||||
nameserverGroup: NameServerGroupActions,
|
||||
event: EventActions,
|
||||
dnsSettings: DNSSettingsActions,
|
||||
account: AccountActions,
|
||||
personalAccessToken: PersonalAccessTokenActions
|
||||
};
|
||||
|
||||
@@ -5,15 +5,25 @@ import { reducer as setupKey } from './setup-key';
|
||||
import { reducer as user } from './user';
|
||||
import { reducer as group } from './group';
|
||||
import { reducer as rule } from './rule';
|
||||
import { reducer as policy } from './policy';
|
||||
import { reducer as route } from './route';
|
||||
import { reducer as nameserverGroup } from './nameservers';
|
||||
import { reducer as event } from './event';
|
||||
import { reducer as dnsSettings } from './dns-settings';
|
||||
import { reducer as account } from './account';
|
||||
import { reducer as personalAccessToken } from './personal-access-token';
|
||||
|
||||
export default combineReducers({
|
||||
peer,
|
||||
setupKey,
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
route,
|
||||
nameserverGroup
|
||||
peer,
|
||||
setupKey,
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
policy,
|
||||
route,
|
||||
nameserverGroup,
|
||||
event,
|
||||
dnsSettings,
|
||||
account,
|
||||
personalAccessToken
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {Route} from './types';
|
||||
import {Route, RouteToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
@@ -13,7 +13,7 @@ const actions = {
|
||||
'SAVE_ROUTE_REQUEST',
|
||||
'SAVE_ROUTE_SUCCESS',
|
||||
'SAVE_ROUTE_FAILURE',
|
||||
)<RequestPayload<Route>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
|
||||
)<RequestPayload<RouteToSave>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
|
||||
setSavedRoute: createAction('SET_CREATE_ROUTE')<CreateResponse<Route | null>>(),
|
||||
resetSavedRoute: createAction('RESET_CREATE_ROUTE')<null>(),
|
||||
|
||||
@@ -28,6 +28,7 @@ const actions = {
|
||||
|
||||
setRoute: createAction('SET_ROUTE')<Route>(),
|
||||
setSetupNewRouteVisible: createAction('SET_SETUP_NEW_ROUTE_VISIBLE')<boolean>(),
|
||||
setSetupEditRouteVisible: createAction('SET_SETUP_EDIT_ROUTE_VISIBLE')<boolean>(),
|
||||
setSetupNewRouteHA: createAction('SET_SETUP_NEW_ROUTE_HA')<boolean>()
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ type StateType = Readonly<{
|
||||
deleteRoute: DeleteResponse<string | null>;
|
||||
savedRoute: CreateResponse<Route | null>;
|
||||
setupNewRouteVisible: boolean;
|
||||
setupNewRouteHA: boolean
|
||||
setupNewRouteHA: boolean;
|
||||
setupEditRouteVisible: boolean;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
@@ -27,17 +28,18 @@ const initialState: StateType = {
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
data: null,
|
||||
},
|
||||
savedRoute: <CreateResponse<Route | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
data: null,
|
||||
},
|
||||
setupNewRouteVisible: false,
|
||||
setupNewRouteHA: false
|
||||
setupNewRouteHA: false,
|
||||
setupEditRouteVisible: false,
|
||||
};
|
||||
|
||||
const data = createReducer<Route[], ActionTypes>(initialState.data as Route[])
|
||||
@@ -79,6 +81,13 @@ const savedRoute = createReducer<CreateResponse<Route | null>, ActionTypes>(init
|
||||
const setupNewRouteVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRouteVisible)
|
||||
.handleAction(actions.setSetupNewRouteVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditRouteVisible = createReducer<boolean, ActionTypes>(
|
||||
initialState.setupEditRouteVisible
|
||||
).handleAction(
|
||||
actions.setSetupEditRouteVisible,
|
||||
(store, action) => action.payload
|
||||
);
|
||||
|
||||
const setupNewRouteHA = createReducer<boolean, ActionTypes>(initialState.setupNewRouteHA)
|
||||
.handleAction(actions.setSetupNewRouteHA, (store, action) => action.payload)
|
||||
|
||||
@@ -91,5 +100,6 @@ export default combineReducers({
|
||||
deletedRoute,
|
||||
savedRoute,
|
||||
setupNewRouteVisible,
|
||||
setupNewRouteHA
|
||||
setupNewRouteHA,
|
||||
setupEditRouteVisible,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,9 @@ import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../servi
|
||||
import {Route} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
|
||||
export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>): Generator {
|
||||
try {
|
||||
@@ -40,6 +43,21 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
|
||||
|
||||
const routeToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = routeToSave.groupsToCreate
|
||||
if (!groupsToCreate) {
|
||||
groupsToCreate = []
|
||||
}
|
||||
|
||||
// first, create groups that were newly added by user
|
||||
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {name: g}
|
||||
})
|
||||
))
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
|
||||
const newGroups = [...routeToSave.groups, ...resGroups]
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
@@ -50,7 +68,8 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
|
||||
metric: routeToSave.metric,
|
||||
network: routeToSave.network,
|
||||
network_id: routeToSave.network_id,
|
||||
peer: routeToSave.peer
|
||||
peer: routeToSave.peer,
|
||||
groups: newGroups
|
||||
} as Route
|
||||
}
|
||||
|
||||
@@ -72,8 +91,19 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
|
||||
data: response.body
|
||||
} as CreateResponse<Route | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getRoutes.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
|
||||
} catch (err) {
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.saveRoute.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
export interface Route {
|
||||
id?: string
|
||||
id?: string | null
|
||||
description: string
|
||||
enabled: boolean
|
||||
peer: string
|
||||
@@ -8,4 +9,10 @@ export interface Route {
|
||||
network_type?: string
|
||||
metric?: number
|
||||
masquerade: boolean
|
||||
groups: string[]
|
||||
}
|
||||
|
||||
export interface RouteToSave extends Route
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { Rule } from './types';
|
||||
import {SetupKey} from "../setup-key/types";
|
||||
|
||||
export default {
|
||||
async getRules(payload:RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
|
||||
return apiClient.get<Rule[]>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedRule(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/rules/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
return apiClient.post<Rule>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Rule>(
|
||||
`/api/rules/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async getRules(payload: RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
|
||||
return apiClient.get<Rule[]>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedRule(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/rules/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
return apiClient.post<Rule>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Rule>(
|
||||
`/api/rules/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Group} from "../group/types";
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export interface Rule {
|
||||
id?: string
|
||||
@@ -7,6 +7,8 @@ export interface Rule {
|
||||
sources: Group[] | string[] | null
|
||||
destinations: Group[] | string[] | null
|
||||
flow: string
|
||||
protocol: string
|
||||
ports: string[]
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
@@ -14,4 +16,4 @@ export interface RuleToSave extends Rule {
|
||||
sourcesNoId: string[],
|
||||
destinationsNoId: string[],
|
||||
groupsToSave: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ const actions = {
|
||||
|
||||
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
|
||||
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
|
||||
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>()
|
||||
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>(),
|
||||
setSetupEditKeyVisible: createAction('SET_SETUP_EDIT_KEY_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
|
||||
@@ -5,15 +5,16 @@ import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: SetupKey[] | null;
|
||||
setupKey: SetupKey | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletedSetupKey: DeleteResponse<string | null>;
|
||||
revokedSetupKey: ChangeResponse<SetupKey | null>;
|
||||
savedSetupKey: CreateResponse<SetupKey | null>;
|
||||
setupNewKeyVisible: boolean
|
||||
data: SetupKey[] | null;
|
||||
setupKey: SetupKey | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletedSetupKey: DeleteResponse<string | null>;
|
||||
revokedSetupKey: ChangeResponse<SetupKey | null>;
|
||||
savedSetupKey: CreateResponse<SetupKey | null>;
|
||||
setupNewKeyVisible: boolean;
|
||||
setupEditKeyVisible: boolean;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
@@ -43,7 +44,8 @@ const initialState: StateType = {
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewKeyVisible: false
|
||||
setupNewKeyVisible: false,
|
||||
setupEditKeyVisible: false
|
||||
};
|
||||
|
||||
const data = createReducer<SetupKey[], ActionTypes>(initialState.data as SetupKey[])
|
||||
@@ -85,13 +87,17 @@ const savedSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes
|
||||
const setupNewKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewKeyVisible)
|
||||
.handleAction(actions.setSetupNewKeyVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupEditKeyVisible)
|
||||
.handleAction(actions.setSetupEditKeyVisible, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
setupKey,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedSetupKey,
|
||||
savedSetupKey: savedSetupKey,
|
||||
setupNewKeyVisible
|
||||
data,
|
||||
setupKey,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedSetupKey,
|
||||
savedSetupKey: savedSetupKey,
|
||||
setupNewKeyVisible,
|
||||
setupEditKeyVisible,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {ExpiresInValue} from "../../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
|
||||
export interface SetupKey {
|
||||
expires: string;
|
||||
@@ -13,12 +14,14 @@ export interface SetupKey {
|
||||
valid: boolean;
|
||||
auto_groups: string[]
|
||||
expires_in: number;
|
||||
usage_limit: number;
|
||||
usage_limit: any;
|
||||
}
|
||||
|
||||
export interface FormSetupKey extends SetupKey {
|
||||
autoGroupNames: string[]
|
||||
expiresInFormatted: ExpiresInValue
|
||||
exp: moment.Moment
|
||||
last: moment.Moment
|
||||
}
|
||||
|
||||
export interface SetupKeyToSave extends SetupKey
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
|
||||
import {User, UserToSave} from './types';
|
||||
import {ApiError, CreateResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getUsers: createAsyncAction(
|
||||
@@ -9,10 +9,38 @@ const actions = {
|
||||
'GET_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
getServiceUsers: createAsyncAction(
|
||||
'GET_SERVICE_USERS_REQUEST',
|
||||
'GET_SERVICE_USERS_SUCCESS',
|
||||
'GET_SERVICE_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
getRegularUsers: createAsyncAction(
|
||||
'GET_REGULAR_USERS_REQUEST',
|
||||
'GET_REGULAR_USERS_SUCCESS',
|
||||
'GET_REGULAR_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
deleteUser: createAsyncAction(
|
||||
'DELETE_USER_REQUEST',
|
||||
'DELETE_USER_SUCCESS',
|
||||
'DELETE_USER_FAILURE',
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedUser: createAction('SET_DELETED_USER')<DeleteResponse<string | null>>(),
|
||||
resetDeletedUser: createAction('RESET_DELETED_USER')<null>(),
|
||||
|
||||
// used to set a user object that was picked in the user table in the UserUpdate drawer (user update window on right-side).
|
||||
setUser: createAction('SET_USER')<User>(),
|
||||
// used to make the UserUpdate drawer visible in the UI.
|
||||
setUpdateUserDrawerVisible: createAction('SET_UPDATE_USER_VISIBLE')<boolean>(),
|
||||
// used to make the ViewUserPopup visible in the UI.
|
||||
setInviteUserPopupVisible: createAction('SET_INVITE_USER_VISIBLE')<boolean>(),
|
||||
// used to make the EditUserPopup visible in the UI.
|
||||
setEditUserPopupVisible: createAction('SET_EDIT_USER_VISIBLE')<boolean>(),
|
||||
// used to make the AddServiceUserPopup visible in the UI.
|
||||
setAddServiceUserPopupVisible: createAction('SET_ADD_SERVICE_USER_VISIBLE')<boolean>(),
|
||||
// used to remember what tab was open on users page
|
||||
setUserTabOpen: createAction('SET_USER_TAB_OPEN')<string>(),
|
||||
|
||||
saveUser: createAsyncAction(
|
||||
'SAVE_USER_REQUEST',
|
||||
|
||||
@@ -2,24 +2,44 @@ import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { User } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, CreateResponse} from "../../services/api-client/types";
|
||||
import {ApiError, CreateResponse, DeleteResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: User[] | null;
|
||||
serviceUsers: User[] | null;
|
||||
regularUsers: User[] | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
user: User | null;
|
||||
deletedUser: DeleteResponse<string | null>;
|
||||
savedUser: CreateResponse<User | null>;
|
||||
updateUserDrawerVisible: boolean
|
||||
editUserPopupVisible: boolean
|
||||
inviteUserPopupVisible: boolean
|
||||
addServiceUserPopupVisible: boolean
|
||||
usersTabOpen: string
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
serviceUsers: [],
|
||||
regularUsers: [],
|
||||
loading: false,
|
||||
failed: null,
|
||||
user: null,
|
||||
deletedUser: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
// right-sided user update drawer
|
||||
updateUserDrawerVisible: false,
|
||||
editUserPopupVisible: false,
|
||||
inviteUserPopupVisible: false,
|
||||
addServiceUserPopupVisible: false,
|
||||
usersTabOpen: 'Users',
|
||||
savedUser: <CreateResponse<User | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
@@ -33,6 +53,14 @@ const data = createReducer<User[], ActionTypes>(initialState.data as User[])
|
||||
.handleAction(actions.getUsers.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getUsers.failure, () => []);
|
||||
|
||||
const serviceUsers = createReducer<User[], ActionTypes>(initialState.serviceUsers as User[])
|
||||
.handleAction(actions.getServiceUsers.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getServiceUsers.failure, () => []);
|
||||
|
||||
const regularUsers = createReducer<User[], ActionTypes>(initialState.regularUsers as User[])
|
||||
.handleAction(actions.getRegularUsers.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getRegularUsers.failure, () => []);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getUsers.request, () => true)
|
||||
.handleAction(actions.getUsers.success, () => false)
|
||||
@@ -45,9 +73,29 @@ const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
|
||||
const user = createReducer<User, ActionTypes>(initialState.user as User)
|
||||
.handleAction(actions.setUser, (store, action) => action.payload);
|
||||
|
||||
const deletedUser = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedUser)
|
||||
.handleAction(actions.deleteUser.request, () => initialState.deletedUser)
|
||||
.handleAction(actions.deleteUser.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deleteUser.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedUser, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedUser, (store, action) => initialState.deletedUser);
|
||||
|
||||
const updateUserDrawerVisible = createReducer<boolean, ActionTypes>(initialState.updateUserDrawerVisible)
|
||||
.handleAction(actions.setUpdateUserDrawerVisible, (store, action) => action.payload);
|
||||
|
||||
const inviteUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.inviteUserPopupVisible)
|
||||
.handleAction(actions.setInviteUserPopupVisible, (store, action) => action.payload);
|
||||
|
||||
const editUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.editUserPopupVisible)
|
||||
.handleAction(actions.setEditUserPopupVisible, (store, action) => action.payload);
|
||||
|
||||
const addServiceUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.addServiceUserPopupVisible)
|
||||
.handleAction(actions.setAddServiceUserPopupVisible, (store, action) => action.payload);
|
||||
|
||||
const userTabOpen = createReducer<string, ActionTypes>(initialState.usersTabOpen)
|
||||
.handleAction(actions.setUserTabOpen, (store, action) => action.payload);
|
||||
|
||||
const savedUser = createReducer<CreateResponse<User | null>, ActionTypes>(initialState.savedUser)
|
||||
.handleAction(actions.saveUser.request, () => initialState.savedUser)
|
||||
.handleAction(actions.saveUser.success, (store, action) => action.payload)
|
||||
@@ -57,9 +105,16 @@ const savedUser = createReducer<CreateResponse<User | null>, ActionTypes>(initia
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
serviceUsers,
|
||||
regularUsers,
|
||||
loading,
|
||||
failed,
|
||||
user,
|
||||
savedUser,
|
||||
updateUserDrawerVisible
|
||||
deletedUser,
|
||||
updateUserDrawerVisible,
|
||||
inviteUserPopupVisible,
|
||||
editUserPopupVisible,
|
||||
addServiceUserPopupVisible,
|
||||
userTabOpen
|
||||
});
|
||||
|
||||